[英]Defaults for missing properties in play 2 JSON formats
我在 play 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 的游戏,最佳选择仍然是使用以下选项之一:
播放-json-额外
我发现了一个更好的解决方案来解决我在 play-json 中的大多数缺点,包括问题中的缺点:
play-json-extra 在内部使用 [play-json-extensions] 来解决此问题中的特定问题。
它包含一个宏,它将自动包含序列化器/反序列化器中缺少的默认值,从而使重构更不容易出错!
import play.json.extra.Jsonx
implicit def jsonFormat = Jsonx.formatCaseClass[Foo]
您可能需要查看更多库: play-json-extra
Json 转换器
我目前的解决方案是创建一个 JSON Transformer 并将其与宏生成的 Reads 结合起来。 变压器由以下方法生成:
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 实例。
在我看来,这有两个主要缺陷:
我发现的最干净的方法是使用“或纯”,例如,
...
((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]
与来自InvariantFunctor
inmap
结合使用。
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 参数,它的默认值是 DateTime.Now,那么你现在将获得进程的启动时间 - 可能不是你想要的 - 而使用 play-json-extra 你有创建的时间来自 JSON。
我刚刚遇到的情况是,我希望所有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]
,这可能是您想要避免的? 另一个缺点是,只有在缺少键或值为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]
声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.