Predeterminado para propiedades faltantes en el juego 2 formatos JSON

Tengo un equivalente del siguiente modelo en juego scala:

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

Para la siguiente instancia de Foo

 Foo(1, "foo") 

Me gustaría obtener el siguiente documento JSON:

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

Este JSON persiste y se lee desde un almacén de datos. Ahora mis requisitos han cambiado y necesito agregar una propiedad a Foo. La propiedad tiene un valor predeterminado:

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

Escribir a JSON no es un problema:

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

Sin embargo, la lectura de este da un error JsError por perder la ruta “/ estado”.

¿Cómo puedo proporcionar un valor predeterminado con el menor ruido posible?

(PD: Tengo una respuesta que publicaré a continuación pero no estoy muy satisfecho con ella y votaría y aceptaría una mejor opción)

Reproducir 2.6

Según la respuesta de @ CanardMoussant, comenzando con Play 2.6, se ha mejorado la macro play-json y se proponen múltiples funciones nuevas que incluyen el uso de los valores predeterminados como marcadores de posición cuando se deserializa:

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

Para jugar por debajo de 2.6, la mejor opción sigue siendo usar una de las siguientes opciones:

play-json-extra

Descubrí una solución mucho mejor para la mayoría de las deficiencias que tenía con play-json, incluida la de la pregunta:

play-json-extra que usa [play-json-extensions] internamente para resolver el problema particular en esta pregunta.

¡Incluye una macro que incluirá automáticamente los valores por defecto que faltan en el serializador / deserializador, haciendo que los refactores sean mucho menos propensos a errores!

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

hay más en la biblioteca que puede consultar: play-json-extra

Transformadores Json

Mi solución actual es crear un transformador JSON y combinarlo con las lecturas generadas por la macro. El transformador se genera por el siguiente método:

 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)))) } 

La definición de formato se convierte en:

 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) } 

y

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

de hecho generará una instancia de Foo con el valor predeterminado aplicado.

Esto tiene 2 fallas principales en mi opinión:

  • El nombre de la clave de incumplimiento está en una cadena y no será recogido por una refactorización
  • El valor del valor predeterminado se duplica y si se cambia en un lugar tendrá que cambiarse manualmente en el otro

El enfoque más limpio que he encontrado es utilizar “o puro”, por ejemplo,

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

Esto se puede usar en la forma implícita normal cuando el valor predeterminado es una constante. Cuando es dynamic, necesita escribir un método para crear las Lecturas, y luego introducirlo en el scope, a la

 implicit val packageReader = makeJsonReads(jobId, url) 

Una solución alternativa es usar formatNullable[T] combinado con inmap de 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)) 

Creo que la respuesta oficial ahora debería ser usar los valores predeterminados que vienen junto con Play Json 2.6:

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

Editar:

Es importante tener en cuenta que el comportamiento difiere de la biblioteca play-json-extra. Por ejemplo, si tiene un parámetro DateTime que tiene un valor predeterminado para DateTime.Now, entonces obtendrá el tiempo de inicio del proceso, probablemente no lo que quiere, mientras que con play-json-extra tuvo el tiempo de la creación del JSON.

Me acabo de enfrentar con el caso en el que quería que todos los campos JSON fueran opcionales (es decir, opcional para el usuario) pero internamente deseo que todos los campos no sean opcionales con valores predeterminados definidos con precisión en caso de que el usuario no especifique un determinado campo. Esto debería ser similar a tu caso de uso.

Actualmente estoy considerando un enfoque que simplemente envuelve la construcción de Foo con argumentos totalmente opcionales:

 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") 

Desafortunadamente, requiere escribir Reads[Foo] explícitas Reads[Foo] y Writes[Foo] , ¿cuál es probablemente lo que quería evitar? Otro inconveniente es que el valor predeterminado solo se utilizará si falta la clave o si el valor es null . Sin embargo, si la clave contiene un valor del tipo incorrecto, de nuevo la validación completa devuelve ValidationError .

Anidar tales estructuras JSON opcionales no es un problema, por ejemplo:

 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)) } 

Esto probablemente no satisfará el requisito de “mínimo ruido posible”, pero ¿por qué no introducir el nuevo parámetro como una Option[String] ?

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

Al leer un Foo de un antiguo cliente, obtendrá un None , que luego manejaría (con un getOrElse ) en su código de consumidor.

O bien, si no te gusta esto, introduce un BackwardsCompatibleFoo :

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

y luego conviértalo en un Foo para trabajar más allá, evitando tener que lidiar con este tipo de gimnasia de datos todo el tiempo en el código.

Puede definir el estado como una opción

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

usa JsPath de la siguiente manera:

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