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.
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
.
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.