Defaults for missing properties in play 2 JSON for

2019-01-06 12:17发布

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条回答
贼婆χ
2楼-- · 2019-01-06 13:03

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))
}
查看更多
登录 后发表回答