5

spray-json relies on the presence of an in-scope, implicit JsonWriter[T] when calling toJson on an instance of T.

Say I have a trait with several concrete subtypes, each of which has a JsonWriter:

trait Base
case class Foo(a: Int) extends Base
case class Bar(a: Int, b: Int) extends Base
implicit val FooFormat = jsonFormat1(Foo)
implicit val BarFormat = jsonFormat2(Bar)

def go(o: Base) = {
    o.toJson
}

go doesn't compile because there's no JsonWriter for Base, even though there are writers for all of the concrete subtypes.

How can I reorganize this code such that generic functions of Base use the appropriate json formatters?

ChrisB
  • 4,628
  • 7
  • 29
  • 41
  • Wrap your implicits in an object and import to the scope of `go`. You may still have an issue with the trait itself but those implicits aren't going to be available for `go` unless you import it. – Ayubinator Mar 08 '16 at 18:29
  • Sorry if my snippet was unclear -- my question is about how to write a function on Base that uses the appropriate concrete implicit, assuming the set of these implicits is properly in-scope where it needs to be. As it stands I know how to write `go` if its argument is `(o: Foo)`, but not `(o: Base)`. – ChrisB Mar 08 '16 at 19:30

3 Answers3

6

You can use a generic method with type and context bounds. Like this:

def go[T <: Base : JsonWriter](t: T) = t.toJson
3

Marshalling Base would require a JsonFormat. Following is one of the ways to approach the stated issue.

import spray.json._

object BaseJsonProtocol extends DefaultJsonProtocol {
  implicit val FooFormat = jsonFormat1(Foo)
  implicit val BarFormat = jsonFormat2(Bar)

  implicit object BaseJsonFormat extends RootJsonFormat[Base] {
    def write(obj: Base) = obj match {
      case x: Foo ⇒ x.toJson
      case y: Bar ⇒ y.toJson
      case unknown @ _ => serializationError(s"Marshalling issue with ${unknown}")
    }

    def read(value: JsValue) = {
      value.asJsObject.getFields("a","b") match {
        case Seq(JsNumber(a)) => Foo(a.toInt)
        case Seq(JsNumber(a), JsNumber(b)) => Bar(a.toInt, b.toInt)
        case unknown @ _ => deserializationError(s"Unmarshalling issue with ${unknown} ")
    }
  }
 }
}

As a result, the BaseJsonProtocol can be used to marshall an instance of Base as follows.

import BaseJsonProtocol._

def go(o: Base) =  o.toJson

assert (go(Foo(10)).convertTo[Foo] == Foo(10))
assert (go(Foo(10).asInstanceOf[Base]).convertTo[Foo] == Foo(10))
assert (go(Bar(10,100)).convertTo[Bar] == Bar(10, 100))

Hope it helps!

Adil Akhter
  • 192
  • 3
  • 12
1

I realized one solution is to make go a generic function with a type bound, and declare the implicit value..."explicitly"

def go[T <: Base](t: T)(implicit fmt: JsonWriter[T]) =
    t.toJson

Now the function can compile because the JsonWriter is promised as a function parameter, and each call site can pull in a concrete implementation of JsonWriter[T] depending on the context (FooFormat or BarFormat).

ChrisB
  • 4,628
  • 7
  • 29
  • 41
  • Without a `JsonFormat` for `Base`, for instance: `go(anyBase)` where `val anyBase: Base = Foo(1)` won't compile. – Adil Akhter Mar 09 '16 at 10:42
  • yeah that's a good point -- beyond your implementation of BaseJsonFormat, is there another way to implement it that doesn't require a case statement that enumerates all subtypes of Bar? Conceptually it seems like all the relevant logic (the concrete JsonWriters + type hierarchy) is already defined, and it was bothering me that somewhere I had to duplicate these pieces of code again -- either in a BaseJsonFormat or in separate go functions for each type. – ChrisB Mar 10 '16 at 02:35