繁体   English   中英

play 2 JSON 格式中缺少属性的默认值

[英]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.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM