Vorgaben für fehlende Eigenschaften in Play 2 JSON-Formate

Ich habe ein Äquivalent des folgenden Modells in der Spielskala:

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

Für die folgende Foo-Instanz

Foo(1, "foo")

Ich würde das folgende JSON-Dokument erhalten:

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

Dieser JSON wird beibehalten und aus einem Datenspeicher gelesen. Jetzt haben sich meine Anforderungen geändert und ich muss Foo eine Eigenschaft hinzufügen. Die Eigenschaft hat einen Standardwert:

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

Das Schreiben in JSON ist kein Problem:

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

Das Lesen von ihm ergibt jedoch einen JsError, um den Pfad "/ status" zu verpassen.

Wie kann ich einen Standard mit möglichst geringem Rauschen bereitstellen?

(ps: Ich habe eine Antwort, die ich im Folgenden veröffentlichen werde, aber ich bin nicht wirklich zufrieden damit und würde eine bessere Option annehmen und akzeptieren)

0
Ich benutze immer noch die Transformatoren für jetzt. Bis ich genug Zeit habe, um zu schauen, wie ich meine eigenen Makros schreiben kann, denke ich, dass das alles ist, was ich benutzen kann :(
hinzugefügt der Autor Jean, Quelle
Hallo Jean. Ich habe mich mit diesem Thema beschäftigt und dieses gefunden. Es scheint keine Aktivität zu geben, außer darüber zu reden. Haben Sie schon eine elegantere Lösung als einen Transformator gefunden?
hinzugefügt der Autor Yar, Quelle

7 Antworten

Ich war gerade mit dem Fall konfrontiert, in dem ich alle JSON-Felder als optional (dh optional auf der Benutzerseite) haben wollte, aber intern möchte ich, dass alle Felder nicht-optional sind mit genau definierten Standardwerten für den Benutzer spezifiziert kein bestimmtes Feld. Dies sollte Ihrem Anwendungsfall ähnlich sein.

Ich überlege gerade einen Ansatz, der einfach die Konstruktion von Foo mit vollständig optionalen Argumenten umschließt:

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

Leider erfordert es das Schreiben expliziter Reads [Foo] und Writes [Foo] , was Sie wahrscheinlich vermeiden wollten? Ein weiterer Nachteil ist, dass der Standardwert nur verwendet wird, wenn der Schlüssel fehlt oder der Wert null ist. Wenn der Schlüssel jedoch einen Wert des falschen Typs enthält, gibt die gesamte Validierung erneut einen ValidationError zurück.

Das Verschachteln solcher optionalen JSON-Strukturen stellt kein Problem dar, zum Beispiel:

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
hinzugefügt

Spielen 2.6

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

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

Für Spiele unter 2.6 bleibt die beste Option, wenn Sie eine der folgenden Optionen verwenden:

play-json-extra

Ich fund eine viel bessere Lösung für die meisten Mängel heraus, die ich mit play-json hatte, einschließlich der in der Frage:

play-json-extra which uses [play-json-extensions] internally to solve the particular issue in this question.

Es enthält ein Makro, das die fehlenden Stundardwerte automatisch in den Serializer/Deserializer einfügt, wodurch Refactors weniger fehleranfällig werden!

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-Übertrager

Meine derzeitige Lösung besteht darin, einen JSON-Transformer zu erstellen und ihn mit den vom Makro generierten Lesevorgängen zu kombinieren. Der Transformator wird mit der folgenden Methode generiert:

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

Die Formatdefinition wird dann:

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

und

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

erzeugt in der Tat eine Instanz von Foo mit dem angewendeten Standardwert.

Dies hat meiner Meinung nach zwei große Mängel:

  • The defaulter key name is in a string und won't get picked up by a refactoring
  • The value of the default is duplicated und if changed at one place will need to be changed manually at the other
0
hinzugefügt
Ich habe meine eigene Antwort für jetzt angenommen und für Fragen mit Lösungen, die versuchen, die anfängliche Anforderung zu beantworten, habe ich versucht zu erklären, warum es nicht passte. Wenn eine bessere Antwort vorgeschlagen wird, werde ich die angenommene Antwort entsprechend verschieben.
hinzugefügt der Autor Jean, Quelle
In der Tat, ich habe die Links zum Stamm des Doc-Ordners auf GitHub für jetzt aktualisiert github.com/aparo/play-json-extra/tree/master/doc
hinzugefügt der Autor Jean, Quelle
In meinem Fall war das Feld immer dasselbe ( createdAt ). In diesem Fall wird der erste Fehler leicht gemildert: gist.github.com/felipehummel/5569dd69038142ce3bb5
hinzugefügt der Autor Felipe Hummel, Quelle
Der Link zu play-json-extra ist jetzt kaputt
hinzugefügt der Autor Thilo, Quelle

Der sauberste Ansatz, den ich gefunden habe, ist "oder rein" zu verwenden, z.

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

Dies kann normal normal verwendet werden, wenn der Standardwert eine Konstante ist. Wenn es dynamisch ist, müssen Sie eine Methode schreiben, um das Lesen zu erstellen, und dann in-scope, a la, einführen

implicit val packageReader = makeJsonReads(jobId, url)
0
hinzugefügt
Das Code-Beispiel impliziert, dass Sie Ihr Mapping von Hand schreiben, ich bevorzuge eine Möglichkeit, das Makro-basierte Mapping mit den richtigen Standardwerten zu verbessern, besonders für "größere" Fall-Klassen. Abgesehen davon verwendet meine eigene Lösung auch Reads.pure im JSON-Transformer, um den Standardwert festzulegen.
hinzugefügt der Autor Jean, Quelle
Dieser Anwendungsfall ist ziemlich weit von dem, was ich in der ersten Frage beschrieben habe :)
hinzugefügt der Autor Jean, Quelle
@Jean - Ja, ich arbeite mit "dreckigen" json, wo das Makro liest ist ziemlich nutzlos - schlechte Feldnamen, zusätzliche Verarbeitung erforderlich, etc. Ich kann sehen, wo, wenn Sie die Kontrolle über das JSON-Format und Qualität haben, es zu benutzen würde viel mehr Sinn machen.
hinzugefügt der Autor Ed Staub, Quelle
Das hat mir sehr geholfen.
hinzugefügt der Autor grubjesic, Quelle

