Question

I want to be able to create case classes in Scala using spray-json, but define an asJson method in the class , but I can't seem to figure out how. For example, I'd like to be able to do this:

case class Foo(bar: String) {
  def toJson: JsValue = ...
}

It would be easy enough to create an implicit JSON converter:

object JsonProtocols extends DefaultJsonProtocol {
  implicit val fooFormat = jsonFormat1(Foo)
}

But as far as I know, this can only be done outside of the class. I would love to find a way of declaring a JSON format and convert to JSON within the class itself.

Was it helpful?

Solution

You could imagine doing this:

scala> import spray.json._
import spray.json._

scala> case class Foo(bar: String) {
  def toJson:JsValue = JsObject( "bar" -> JsString(bar) )
}
defined class Foo

scala> Foo("bar").toJson
res2: spray.json.JsValue = {"bar":"bar"}

So far so good, but that doesn't fit into Spray's typeclass mechanism. Spray's routing DSL, for example, would give you a type error if you then tried to convert a Foo to/from a JsValue (e.g. using the route entity( as[Foo] ) { ... } ). Nor could the implicits they have already prepared for you, for types like List and Set, work with Foo:

scala> import DefaultJsonProtocol._
import DefaultJsonProtocol._

scala> List(Foo("bar")).toJson
<console>:31: error: Cannot find JsonWriter or JsonFormat type class for List[Foo]
              List(Foo("bar")).toJson

because there is no JsonFormat class for them to use to convert a Foo, like the one that JsonFormat1(Foo) would have created.

You might then think to put the format inside the Foo companion object, since an in-scope class's companion object is on the implicit search path, like so:

object Foo extends DefaultJsonProtocol {
  implicit val fooFormat = jsonFormat1(Foo)
}
case class Foo(bar: String)

But because we're not finished defining Foo at that point, the compiler gives us a type error:

[error]  found   : Foo.type
[error]  required: ? => ?
[error]  Note: implicit value fooFormat is not applicable here because it comes after the application point and it lacks an explicit result type

Adding an explicit result type of RootJsonFormat[Foo] doesn't solve the problem:

[error]  found   : Foo.type
[error]  required: ? => Foo
[error]   implicit val fooFormat:RootJsonFormat[Foo] = jsonFormat1(Foo)

The trick (thanks knutwalker!) is to explicitly pass Foo.apply:

object Foo extends DefaultJsonProtocol {
  implicit val fooFormat = jsonFormat1(Foo.apply)
}
case class Foo(bar: String)
Licensed under: CC-BY-SA with attribution
Not affiliated with StackOverflow
scroll top