1
votes

I have a class hierarchy that represents filter types, and one of the types contains a list of the base types. I cannot figure out how to set up spray-json formats for these types because the formatters for the base type and the containing type need to refer to each other.

Let's start with a class hierarchy and json formats where the problematic parts are commented out:

object Filters {
  sealed trait Filter
  case class SimpleFilter(foo: String) extends Filter
  case class DoubleFilter(foo: String, bar: String) extends Filter
  implicit val simpleFormat = jsonFormat1(SimpleFilter)
  implicit val doubleFormat = jsonFormat2(DoubleFilter)

//  case class AndFilter(filters: List[Filter]) extends Filter
//  implicit val andFormat = lazyFormat(jsonFormat1(AndFilter))    

  // (would really use a type field, keeping simple for example)
  implicit val filterFormat = new RootJsonFormat[Filter] {
    override def write(obj: Filter): JsValue = obj match {
      case x: SimpleFilter => x.toJson
      case x: DoubleFilter => x.toJson
//    case x: AndFilter => x.toJson
    }

    override def read(json: JsValue): Filter = json.asJsObject.getFields("bar") match {
      case Seq(_) => json.convertTo[DoubleFilter]
      case Seq()  => json.convertTo[SimpleFilter]
    }
  }
}

This compiles and works as expected, I can serialize and deserialize concrete filter subclasses as Filter no problem.

But let's comment in the AndFilter stuff. Now there is trouble! With the declaration of andFormat ahead of filterFormat (as above), it won't compile because andFormat needs filterFormat:

Error:(17, 43) could not find implicit value for evidence parameter of type spray.json.DefaultJsonProtocol.JF[List[classpath.Filters.Filter]] implicit val andFormat = jsonFormat1(AndFilter)

Switching the order around to andFormat after filterFormat will let things compile. But of course I also want to add andFormat-referencing clauses to filter format, i.e. case x: AndFilter => x.toJson in the write method and whatever including json.convertTo[AndFilter] in the read method. And that doesn't compile either:

Error:(23, 34) Cannot find JsonWriter or JsonFormat type class for classpath.Filters.Filter with classpath.Filters.AndFilter case x: AndFilter => x.toJson

I can't find any way around this. I've tried spray-json's lazyFormat but it doesn't help (only works for recursive self references, not a cross reference like this). Any ideas?

2

2 Answers

1
votes

I believe the following modification resolves the issue with implicit resolution of JsonFormat[AndFilter], stated in the question.

implicit val andFormat: JsonFormat[AndFilter] = lazyFormat(jsonFormat1(AndFilter))

Please note that, we need to supply explicit type annotation (i.e.,JsonFormat[AddFilter]) for andFormat so that SparyJsonSupport can pick it up as an instance of RootJsonFormat, instead of JsonFormat returned by lazyFormat:

  import spray.json._
  import DefaultJsonProtocol._

  object Filters {
    sealed trait Filter
    case class SimpleFilter(foo: String) extends Filter
    case class DoubleFilter(foo: String, bar: String) extends Filter
    case class AndFilter(filters: List[Filter]) extends Filter

    implicit val simpleFormat = jsonFormat1(SimpleFilter)
    implicit val doubleFormat = jsonFormat2(DoubleFilter)
    implicit val andFormat: JsonFormat[AndFilter] = lazyFormat(jsonFormat1(AndFilter))

    implicit val filterFormat = new RootJsonFormat[Filter] {
      override def write(obj: Filter): JsValue = ???
      override def read(json: JsValue): Filter = ???
    }
  }

For details: please see https://github.com/spray/spray-json#jsonformats-for-recursive-types.

1
votes

Sometimes you need to help the compiler a bit. Adding an explicit type on the filterFormat and an explicit write will make it compile.

sealed trait Filter
case class SimpleFilter(foo: String) extends Filter
case class DoubleFilter(foo: String, bar: String) extends Filter
case class AndFilter(filters: List[Filter])
implicit val simpleFormat = jsonFormat1(SimpleFilter)
implicit val doubleFormat = jsonFormat2(DoubleFilter)
implicit val andFormat    = jsonFormat1(AndFilter)

implicit val filterFormat: RootJsonFormat[Filter] = new RootJsonFormat[Filter] {
  override def write(obj: Filter): JsValue = obj match {
    case x: SimpleFilter => x.toJson
    case x: DoubleFilter => x.toJson
    case x: AndFilter    => andFormat.write(x)
  }

  override def read(json: JsValue): Filter = json.asJsObject.getFields("bar") match {
    case Seq(_) => json.convertTo[DoubleFilter]
    case Seq()  => json.convertTo[SimpleFilter]
  }
}