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)

0
Todavía estoy usando los transformadores por ahora. Hasta que tenga suficiente tiempo para ver cómo escribir mis propias macros, creo que eso es todo lo que podré usar :(
agregado el autor Jean, fuente
Hola Jean He estado investigando este tema y he encontrado this . No parece haber ninguna actividad excepto hablar de ello. ¿Has encontrado una solución más elegante que un transformador?
agregado el autor Yar, fuente

7 Respuestas

Me acabo de enfrentar con el caso en el que quería todos campos JSON para ser opcionales (es decir, opcional en el lado del usuario) pero internamente quiero que todos los campos sean no opcionales con valores predeterminados definidos con precisión en caso de que el usuario no especifica un cierto 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, se requiere escribir de forma explícita [Foo] y Escribe [Foo] , ¿cuál es probablemente lo que quería evitar? Otro inconveniente es que el valor predeterminado solo se usará 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 un 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))
}
0
agregado

Reproducir 2.6

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

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 which uses [play-json-extensions] internally to solve the particular issue in this question.

¡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]

there is more to the library you may want to check: 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:

  • The defaulter key name is in a string y won't get picked up by a refactoring
  • The value of the default is duplicated y if changed at one place will need to be changed manually at the other
0
agregado
He aceptado mi propia respuesta por el momento y para preguntas con soluciones que intentan responder al requisito inicial, he tratado de explicar por qué no encajaba. Si se propone una mejor respuesta, moveré la respuesta aceptada en consecuencia.
agregado el autor Jean, fuente
De hecho, he actualizado los enlaces a la raíz de la carpeta doc en github por ahora github.com/aparo/play-json-extra/tree/master/doc
agregado el autor Jean, fuente
En mi caso, el campo siempre fue el mismo ( createdAt ). En ese caso, la primera falla se alivia levemente: gist.github.com/felipehummel/5569dd69038142ce3bb5
agregado el autor Felipe Hummel, fuente
El enlace a play-json-extra está roto ahora
agregado el autor Thilo, fuente

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 dinámico, necesita escribir un método para crear las Lecturas, y luego introducirlo en el alcance, a la

implicit val packageReader = makeJsonReads(jobId, url)
0
agregado
El ejemplo de código implica que usted escribe su mapeo a mano, prefiero una forma de mejorar el mapeo basado en macro con los valores predeterminados adecuados, especialmente para clases de casos "más grandes". Aparte de esto, mi propia solución también usa Reads.pure en el transformador json para establecer el valor predeterminado.
agregado el autor Jean, fuente
este caso de uso está bastante lejos de lo que describí en la pregunta inicial :)
agregado el autor Jean, fuente
@Jean - Sí, estoy trabajando con json "sucio" donde el macro Reads es bastante inútil - malos nombres de campo, procesamiento adicional requerido, etc. Puedo ver dónde, si tienes el control del formato JSON y la calidad, usarlo tendría mucho más sentido.
agregado el autor Ed Staub, fuente
Esto me ayudó mucho.
agregado el autor grubjesic, fuente

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]
0
agregado
Excepto que no quiero tener una opción, quiero tener un valor que proporcione un valor predeterminado si no se proporciona. La semántica de Option [String] y String definitivamente no es la misma.
agregado el autor Jean, fuente

Creo que la respuesta oficial ahora debería ser utilizar 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.

0
agregado
También confirmo que esto funciona en Play 2.6, no es necesario agregar dependencias adicionales. Traté de agregar play-json-extra pero tuve algunos problemas relacionados con la incompatibilidad en dependencias en sbt.
agregado el autor Robert Gabriel, fuente

Probablemente esto no satisfaga el requisito de "mínimo ruido posible", pero ¿por qué no introducir el nuevo parámetro como una opción [String] ?

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

Al leer un Foo de un antiguo cliente, obtendrás un None , que luego manejaría (con un getOrElse ) en tu 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 adelante, evitando tener que lidiar con este tipo de gimnasia de datos todo el tiempo en el código.

0
agregado
El problema con la opción es que luego debo mapear u obtener, y generalmente uso operaciones monádicas para acceder a un valor que es no opcional, solo de manera predeterminada. El uso de una opción introduciría una señal falsa en lo que respecta a la mecanografía, solo para que coincida con la implementación actual de la biblioteca.
agregado el autor Jean, fuente
Exactamente: estoy tratando de tener un mecanismo para gestionar con elegancia la compatibilidad con los datos de la tienda (o con clientes que no se han actualizado) al proporcionar un valor predeterminado aceptable. De esta forma, cuando leo, escribo mis datos nuevamente en la tienda, los actualizo automáticamente y mi sistema no explota para los clientes más antiguos. Obviamente, uso Option cuando no hay valores predeterminados razonables, pero en muchos casos puedo proporcionar el valor predeterminado (como lo hago en la clase de caso)
agregado el autor Jean, fuente
Es cierto, pero menciona que sus requisitos han cambiado, por lo que desde la perspectiva del "cliente" (en este caso, el almacén de datos), ese valor es opcional (es una nueva versión del esquema, por así decirlo). En este caso, recomendaría migrar los datos en la tienda (estableciendo el valor predeterminado donde sea que falte), o tener un mecanismo que trate con la elegante compatibilidad con versiones anteriores de los datos en el almacén de datos.
agregado el autor Manuel Bernhardt, fuente

Una solución alternativa es usar formatNullable [T] combinado con inmap desde 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))
0
agregado
El enfoque inmap es interesante, tendré que ver si hay una forma práctica de usarlo que no implique escribir manualmente toda la asignación. En el caso de Foo en esta pregunta, es fácil, pero imaginemos que foo tiene 15 atributos ...
agregado el autor Jean, fuente
He tomado este enfoque; funciona y también es más limpio que la respuesta actualmente aceptada, pero desafortunadamente comparte uno de los inconvenientes en que los tipos tienen que especificarse en dos lugares (la clase de caso y la definición de lecturas).
agregado el autor Steven Bakhtiari, fuente
Estoy de acuerdo con @StevenBakhtiari: he estado utilizando este enfoque y es muy útil, y no solo para los valores predeterminados, sino también para otras operaciones sobre el valor devuelto por JsPath.
agregado el autor DemetriKots, fuente
¡Aunque funciona para mí!
agregado el autor Guillaume, fuente
JavaScript - Comunidad española
JavaScript - Comunidad española
4 de los participantes

En este grupo hablamos de JavaScript. Partner: es.switch-case.com