Have you ever needed to write your polymorphic class or trait to JSON, and realized that you can’t just do it with one line of code? (Oh, c’mon, sure you have — we all have at one time or another!)
Ok, so here’s the situation: I really like the play-json
library put out by Lightbend. I mean, the convenience of being able to do this is awesome:
import play.api.libs.json._ implicit val geolocation = Json.format[Geolocation] Json.toJson(someGeoLocation)
The fact that I don’t actually have to write my JSON serializer and deserializer code is just convenient. Of course other libraries do this too, but many rely on reflection. Not so with play-json
since it figures it all out at compile-time. It’s fast and efficient, and I love it. Plus, the dialect for working with JSON directly is pretty straightforward.
But every now and then I do run into a limitation. Let’s say I’ve got a polymorphic structure like this:
trait Customer { val customerNumber: Option[String] } case class BusinessCustomer(name: String, ein: String, customerNumber: Option[String] = None) extends Customer case class IndividualCustomer(firstName: String, lastName: String, ssn: String, customerNumber: Option[String] = None) extends Customer implicit val individualFormat = Json.format[IndividualCustomer] implicit val businessFormat = Json.format[BusinessCustomer]
Unfortunately, I can’t just declare an implicit Format
for the Customer
trait. If you think about it, that makes sense… how would the compiler be able to figure that out? But it leaves me with a bit of a problem. Let’s say I serialize an IndividualCustomer
, I end up with this:
val u = IndividualCustomer("Zaphod", "Beeblebrox", "001-00-0001") Json.toJson(u) // {"firstName":"Zaphod","lastName":"Beeblebrox","ssn":"001-00-0001"}
There’s no context there. Now when I go to deserialize the JSON representation I have to know, in advance, that it’s going to be an IndividualCustomer
and not a BusinessCustomer
. Hence, I can’t just deserialize a Customer
and get the right type, automatically.
Containers to the rescue
So there is a simple solution, it turns out. What we can do is wrap the JSON in some kind of contextual information. For example, if we can change the JSON to include type information:
{ "type":"IndividualCustomer", "value": { "firstName":"Zaphod","lastName":"Beeblebrox","ssn":"001-00-0001" }}
This is pretty straightforward to implement using a simple container class. The container itself is going to have to handle the abstraction of the Customer
, so I’ll write a custom Format
that adds the extra context I need:
case class Container(customer: Customer) implicit val containerFormat = new Format[Container] { def writes(container: Container) = Json.obj( "type" -> container.customer.getClass.getSimpleName, "value" -> { container.customer match { case c: IndividualCustomer => Json.toJson(c) case c: BusinessCustomer => Json.toJson(c) } } ) def reads(json: JsValue) = { val v: JsValue = (json \ "value").get val c: String = (json \ "type").as[String] val z = c match { case "IndividualCustomer" => v.as[IndividualCustomer] case "BusinessCustomer" => v.as[BusinessCustomer] } new JsSuccess(Container(z)) } }
With this custom Format
(and the corresponding reads
and writes
functions) I can now serialize any Container
to JSON, and get enough contextual information so that I can deserialize back to the correct type:
val c1 = Container(u) Json.toJson(c1) // yay! context! // {"type":"IndividualCustomer","value":{"firstName":"Zaphod","lastName":"Beeblebrox","ssn":"001-00-0001"}}
Since we are using the Container
around all instances of the Customer
trait, we get context. Now we can read and write our abstracted Customer
instances to and from JSON:
val someJsonString = """{"type":"IndividualCustomer","value":{"firstName":"Zaphod","lastName":"Beeblebrox","ssn":"001-00-0001"}}""" val someJsValue = Json.parse(someJsonString) val backAgain = Json.fromJson[Container](someJsValue) val maybeContainer = backAgain.asOpt assert(maybeContainer.isInstanceOf[Option[Container]])
Alternatives
Another approach I’ve used is to put the wrapper logic into the trait itself. Personally, I don’t like that approach for a couple of reasons. First of all, it clutters the trait with things that have nothing to do with the trait’s purpose. Second of all, it eliminates an otherwise nice separation of concerns. I’d rather keep my traits pure, and add in something like Container
(or, perhaps call it a CustomerJSONContainer
if you like). I could easily enough see a hybrid approach that implements a trait, such as AddsCustomerContext
, and then mixing that trait in.
About the only thing I’m not happy about is the relatively static bit of code in Container
. If I could find a way to avoid match cases on each Customer
type, that would be ideal… but then, if I could do that, Scala would probably be able to create an implicit Format[Customer]
and none of this would be necessary in the first place.