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

声明:本页面是StackOverFlow热门问题的中英对照翻译,遵循CC BY-SA 4.0协议,如果您需要使用它,必须同样遵循CC BY-SA许可,注明原文地址和作者信息,同时你必须将它归于原作者(不是我):StackOverFlow 原文地址: http://stackoverflow.com/questions/20616677/
Warning: these are provided under cc-by-sa 4.0 license. You are free to use/share it, But you must attribute it to the original authors (not me): StackOverFlow

提示:将鼠标放在中文语句上可以显示对应的英文。显示中英文
时间:2020-10-22 05:56:33  来源:igfitidea点击:

Defaults for missing properties in play 2 JSON formats

jsonscalaplayframework-2.2

提问by Jean

I have an equivalent of the following model in play scala :

我在 play scala 中有一个相当于以下模型的模型:

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

For the following Foo instance

对于以下 Foo 实例

Foo(1, "foo")

I would get the following JSON document:

我会得到以下 JSON 文档:

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

This JSON is persisted and read from a datastore. Now my requirements have changed and I need to add a property to Foo. The property has a default value :

这个 JSON 被持久化并从数据存储中读取。现在我的要求发生了变化,我需要向 Foo 添加一个属性。该属性有一个默认值:

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

Writing to JSON is not a problem :

写入 JSON 不是问题:

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

Reading from it however yields a JsError for missing the "/status" path.

然而,从中读取会因缺少“/status”路径而产生 JsError。

How can I provide a default with the least possible noise ?

我怎样才能提供一个噪音最小的默认值?

(ps: I have an answer which I will post below but I am not really satisfied with it and would upvote and accept any better option)

(ps:我有一个答案,我将在下面发布,但我对此并不满意,并且会赞成并接受任何更好的选择)

回答by Jean

Play 2.6

玩 2.6

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

根据@CanardMoussant 的回答,从 Play 2.6 开始, play-json 宏得到了改进,并提出了多项新功能,包括在反序列化时使用默认值作为占位符:

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

For play below 2.6 the best option remains using one of the options below :

对于低于 2.6 的游戏,最佳选择仍然是使用以下选项之一:

play-json-extra

播放-json-额外

I found out about a much better solution to most of the shortcomings I had with play-json including the one in the question:

我发现了一个更好的解决方案来解决我在 play-json 中的大多数缺点,包括问题中的缺点:

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

play-json-extra 在内部使用 [play-json-extensions] 来解决此问题中的特定问题。

It includes a macro which will automatically include the missing defaults in the serializer/deserializer, making refactors much less error prone !

它包含一个宏,该宏将自动包含序列化器/反序列化器中缺少的默认值,从而使重构更不容易出错!

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

您可能需要查看更多库:play-json-extra

Json transformers

Json 转换器

My current solution is to create a JSON Transformer and combine it with the Reads generated by the macro. The transformer is generated by the following method:

我目前的解决方案是创建一个 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))))
}

The format definition then becomes :

格式定义然后变成:

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

and

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

will indeed generate an instance of Foo with the default value applied.

确实会生成一个应用了默认值的 Foo 实例。

This has 2 major flaws in my opinion:

在我看来,这有两个主要缺陷:

  • The defaulter key name is in a string and won't get picked up by a refactoring
  • The value of the default is duplicated and if changed at one place will need to be changed manually at the other
  • 默认键名称在一个字符串中,不会被重构获取
  • 默认值是重复的,如果在一处更改,则需要在另一处手动更改

回答by Ed Staub

The cleanest approach that I've found is to use "or pure", e.g.,

我发现的最干净的方法是使用“或纯”,例如,

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

This can be used in the normal implicit way when the default is a constant. When it's dynamic, then you need to write a method to create the Reads, and then introduce it in-scope, a la

当默认值为常量时,这可以以正常的隐式方式使用。当它是动态的,那么你需要写一个方法来创建Reads,然后在范围内引入它,就这样

implicit val packageReader = makeJsonReads(jobId, url)

回答by MikeMcKibben

An alternative solution is to use formatNullable[T]combined with inmapfrom InvariantFunctor.

另一种解决方案是formatNullable[T]inmapfrom结合使用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))

回答by CanardMoussant

I think the official answer should now be to use the WithDefaultValues coming along Play Json 2.6:

我认为现在官方的答案应该是使用 Play Json 2.6 附带的 WithDefaultValues:

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

Edit:

编辑:

It is important to note that the behavior differs from the play-json-extra library. For instance if you have a DateTime parameter that has a default value to DateTime.Now, then you will now get the startup time of the process - probably not what you want - whereas with play-json-extra you had the time of the creation from the JSON.

需要注意的是,该行为与 play-json-extra 库不同。例如,如果您有一个 DateTime 参数,它的默认值是 DateTime.Now,那么您现在将获得进程的启动时间 - 可能不是您想要的 - 而使用 play-json-extra 您有创建时间来自 JSON。

回答by bluenote10

I was just faced with the case where I wanted allJSON fields to be optional (i.e. optional on user side) but internally I want all fields to be non-optional with precisely defined default values in case the user does not specify a certain field. This should be similar to your use case.

我刚刚遇到的情况是,我希望所有JSON 字段都是可选的(即在用户端可选),但在内部,我希望所有字段都是非可选的,并具有精确定义的默认值,以防用户未指定某个字段。这应该类似于您的用例。

I'm currently considering an approach which simply wraps the construction of Foowith fully optional arguments:

我目前正在考虑一种方法,它简单地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")

Unfortunately, it requires writing explicit Reads[Foo]and Writes[Foo], which is probably what you wanted to avoid? One further drawback is that the default value will only be used if the key is missing or the value is null. However if the key contains a value of the wrong type, then again the whole validation returns a ValidationError.

不幸的是,它需要编写明确的Reads[Foo]and Writes[Foo],这可能是您想要避免的?另一个缺点是,只有在缺少键或值为 时才会使用默认值null。但是,如果键包含错误类型的值,则整个验证再次返回一个ValidationError.

Nesting such optional JSON structures is not a problem, for instance:

嵌套此类可选的 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))
}

回答by Manuel Bernhardt

This probably won't satisfy the "least possible noise" requirement, but why not introduce the new parameter as an Option[String]?

这可能无法满足“最小可能的噪音”要求,但为什么不将新参数引入为Option[String]

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

When reading a Foofrom an old client, you'll get a None, which I'd then handle (with a getOrElse) in your consumer code.

Foo从旧客户端读取 a 时,您会得到 a None,然后我会getOrElse在您的消费者代码中处理(使用 a )。

Or, if you don't like this, introduce an BackwardsCompatibleFoo:

或者,如果您不喜欢这样,请引入BackwardsCompatibleFoo

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

and then turn that one into a Footo work with further on, avoiding to have to deal with this kind of data gymnastics all along in the code.

然后把它变成一个Foo进一步工作,避免在代码中一直处理这种数据体操。

回答by Rubber Duck

You may define status as an Option

您可以将状态定义为选项

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

use JsPath like so:

像这样使用 JsPath:

(JsPath \ "gender").readNullable[String]