Serializing JSON and Generic Classes with spray-json
Continuing on from where the last post left off, let's talk about structure. Whether or not you're working in an object oriented paradigm or a functional one, having base classes and a nice class hierarchy can make your code easier to understand and use. Of course since we'll be working in example code, none of these classes will actually be useful, but hey, you can always apply the concepts yourself.
So let's say you've got a base class and a couple of subclasses:
class Base(msg: String) case class Thing1(msg: String, thing1Thing: String) extends Base(msg) case class Thing2(msg: String, thing2Thing: String) extends Base(msg)
It's easy to define a few conversion implicits:
import spray.json._ import spray.json.DefaultJsonProtocol._ implicit val thing1Conversion = jsonFormat2(Thing1) implicit val thing2Conversion = jsonFormat2(Thing2)
ning
And we can use the toJson
and convertTo[ClassName]
methods from the spray
package just fine. But what about when we want to do something like this:
case class ServiceResponse(status: Int, result: Base)
And then define a serializer for that? Attempting to create one with the jsonFormat2
call will fail:
implicit val srConv = jsonFormat2(ServiceResponse) <console>:23: error: could not find implicit value for evidence parameter of type spray.json.DefaultJsonProtocol.JF[Base]
and conveniently tell you that you need to define a protocol for the Base
class.
Because case classes inherentance is prohibited by the compiler, we need to handle
Base
as we would any other normal class. So we provide JsonFormat[T]
for it.
The write method is simple:
implicit object ColorJsonFormat extends RootJsonFormat[Base] { def write(c: Base) = c match { case s: Thing2 => JsObject(("msg",JsString(s.msg)), ("thing2Thing",JsString(s.thing2Thing))) case s: Thing1 => JsObject(("msg",JsString(s.msg)), ("thing1Thing",JsString(s.thing1Thing))) case _ => serializationError(s"Could not write object $c") } ...
We pattern match according to the type, then proceed to construct a simple JSON object out of the data. What about reading?
def read(json: JsValue) = { json match { case JsObject(map) => List("msg","thing1Thing","thing2Thing").map(i => map.contains(i)).toArray match { case Array(true,true,false) => Thing1(map("msg").toString, map("thing1Thing").toString) case Array(true,false,true) => Thing2(map("msg").toString, map("thing2Thing").toString) case _ => deserializationError("fields invalid") } case _ => deserializationError("Base expected") } }
The above is a bit clunky since we can't pattern match against a Map. But you can now use the ServiceResponse like so:
implicit val srConv = jsonFormat2(ServiceResponse) """{"status" : 200, "result" :{"msg":"a","thing1Thing":"t"}}""".parseJson.convertTo[ServiceResponse] //res0: ServiceResponse = ServiceResponse(200,Thing1("a","t"))
But we can make this code a bit easier to deal with by leveraging implicit conversions on each of our subclasses:
implicit val thing1Conversion = jsonFormat2(Thing1) implicit val thing2Conversion = jsonFormat2(Thing2) class BaseConversion extends RootJsonFormat[Base] { def write(obj: Base) = obj match { case t1: Thing1 => t1.toJson case t2: Thing2 => t2.toJson case _ => serializationError("Could not serialize $obj, no conversion found") } def read(json: JsValue) = { val discrimator = List( "thing1Thing", //Thing1 unique field "thing2Thing" //Thing2 unique field ).map( d => json.asJsObject.fields.contains(d) ) discrimator.indexOf(true) match { case 0 => json.convertTo[Thing1] case 1 => json.convertTo[Thing2] case _ => deserializationError("Base expected") } } }
We still need to work a bit of voodoo on the read
function in order to discriminate
between the two different types, but so long as we always have a unique field to
go by, we'll be ok. The nice thing is that this works with Traits as well as classes.
Here's a full example I submitted to the spray user group mailing list:
package example import spray.json._ import spray.json.DefaultJsonProtocol._ sealed trait MyTrait case class Class1(someField: String) extends MyTrait case class Class2(someOtherField: Int) extends MyTrait case class Class3(yetAnotherField: String) extends MyTrait case class TestCase(works: Boolean, myClassWithTrait: MyTrait) object ImplicitConversions extends DefaultJsonProtocol { implicit val c1Conv = jsonFormat1(Class1) implicit val c2Conv = jsonFormat1(Class2) implicit val c3Conv = jsonFormat1(Class3) class MyTraitConversion extends RootJsonFormat[MyTrait] { def write(obj: MyTrait) = obj match { case c : Class1 => c.toJson case c : Class2 => c.toJson case c : Class3 => c.toJson case _ => serializationError(s"Could not write object $obj") } /* Read is kind of frustrating as we need to use the fields to determine which type to turn it into */ def read(json: JsValue) = { val discrimator = List( "someField", "someOtherField", "yetAnotherField" ).map( d => json.asJsObject.fields.contains(d) ) discrimator.indexOf(true) match { case 0 => json.convertTo[Class1] case 1 => json.convertTo[Class2] case 2 => json.convertTo[Class3] case _ => deserializationError("MyTrait expected") } } } } import spray.json._ object Example{ def main(args: Array[String]): Unit = { import ImplicitConversions._ implicit val mtc =new MyTraitConversion() implicit val tc = jsonFormat2(TestCase) val result = """{"works": true, "myClassWithTrait" : {"someOtherField" : 2}}""".parseJson.convertTo[TestCase] println(result) //example.TestCase = TestCase(true,Class2(2)) } }
Hopefully this gives you some more insight in how use spray's json library. Happy serializing! You can see all this code here on github
java.lang.RuntimeException: Cannot automatically determine case class field names and order for 'example.Thing1', please use the 'jsonFormat' overload with explicit field name specification