Friday, November 01, 2013

Add/Remove fields to Play's default case class JSON Writes

Play's JSON Macro Inception is great. For most of the time all you need to write for JSON parsing/serialization is something like:
implicit val personFormat = Json.format[Person]
In this example, Person is a case class model in your app.
case class Person(name: String, age: Int)
The implicit personFormat will silently help you whenever you need to either parse a piece of JSON to an instance of Person or serialize an instance of Person to a JSON string.
Now, let's add a boolean val inside the class constructor: isAdult
case class Person(name: String, age: Int) {
  val isAdult: Boolean = age >= 18
}
Since Play!'s JSON Macro Inception only works with the default apply and unapply methods of the case class, it won't include the isAdult in the JSON writes (reads doesn't make much sense here). There isn't much documentation on how to include this new field while still take advantage of the default Writes generated by Play's JSON Macro. Here is how you can achieve it using writes composition.
val personWrites = Json.writes[Person]
val writesWithNewField: Writes[Person] = (personWrites ~ (__ \ "isAdult").write[Boolean])((p: Person) => (p, p.isAdult))
This is not bad, but we can also introduce some generalized helper to minimize the boilerplate. Introducing the OWritesOpts class:
class OWritesOps[A](writes: OWrites[A]) {
  def addField[T: Writes](fieldName: String, field: A => T): OWrites[A] = 
    (writes ~ (__ \ fieldName).write[T])((a: A) => (a, field(a)))


  def removeField(fieldName: String): OWrites[A] = OWrites { a: A =>
    val transformer = (__ \ fieldName).json.prune
    Json.toJson(a)(writes).validate(transformer).get
  }
}

object OWritesOps {
  implicit def from[A](writes: OWrites[A]): OWritesOps[A] = new OWritesOps(writes)
}

With this class, you can add/remove field to default JSON writes like this:
//writes that replaces the age field with the isAdult field in the JSON output. 
val customWrites: Writes[Person] = Json.Writes[Person].
                                     addField("isAdult", _.isAdult).
                                     removeField("age")

That's it, enjoy. I am still new to Play!, if you have a better alternative, please suggest it in the comment. Will appreciate it, that's partly why I wrote this little blog post.