JSON Madness in Play with Scala
The thing with typesafety is, that it gets in your way all the time, making the simplest of things incredibly complicated. Still I remain convinced that it is the right thing to do, at least for now.
So this is post is dedicated towards getting started with JSON in Play 2.2 fast.
Say you wanted to write some kind of REST service. What you probably have to do at some point is to send case classes over the wire. You would think that it can’t be too hard to do, especially when you did something similar in node.js without even thinking about it.
To be honest, after digging long enough and finding the right piece of documentation it’s actually pretty easy.
For a simple case class all you need is:
case class Contact(id: Option[String],
name: String,
username: String,
date: Long = 0)
object Contact {
// note: severe scala madness going on in the background
implicit val format = Json.format[Contact]
}
The important part here is in the companion object. It uses Scala macros to generated a bunch of boilerplate code at compile time.
Also note the type of the id, it’s Option[String]
. This means that a contact can have an id or not. A missing id is common with REST when a new model gets POSTed to the server. Bottomline: Just make sure everything that might be missing is of type Option
.
What you want to know is, that after you’ve taken care of this you can serve Contact
objects on HTTP requests by passing them directly to the toJson
method:
def get = Action {
Ok(Json.toJson(Contact(...)))
}
So far so good, but it gets a bit crazy when you want do something special like serving a list of two different types on a request:
def get = Action {
Ok(Json.toJson(
Seq(
Transaction(...),
Notification(...),
Transaction(...),
Notification(...),
Transaction(...)
)
))
}
Here we want to serve a sequence of Transaction
and Notification
objects. Obviously we will need a common base type, otherwise the type checker infers something pretty unexpected…
trait ListEntry {
def id: Option[String]
}
case class Notification(...) extends ListEntry
case class Transaction(...) extends ListEntry
Note that both these case classes need an implicit formatter in their companion objects as well, as in the previous example.
Now the compiler will still complain, because it can’t find an implicit formatter that will turn a ListEntry
into a JsValue
. The right place to define such a formatter is in the companion object of ListEntry
:
object ListEntry {
implicit val writes = new Writes[ListEntry] {
def writes(c: ListEntry): JsValue = c match {
case t: Transaction => Transaction.format.writes(t)
case n: Notification => Notification.format.writes(n)
}
}
}
This is obviously not the greatest solution in the world, but apparently it’s typesafe. What’s happening is that we find out the subtype of the ListEntry
and then have its own formatter do the work. Done.