37
votes

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)

7
Hi Jean. I've been digging into this issue and found this. There doesn't seem to be any activity except talking around it. Have you found a more elegant solution than a transformer yet?Yar
I am still using the transformers for now. Until I get enough time to look at how to write my own macros I think that's all I will be able to use :(Jean

7 Answers

41
votes

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
21
votes

The cleanest approach that I've found is to use "or pure", e.g.,

...      
((JsPath \ "notes").read[String] or Reads.pure("")) and
((JsPath \ "title").read[String] or Reads.pure("")) and
...

This can be used in the normal implicit way when the default is a constant. When it's dynamic, then you need to write a method to create the Reads, and then introduce it in-scope, a la

implicit val packageReader = makeJsonReads(jobId, url)
17
votes

An alternative solution is to use formatNullable[T] combined with inmap from InvariantFunctor.

import play.api.libs.functional.syntax._
import play.api.libs.json._

implicit val fooFormats = 
  ((__ \ "id").format[Int] ~
   (__ \ "value").format[String] ~
   (__ \ "status").formatNullable[String].inmap[String](_.getOrElse("pending"), Some(_))
  )(Foo.apply, unlift(Foo.unapply))
6
votes

I think the official answer should now be to use the WithDefaultValues coming along Play Json 2.6:

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

Edit:

It is important to note that the behavior differs from the play-json-extra library. For instance if you have a DateTime parameter that has a default value to DateTime.Now, then you will now get the startup time of the process - probably not what you want - whereas with play-json-extra you had the time of the creation from the JSON.

2
votes

I was just faced with the case where I wanted all JSON fields to be optional (i.e. optional on user side) but internally I want all fields to be non-optional with precisely defined default values in case the user does not specify a certain field. This should be similar to your use case.

I'm currently considering an approach which simply wraps the construction of Foo with fully optional arguments:

case class Foo(id: Int, value: String, status: String)

object FooBuilder {
  def apply(id: Option[Int], value: Option[String], status: Option[String]) = Foo(
    id     getOrElse 0, 
    value  getOrElse "nothing", 
    status getOrElse "pending"
  )
  val fooReader: Reads[Foo] = (
    (__ \ "id").readNullable[Int] and
    (__ \ "value").readNullable[String] and
    (__ \ "status").readNullable[String]
  )(FooBuilder.apply _)
}

implicit val fooReader = FooBuilder.fooReader
val foo = Json.parse("""{"id": 1, "value": "foo"}""")
              .validate[Foo]
              .get // returns Foo(1, "foo", "pending")

Unfortunately, it requires writing explicit Reads[Foo] and Writes[Foo], which is probably what you wanted to avoid? One further drawback is that the default value will only be used if the key is missing or the value is null. However if the key contains a value of the wrong type, then again the whole validation returns a ValidationError.

Nesting such optional JSON structures is not a problem, for instance:

case class Bar(id1: Int, id2: Int)

object BarBuilder {
  def apply(id1: Option[Int], id2: Option[Int]) = Bar(
    id1     getOrElse 0, 
    id2     getOrElse 0 
  )
  val reader: Reads[Bar] = (
    (__ \ "id1").readNullable[Int] and
    (__ \ "id2").readNullable[Int]
  )(BarBuilder.apply _)
  val writer: Writes[Bar] = (
    (__ \ "id1").write[Int] and
    (__ \ "id2").write[Int]
  )(unlift(Bar.unapply))
}

case class Foo(id: Int, value: String, status: String, bar: Bar)

object FooBuilder {
  implicit val barReader = BarBuilder.reader
  implicit val barWriter = BarBuilder.writer
  def apply(id: Option[Int], value: Option[String], status: Option[String], bar: Option[Bar]) = Foo(
    id     getOrElse 0, 
    value  getOrElse "nothing", 
    status getOrElse "pending",
    bar    getOrElse BarBuilder.apply(None, None)
  )
  val reader: Reads[Foo] = (
    (__ \ "id").readNullable[Int] and
    (__ \ "value").readNullable[String] and
    (__ \ "status").readNullable[String] and
    (__ \ "bar").readNullable[Bar]
  )(FooBuilder.apply _)
  val writer: Writes[Foo] = (
    (__ \ "id").write[Int] and
    (__ \ "value").write[String] and
    (__ \ "status").write[String] and
    (__ \ "bar").write[Bar]
  )(unlift(Foo.unapply))
}
1
votes

This probably won't satisfy the "least possible noise" requirement, but why not introduce the new parameter as an Option[String]?

case class Foo(id:String,value:String, status:Option[String] = Some("pending"))

When reading a Foo from an old client, you'll get a None, which I'd then handle (with a getOrElse) in your consumer code.

Or, if you don't like this, introduce an BackwardsCompatibleFoo:

case class BackwardsCompatibleFoo(id:String,value:String, status:Option[String] = "pending")
case class Foo(id:String,value:String, status: String = "pending")

and then turn that one into a Foo to work with further on, avoiding to have to deal with this kind of data gymnastics all along in the code.

1
votes

You may define status as an Option

case class Foo(id:String, value:String, status: Option[String])

use JsPath like so:

(JsPath \ "gender").readNullable[String]