Sie können den Status als Option definieren

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

benutze JsPath so:

(JsPath \ "gender").readNullable[String]
0
hinzugefügt
Außer, dass ich keine Option haben möchte, möchte ich einen Wert haben und einen Standardwert angeben, wenn dieser nicht angegeben ist. Die Semantik von Option [String] und String ist definitiv nicht dieselbe.
hinzugefügt der Autor Jean, Quelle

Ich denke, die offizielle Antwort sollte jetzt sein, die WithDefaultValues ​​zu verwenden, die Play Json 2.6 begleiten:

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

Bearbeiten:

Es ist wichtig zu beachten, dass sich das Verhalten von der play-json-extra-Bibliothek unterscheidet. Wenn Sie beispielsweise einen DateTime-Parameter mit einem Standardwert für DateTime.Now haben, erhalten Sie jetzt die Startzeit des Prozesses - wahrscheinlich nicht das, was Sie wollen - während Sie mit play-json-extra die Zeit der Erstellung hatten aus dem JSON.

0
hinzugefügt
Ich bestätige auch, dass dies in Play 2.6 funktioniert und keine zusätzlichen Abhängigkeiten hinzugefügt werden müssen. Ich habe versucht, Play-Json-Extra hinzuzufügen, aber ich hatte einige Probleme im Zusammenhang mit Inkompatibilität in Abhängigkeiten in sbt.
hinzugefügt der Autor Robert Gabriel, Quelle

