Working with JSON in Play 2.1

Play 2.1 now provides a really nice library for formatting Scala objects as JSON. As we’ve migrated from Play 2.0 to Play 2.1, we’ve been taking advantage of several of its features to reduce the size and complexity of our code.

To demonstrate how we use it, let’s start with a simple example. Suppose I have a Person object that I want to both serialize and deserialize:

case class Person(
  id: Option[Long] = None, firstName: String, lastName: String, birthDate: LocalDate)

Here we use the macro-based Json.format to create a reader and writer for the Person class:

import play.api.libs.json._
 
implicit val personFormat = Json.format[Person]
 
Json.toJson(Person(Some(1), "Arthur", "Dent", "arthur@example.com", new LocalDate("1952-03-11")))
// => {"id":1,"firstName":"Arthur","lastName":"Dent",birthDate":"1952-03-11"}

But in our codebase things are a bit more complicated. We like to have type-safe IDs, and might use a Name wrapper for the first and last name, so our person might actually look like this:

case class Id[T](id: Long) { override def toString = id.toString } // in Id library
 
case class Name(firstName: String, lastName: String) {
  def firstLast = s"$firstName $lastName"
  def lastFirst = s"$lastName, $firstName"
}
case class Person(
  id: Option[Id[Person]] = None, name: Name, email: String, birthDate: LocalDate)

In that case we have a helper to generate our ID serializer for each ID type, and generate the Name serializer using Json.format

def idFormat[T]: Format[Id[T]] = Format(
  __.read[Long].map(Id(_)),
  new Writes[Id[T]]{ def writes(o: Id[T]) = JsNumber(o.id) })
 
implicit val personIdFormat = idFormat[Person]
implicit val nameFormat = Json.format[Name]
implicit val personFormat = Json.format[Person]
 
Json.toJson(Person(Some(Id(1)), Name("Arthur","Dent"), "arthur@example.com", new LocalDate("1952-03-11")))
// => {"id":1,"name":{"firstName":"Arthur","lastName":"Dent"},"email":"arthur@example.com","birthDate":"1952-03-11"}

You can see that we need to be a bit more verbose when creating formatters for single-element case classes because of limitations in the Play API.

Using functional combinators

Sometimes we want the external JSON to be structured differently than our Scala code. Suppose we want to place the firstName and lastName properties directly in the JSON for a Person and want to validate the email address format. Then we can use the functional combinators to construct our Format[Person]:

import play.api.libs.functional.syntax._
 
implicit val personFormat: Format[Person] = (
  (__  "id").formatNullable[Id[Person]] and
  ((__  "firstName").format[String] and
    (__  "lastName").format[String])(Name.apply, unlift(Name.unapply)) and
  (__  "email").format(Reads.email) and
  (__  "birthDate").format[LocalDate]
)(Person.apply, unlift(Person.unapply))
 
Json.toJson(Person(Some(Id(0)), Name("Arthur","Dent"), "arthur@example.com", new LocalDate("1952-03-11")))
// => {"id":0,"firstName":"Arthur","lastName":"Dent","email":"arthur@example.com","birthDate":"1952-03-11"}
 
Json.fromJson[Person](Json.parse("""{"id":0,"firstName":"Arthur","lastName":"Dent","email":"arthur@example.com","birthDate":"1952-03-11"}"""))
// => JsSuccess(Person(Some(Id(0)),Name(Arthur,Dent),arthur@example.com,1952-03-11),)
Json.fromJson[Person](Json.parse("""{"id":0,"firstName":"Arthur","lastName":"Dent","email":"example.com","birthDate":"1952-03-11"}"""))
// => JsError(List((/email,List(ValidationError(validate.error.email,WrappedArray())))))

Notice we used the provided Reads.email to validate the email (along with the implicit string writer). In practice, we might use an Email value class wrapper and write a serializer for that, but I’ll leave that as an exercise for the reader.

Backwards compatibility

If we’ve decided to store our JSON anywhere and then change the serializer, we might run into backwards-compatibility issues when reading back the old JSON. There are a couple of ways to deal with this problem. One is to keep your old reader around and define a new reader using orElse:

val personFormatOld: Format[Person] = ??? // old serializer
val personFormat: Format[Person] = ??? // new serializer
implicit val personReads: Reads[Person] = personFormat orElse personFormatOld
implicit val personWrites: Writes[Person] = personFormat

We can also define default values for properties we added. Let’s say I just added the email property:

implicit val personFormat: Format[Person] = (
  (__  "id").formatNullable[Id[Person]] and
  (__  "name").format[Name] and
  (__  "email").formatNullable(Format(Reads.email, Writes.StringWrites))
    .inmap[String](_.getOrElse(""), Some(_).filterNot(_.isEmpty)) and
  (__  "birthDate").format[LocalDate]
)(Person.apply, unlift(Person.unapply))
 
Json.fromJson[Person](Json.parse("""{"id":0,"name":{"firstName":"Arthur","lastName":"Dent"},"birthDate":"1952-03-11"}"""))
// => JsSuccess(Person(Some(0),Name(Arthur,Dent),,1952-03-11),)

Note that we can use Format#inmap (or Reads#map/Writes#contramap) to do a transformation between the Option[String] we get from formatNullable and a String with a default value.

Formatting DateTimes

It’s also nice to have DateTime values printed as human-readable strings (as is the case for LocalDate shown in our example). We can easily define a formatter to do this. The format pattern used is the same as in SimpleDateFormat.

import play.api.libs.json._
 
val pattern = "yyyy-MM-dd'T'HH:mm:ssz"
implicit val dateFormat =
  Format[DateTime](Reads.jodaDateReads(pattern), Writes.jodaDateWrites(pattern))
 
Json.toJson(new DateTime("1952-03-11")) // "1952-03-11T00:00:00PDT"

Conclusion

As you can see, Play 2.1 makes it really easy to read and write Scala case classes as JSON. We can create simple serializers using the macro-based approach, and deal with more complex cases using the functional combinators.

For more comprehensive information, you might find Play’s documentation on JSON combinators and handling JSON requests to be helpful.

We wrote this post while working on Kifi — tools that help people outsmart information overload together. Learn more.

1 comments