JsonFormat for abstract class with generic paramet

2019-08-17 05:23发布

问题:

I am trying to write a JsonFormat for an abstract class with a generic parameter that looks like something like this:

abstract class Animal[A] {
    def data: A
    def otherStuff: String = "stuff"
}
case class CatData(catField: String)
case class Cat(data: CatData) extends Animal[CatData]

So far my attempt at this looks like:

object AnimalProtocol extends DefaultJsonProtocol {

    implicit val catDataFormat = jsonFormat1(CatData)
    implicit val catFormat = jsonFormat1(Cat)

    implicit def animalFormat[T <: Animal[T]](t: T)(implicit fmt: JsonWriter[T]) = new RootJsonFormat[Animal[T]] {
    def write(obj: Animal[T]) = obj match {
        case x: Cat => catFormat.write(x)
    }

    def read(json: JsValue) = ???
  }

Now, if I try to do this:

import AnimalProtocol._
val cat: Animal[CatData] = Cat(CatData("this is cat data"))

I get the compiler error:

Cannot find JsonWriter or JsonFormat type class for Animal[CatData]

How can I make it work? In the end I want to write json with the fields in Animal and with data set to whatever case class applies.

回答1:

You need to provide a type parameter for both the generic field and the subclass of Animal in your implicit def:

object AnimalProtocol2 extends DefaultJsonProtocol {

  implicit val catDataFormat = jsonFormat1(CatData)

  implicit def animalFormat[A, T <: Animal[A]](implicit fmt: JsonWriter[A]): RootJsonFormat[T] = new RootJsonFormat[T] {
    def write(obj: T) = {
      JsObject(
        "data" -> obj.data.toJson,
        "otherStuff" -> obj.otherStuff.toJson
      )
    }

    def read(json: JsValue) = ???
  }
}

That also allows you to get rid of pattern matching on subclasses inside animalFormat.



回答2:

I don't use spray-json (I've got much more experience with play-json), but I'll try to help by pointing at a few strange things in your code.

I'm not sure you need implicit val catFormat = jsonFormat1(Cat), unless you want it to be applied instead of animalFormat when type is known to be Cat.

You definition of animalFormat looks wrong/strange for the following reasons:

  • type is strange, T <: Animal[T] doesn't correspond to your types i.e., you don't have CatData <: Animal[CatData]
  • you don't use t
  • you don't use fmt (but instead you pattern match on obj)

I would suggest to either define a static animalFormat, something like (not sure about the wildcard type _):

val animalFormat: RootJsonFormat[Animal[_]] = new RootJsonFormat[Animal[_]] {
    def write(obj: Animal[_]) = {
      JsObject(
        "otherStuff" -> JsString(obj.otherStuff),
        "data" -> obj match {
          case x: Cat => catDataFormat.write(x.data)
        }
      )

    def read(json: JsValue) = ???
  }

or, without using pattern matching:

implicit def animalFormat[T](implicit fmt: JsonWriter[T]) = new RootJsonFormat[Animal[T]] {
    def write(obj: Animal[T]) = 
          JsObject(
            "otherStuff" -> JsString(obj.otherStuff),
            "data" -> fmt.write(obj.data)
          )

    def read(json: JsValue) = ???
  }

Note that with this approach, you won't be able to read a generic Animal as there's no type information in the json.