私はプレイ中に次のモデルと同等のものを持っています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:私は以下に投稿する回答がありますが、私はそれには本当に満足しておらず、賛成投票してより良いオプションを受け入れます)
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つの大きな欠陥があります。
私が見つけた最もクリーンなアプローチは、「または純粋」を使用することです。たとえば、
...
((JsPath \ "notes").read[String] or Reads.pure("")) and
((JsPath \ "title").read[String] or Reads.pure("")) and
...
デフォルトが定数の場合、これは通常の暗黙的な方法で使用できます。動的な場合は、Readsを作成するメソッドを記述し、それをスコープ内に導入する必要があります。
implicit val packageReader = makeJsonReads(jobId, url)
別の解決策は、formatNullable[T]
とinmap
の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))
正式な答えは、Play Json 2.6に付属するWithDefaultValuesを使用することになるはずです。
implicit def jsonFormat = Json.using[Json.WithDefaultValues].format[Foo]
編集:
動作はplay-json-extraライブラリーとは異なることに注意することが重要です。たとえば、デフォルト値がDateTime.NowであるDateTimeパラメータがある場合は、プロセスの起動時間を取得します-おそらく望んだものではありません-play-json-extraを使用すると、作成時間を取得できますJSONから。
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))
}
これはおそらく「最小限のノイズ」の要件を満たさないでしょうが、なぜ新しいパラメータを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
に変換してさらに作業を進めると、コード全体でこの種のデータ体操に対処する必要がなくなります。
ステータスをオプションとして定義できます
case class Foo(id:String, value:String, status: Option[String])
次のようにJsPathを使用します。
(JsPath \ "gender").readNullable[String]