web-dev-qa-db-ja.com

Play 2 JSON形式で欠落しているプロパティのデフォルト

私はプレイ中に次のモデルと同等のものを持っていますscala:

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

次のFooインスタンスの場合

Foo(1, "foo")

次のJSONドキュメントを取得します。

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

このJSONは永続化され、データストアから読み取られます。これで要件が変更され、Fooにプロパティを追加する必要があります。プロパティにはデフォルト値があります:

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

JSONへの書き込みは問題ではありません。

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

ただし、そこから読み取ると、「/ status」パスがないためにJsErrorが発生します。

ノイズをできるだけ少なくしたデフォルトをどのように提供できますか?

(ps:私は以下に投稿する回答がありますが、私はそれには本当に満足しておらず、賛成投票してより良いオプションを受け入れます)

35
Jean

2.6をプレイ

@CanardMoussantの回答によると、Play 2.6以降では、play-jsonマクロが改善され、デシリアライズ時にプレースホルダーとしてデフォルト値を使用するなど、複数の新機能が提案されています。

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

2.6未満でプレイする場合は、以下のオプションのいずれかを使用して、最良のオプションをそのまま使用します。

play-json-extra

私はplay-jsonで私が抱えていたほとんどの欠点のはるかに優れた解決策を質問の問題も含めて見つけました:

play-json-extra [play-json-extensions]を内部で使用して、この質問の特定の問題を解決します。

これには、欠落しているデフォルトをシリアライザ/デシリアライザに自動的に含めるマクロが含まれているため、リファクタリングでエラーが発生しにくくなります。

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

あなたがチェックしたいかもしれないライブラリにもっとあります: play-json-extra

Jsonトランスフォーマー

私の現在の解決策は、JSONトランスフォーマーを作成し、それをマクロによって生成された読み取りと結合することです。トランスフォーマーは次の方法で生成されます。

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

次に、フォーマット定義は次のようになります。

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

そして

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

実際、デフォルト値が適用されたFooのインスタンスが生成されます。

これには私の意見に2つの大きな欠陥があります。

  • デフォルトのキー名は文字列であり、リファクタリングによって取得されません
  • デフォルトの値は重複しており、ある場所で変更された場合、他の場所で手動で変更する必要があります
39
Jean

私が見つけた最もクリーンなアプローチは、「または純粋」を使用することです。たとえば、

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

デフォルトが定数の場合、これは通常の暗黙的な方法で使用できます。動的な場合は、Readsを作成するメソッドを記述し、それをスコープ内に導入する必要があります。

implicit val packageReader = makeJsonReads(jobId, url)
21
Ed Staub

別の解決策は、formatNullable[T]inmapInvariantFunctorを組み合わせたもの。

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))
16
MikeMcKibben

正式な答えは、Play Json 2.6に付属するWithDefaultValuesを使用することになるはずです。

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

編集:

動作はplay-json-extraライブラリーとは異なることに注意することが重要です。たとえば、デフォルト値がDateTime.NowであるDateTimeパラメータがある場合は、プロセスの起動時間を取得します-おそらく望んだものではありません-play-json-extraを使用すると、作成時間を取得できますJSONから。

6
CanardMoussant

all JSONフィールドをオプション(つまり、ユーザー側ではオプション)にしたい場合に直面しましたが、内部的には、ユーザーの場合に備えて正確に定義されたデフォルト値ですべてのフィールドを非オプションにしたい特定のフィールドを指定していません。これは、ユースケースに似ているはずです。

私は現在、完全にオプションの引数でFooの構築を単純にラップするアプローチを検討しています:

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

残念ながら、明示的なReads[Foo]およびWrites[Foo]、これはおそらくあなたが避けたかったことですか?もう1つの欠点は、デフォルト値は、キーがないか、値がnullの場合にのみ使用されることです。ただし、キーに間違ったタイプの値が含まれている場合、検証全体でValidationErrorが返されます。

このようなオプションのJSON構造のネストは問題ではありません。たとえば、次のようになります。

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

これはおそらく「最小限のノイズ」の要件を満たさないでしょうが、なぜ新しいパラメータをOption[String]として導入しないのですか?

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

古いクライアントからFooを読み取ると、Noneを取得します。これは、コンシューマーコードで(getOrElseを使用して)処理します。

または、これが気に入らない場合は、BackwardsCompatibleFooを導入してください:

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

そして、それをFooに変換してさらに作業を進めると、コード全体でこの種のデータ体操に対処する必要がなくなります。

1

ステータスをオプションとして定義できます

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

次のようにJsPathを使用します。

(JsPath \ "gender").readNullable[String]
1
Rubber Duck