Dies wird wahrscheinlich nicht die Anforderung "geringstmögliches Rauschen" erfüllen, aber warum sollte der neue Parameter nicht als Option [String] eingeführt werden?

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

Wenn Sie einen Foo von einem alten Client lesen, erhalten Sie einen None , mit dem ich dann umgehen würde (mit einem getOrElse ) Verbrauchercode.

Oder, wenn Ihnen das nicht gefällt, führen Sie einen BackwardsCompatibleFoo ein:

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

und dann wandeln Sie diesen in einen Foo um mit weiter zu arbeiten und vermeiden, mit dieser Art von Datengymnastik die ganze Zeit im Code umgehen zu müssen.

0
hinzugefügt
Das Problem mit der Option ist, dass ich dann monadische Operationen zuordnen oder abrufen muss und im Allgemeinen monadische Operationen verwenden muss, um auf einen Wert zuzugreifen, der nicht optional ist, sondern nur voreingestellt ist. Die Verwendung einer Option würde bei der Eingabe ein falsches Signal auslösen, nur um der aktuellen Implementierung der Bibliothek zu entsprechen.
hinzugefügt der Autor Jean, Quelle
Genau: Ich versuche, einen Mechanismus zu haben, der rückwärtskompatibel mit Daten im Speicher (oder mit Clients, die nicht aktualisiert wurden) kompatibel ist, indem er einen akzeptablen Standardwert liefert. Auf diese Weise, wenn ich lese, schreibe meine Daten zurück in den Laden, es wird automatisch aktualisiert und mein System explodiert nicht für ältere Kunden. Ich benutze natürlich Option, wenn es keine vernünftigen Standardwerte gibt, aber in vielen Fällen kann ich den Standard angeben (wie ich es in der Fallklasse mache)
hinzugefügt der Autor Jean, Quelle
Richtig, aber Sie erwähnen, dass sich Ihre Anforderungen geändert haben. Aus der Sicht des "Clients" (in diesem Fall des Datenspeichers) ist dieser Wert optional (es ist sozusagen eine neue Version des Schemas). In diesem Fall würde ich befürworten, entweder die Daten im Speicher zu migrieren (den Standardwert dort einzustellen, wo sie fehlen) oder einen Mechanismus zu haben, der sich mit der Rückwärtskompatibilität von Daten im Datenspeicher befasst
hinzugefügt der Autor Manuel Bernhardt, Quelle

Eine alternative Lösung ist die Verwendung von formatNullable [T] kombiniert mit inmap aus 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
hinzugefügt
Der Inmap-Ansatz ist interessant, ich werde sehen müssen, ob es einen praktischen Weg gibt, ihn zu verwenden, der nicht das manuelle Schreiben des gesamten Mappings beinhaltet. Im Falle von Foo in dieser Frage ist es einfach, aber stell dir vor, foo hat 15 Attribute ...
hinzugefügt der Autor Jean, Quelle
Ich habe diesen Ansatz gewählt; es funktioniert und es ist auch sauberer als die derzeit akzeptierte Antwort, aber leider teilt es einen der Nachteile darin, dass die Typen an zwei Stellen angegeben werden müssen (die Fallklasse und die Lesedefinition).
hinzugefügt der Autor Steven Bakhtiari, Quelle
Vereinbaren Sie mit @StevenBakhtiari - Ich habe diesen Ansatz verwendet und es ist sehr nützlich, nicht nur für Standardwerte, sondern auch für andere Operationen mit dem von JsPath zurückgegebenen Wert.
hinzugefügt der Autor DemetriKots, Quelle
thx es funktioniert für mich!
hinzugefügt der Autor Guillaume, Quelle
JavaScript - Deutsche Gemeinschaft
JavaScript - Deutsche Gemeinschaft
3 der Teilnehmer

In dieser Gruppe sprechen wir über JavaScript.