Search code examples
jsonscalaplayframework-2.2

Defaults for missing properties in play 2 JSON formats


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)


Solution

  • 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