Defaults for missing properties in play 2 JSON formats

Jean picture Jean · Dec 16, 2013 · Viewed 12.5k times · Source

I have an equivalent of the following model in play scala :

case class Foo(id:Int,value:String)
object Foo{
  import play.api.libs.json.Json
  implicit val fooFormats = Json.format[Foo]
}

For the following Foo instance

Foo(1, "foo")

I would get the following JSON document:

{"id":1, "value": "foo"}

This JSON is persisted and read from a datastore. Now my requirements have changed and I need to add a property to Foo. The property has a default value :

case class Foo(id:String,value:String, status:String="pending")

Writing to JSON is not a problem :

{"id":1, "value": "foo", "status":"pending"}

Reading from it however yields a JsError for missing the "/status" path.

How can I provide a default with the least possible noise ?

(ps: I have an answer which I will post below but I am not really satisfied with it and would upvote and accept any better option)

Answer

Jean picture Jean · Dec 16, 2013

Play 2.6+

As per @CanardMoussant's answer, starting with Play 2.6 the play-json macro has been improved and proposes multiple new features including using the default values as placeholders when deserializing :

implicit def jsonFormat = Json.using[Json.WithDefaultValues].format[Foo]

For play below 2.6 the best option remains using one of the options below :

play-json-extra

I found out about a much better solution to most of the shortcomings I had with play-json including the one in the question:

play-json-extra which uses [play-json-extensions] internally to solve the particular issue in this question.

It includes a macro which will automatically include the missing defaults in the serializer/deserializer, making refactors much less error prone !

import play.json.extra.Jsonx
implicit def jsonFormat = Jsonx.formatCaseClass[Foo]

there is more to the library you may want to check: play-json-extra

Json transformers

My current solution is to create a JSON Transformer and combine it with the Reads generated by the macro. The transformer is generated by the following method:

object JsonExtensions{
  def withDefault[A](key:String, default:A)(implicit writes:Writes[A]) = __.json.update((__ \ key).json.copyFrom((__ \ key).json.pick orElse Reads.pure(Json.toJson(default))))
}

The format definition then becomes :

implicit val fooformats: Format[Foo] = new Format[Foo]{
  import JsonExtensions._
  val base = Json.format[Foo]
  def reads(json: JsValue): JsResult[Foo] = base.compose(withDefault("status","bidon")).reads(json)
  def writes(o: Foo): JsValue = base.writes(o)
}

and

Json.parse("""{"id":"1", "value":"foo"}""").validate[Foo]

will indeed generate an instance of Foo with the default value applied.

This has 2 major flaws in my opinion:

  • The defaulter key name is in a string and won't get picked up by a refactoring
  • The value of the default is duplicated and if changed at one place will need to be changed manually at the other