play-json: JsNull 不等于 JsString(null),如何解决这个问题?
play-json: JsNull is not equal to JsString(null), how to get around this issue?
我正在处理一些较大的 JSON 输出。我们有一堆测试来检查输出。我们通过在磁盘上创建 JSON 的副本,在 InputStream
上执行 Json.parse()
并将其与我们在内存中构建的 JsObject 进行比较来创建测试。
在我开始将我们的一些 Writes
转换为使用函数式语法(即,不要覆盖特征中的 writes
方法,而是使用构建器)之前,这一直很有效。
突然,测试开始失败:抱怨 null
字段不相等。
显然,当使用函数语法时,Option[String]
将转换为 JsString(null)
而不是 JsNull
。这在字符串化版本中并不明显。
考虑以下片段,使用
libraryDependencies += "com.typesafe.play" %% "play-json" % "2.7.4"
import play.api.libs.json._
import play.api.libs.functional.syntax._
object FooBar {
case class Foo(option: Option[String])
def main(args: Array[String]): Unit = {
val classic: OWrites[Foo] = new OWrites[Foo] {
override def writes(f: Foo): JsObject = Json.obj("foo" -> f.option)
}
val dsl: OWrites[Foo] = (__ \ "foo").write[String].contramap(foo => foo.option.orNull)
val json_classic = Json.toJsObject(Foo(None))(classic)
val json_dsl = Json.toJsObject(Foo(None))(dsl)
val json_parse = Json.parse("""{"foo":null}""")
val string_classic = Json.prettyPrint(json_classic)
val string_dsl = Json.prettyPrint(json_dsl)
println(
s"""Result is:
|json_dsl == json_classic : ${json_dsl == json_classic} // (expect true)
|json_dsl == json_parse : ${json_dsl == json_parse} // (expect true)
|json_classic == json_parse : ${json_classic == json_parse} // (expect true)
|string_classic == string_dsl : ${string_classic == string_dsl} // (expect true)
|""".stripMargin)
println(s"classic:\n$string_classic")
println(s"dsl:\n$string_dsl")
}
}
实际输出为
Result is:
json_dsl == json_classic : false // (expect true)
json_dsl == json_parse : false // (expect true)
json_classic == json_parse : true // (expect true)
string_classic == string_dsl : true // (expect true)
classic:
{
"foo" : null
}
dsl:
{
"foo" : null
}
在调试时,您会看到 classic 使用 Tuple ("foo", JsNull)
创建包装器对象,而 dsl 使用 Tuple ("foo", JsString(null))
.
创建包装器
似乎 dsl 的预期方式是在这种情况下使用 writeNullable
,但它以这种方式工作感觉很奇怪。
我希望 JsString(null) == JsNull
为真,或者 dsl 会捕获 null
值并阻止创建 JsString
。
我是不是做错了什么?
我只想重写为 .writeNullable[String]
,这将从 JSON 中删除该字段,但我们有一个需要该字段存在的架构:
...
"properties": {
...
"foo": {
"oneOf": [
{"type": "string"},
{"type": "null"}
]
},
...
}
...
"required": [ "foo" ],
这是 API 的一部分,因此更改它需要时间。
澄清一下:字符串表示在所有情况下都是正确的。我只对 JsValue
的内存表示感兴趣,因此我可以在测试期间使用它的相等性。
我无法使用 Play-JSON 2.7.4:
在 REPL 中重现您的测试
import play.api.libs.json._
case class Foo(option: Option[String])
val classic: OWrites[Foo] = new OWrites[Foo] {
override def writes(f: Foo): JsObject = Json.obj("foo" -> f.option)
}
val dsl: OWrites[Foo] = (__ \ "foo").write[String].contramap(foo => foo.option.orNull)
val json_classic = Json.toJsObject(Foo(None))(classic)
val json_dsl = Json.toJsObject(Foo(None))(dsl)
val json_classic = Json.toJsObject(Foo(None))(classic)
// json_classic: play.api.libs.json.JsObject = {"foo":null}
val json_dsl = Json.toJsObject(Foo(None))(dsl)
// json_dsl: play.api.libs.json.JsObject = {"foo":null}
json_classic
和 json_dsl
之间的不等式是另外一回事,但是 JSON 表示在这两种情况下是一致的,即使 foo.option.orNull
是 unsafe/weird 对我来说。
否则,如果您想将 "null"
视为 null
,您可以在需要此特定行为的地方覆盖默认值 Reads[String]
。
scala> val legacyStrReads: Reads[Option[String]] =
| Reads.optionWithNull(Reads.StringReads).map {
| case Some("null") => None
| case other => other
| }
legacyStrReads: play.api.libs.json.Reads[Option[String]] = play.api.libs.json.Reads$$anon@138decb1
scala> Json.toJson("null").validate(legacyStrReads)
res9: play.api.libs.json.JsResult[Option[String]] = JsSuccess(None,)
我想你想要的是这个:
val dsl: OWrites[Foo] = (__ \ "foo").writeOptionWithNull[String]
.contramap(_.option)
或者,如果使用没有 writeOptionWithNull
:
的旧 play-json 版本
val dsl: OWrites[Foo] = (__ \ "foo").write[JsValue]
.contramap(_.option match {
case None => JsNull
case Some(string) => JsString(string)
})
注意,play.api.libs.json.JsNull
和null
是两个完全不同且不相关的概念,切勿混淆或混淆。在 Scala-only APIs 中,整个 Scala 生态系统的约定是永远、永远不要使用 null
,以至于大多数库只是假装它甚至不存在。因此,您不会找到许多执行空检查的 Scala 库,它们只是假设所有内容都是非空的,而当您开始使用空的那一刻,您就处于未经测试和不受支持的边缘情况的狂野西部。您唯一一次使用或处理空值是在使用 Java API 时,因为它们使用空值,并且那里的约定是包装任何可以在 [= 中产生 null
的内容17=] 尽可能早,并尽可能晚地使用 orNull
解包 Option
,这样至少在不直接与 Java 代码接口的 Scala 代码内部,一切使用 Option
,而不是 null
。但是 play-json 专为 Scala 使用而设计,因此与 Scala-only 生态系统的其余部分一样,它只是假设 null
不存在。
当然,在 json 中,null
确实存在,并且可能有正当理由使用它(尤其是在与需要它的其他系统集成时)。所以 play-json 确实对它进行建模,但它以一种非常强类型的方式对其进行建模——没有什么会自动从 Scala 中的 null
变为 JSON 中的 null
,它总是很明确,这符合 Scala 是强类型的。此外,在 JSON 中使用 null
并不常见,因此大多数默认方法(即 writeNullable
方法)将 Option
映射到非-存在值,所以要写JSONnull
的时候要明确一点,如上图
我正在处理一些较大的 JSON 输出。我们有一堆测试来检查输出。我们通过在磁盘上创建 JSON 的副本,在 InputStream
上执行 Json.parse()
并将其与我们在内存中构建的 JsObject 进行比较来创建测试。
在我开始将我们的一些 Writes
转换为使用函数式语法(即,不要覆盖特征中的 writes
方法,而是使用构建器)之前,这一直很有效。
突然,测试开始失败:抱怨 null
字段不相等。
显然,当使用函数语法时,Option[String]
将转换为 JsString(null)
而不是 JsNull
。这在字符串化版本中并不明显。
考虑以下片段,使用
libraryDependencies += "com.typesafe.play" %% "play-json" % "2.7.4"
import play.api.libs.json._
import play.api.libs.functional.syntax._
object FooBar {
case class Foo(option: Option[String])
def main(args: Array[String]): Unit = {
val classic: OWrites[Foo] = new OWrites[Foo] {
override def writes(f: Foo): JsObject = Json.obj("foo" -> f.option)
}
val dsl: OWrites[Foo] = (__ \ "foo").write[String].contramap(foo => foo.option.orNull)
val json_classic = Json.toJsObject(Foo(None))(classic)
val json_dsl = Json.toJsObject(Foo(None))(dsl)
val json_parse = Json.parse("""{"foo":null}""")
val string_classic = Json.prettyPrint(json_classic)
val string_dsl = Json.prettyPrint(json_dsl)
println(
s"""Result is:
|json_dsl == json_classic : ${json_dsl == json_classic} // (expect true)
|json_dsl == json_parse : ${json_dsl == json_parse} // (expect true)
|json_classic == json_parse : ${json_classic == json_parse} // (expect true)
|string_classic == string_dsl : ${string_classic == string_dsl} // (expect true)
|""".stripMargin)
println(s"classic:\n$string_classic")
println(s"dsl:\n$string_dsl")
}
}
实际输出为
Result is:
json_dsl == json_classic : false // (expect true)
json_dsl == json_parse : false // (expect true)
json_classic == json_parse : true // (expect true)
string_classic == string_dsl : true // (expect true)
classic:
{
"foo" : null
}
dsl:
{
"foo" : null
}
在调试时,您会看到 classic 使用 Tuple ("foo", JsNull)
创建包装器对象,而 dsl 使用 Tuple ("foo", JsString(null))
.
似乎 dsl 的预期方式是在这种情况下使用 writeNullable
,但它以这种方式工作感觉很奇怪。
我希望 JsString(null) == JsNull
为真,或者 dsl 会捕获 null
值并阻止创建 JsString
。
我是不是做错了什么?
我只想重写为 .writeNullable[String]
,这将从 JSON 中删除该字段,但我们有一个需要该字段存在的架构:
...
"properties": {
...
"foo": {
"oneOf": [
{"type": "string"},
{"type": "null"}
]
},
...
}
...
"required": [ "foo" ],
这是 API 的一部分,因此更改它需要时间。
澄清一下:字符串表示在所有情况下都是正确的。我只对 JsValue
的内存表示感兴趣,因此我可以在测试期间使用它的相等性。
我无法使用 Play-JSON 2.7.4:
在 REPL 中重现您的测试import play.api.libs.json._
case class Foo(option: Option[String])
val classic: OWrites[Foo] = new OWrites[Foo] {
override def writes(f: Foo): JsObject = Json.obj("foo" -> f.option)
}
val dsl: OWrites[Foo] = (__ \ "foo").write[String].contramap(foo => foo.option.orNull)
val json_classic = Json.toJsObject(Foo(None))(classic)
val json_dsl = Json.toJsObject(Foo(None))(dsl)
val json_classic = Json.toJsObject(Foo(None))(classic)
// json_classic: play.api.libs.json.JsObject = {"foo":null}
val json_dsl = Json.toJsObject(Foo(None))(dsl)
// json_dsl: play.api.libs.json.JsObject = {"foo":null}
json_classic
和 json_dsl
之间的不等式是另外一回事,但是 JSON 表示在这两种情况下是一致的,即使 foo.option.orNull
是 unsafe/weird 对我来说。
否则,如果您想将 "null"
视为 null
,您可以在需要此特定行为的地方覆盖默认值 Reads[String]
。
scala> val legacyStrReads: Reads[Option[String]] =
| Reads.optionWithNull(Reads.StringReads).map {
| case Some("null") => None
| case other => other
| }
legacyStrReads: play.api.libs.json.Reads[Option[String]] = play.api.libs.json.Reads$$anon@138decb1
scala> Json.toJson("null").validate(legacyStrReads)
res9: play.api.libs.json.JsResult[Option[String]] = JsSuccess(None,)
我想你想要的是这个:
val dsl: OWrites[Foo] = (__ \ "foo").writeOptionWithNull[String]
.contramap(_.option)
或者,如果使用没有 writeOptionWithNull
:
val dsl: OWrites[Foo] = (__ \ "foo").write[JsValue]
.contramap(_.option match {
case None => JsNull
case Some(string) => JsString(string)
})
注意,play.api.libs.json.JsNull
和null
是两个完全不同且不相关的概念,切勿混淆或混淆。在 Scala-only APIs 中,整个 Scala 生态系统的约定是永远、永远不要使用 null
,以至于大多数库只是假装它甚至不存在。因此,您不会找到许多执行空检查的 Scala 库,它们只是假设所有内容都是非空的,而当您开始使用空的那一刻,您就处于未经测试和不受支持的边缘情况的狂野西部。您唯一一次使用或处理空值是在使用 Java API 时,因为它们使用空值,并且那里的约定是包装任何可以在 [= 中产生 null
的内容17=] 尽可能早,并尽可能晚地使用 orNull
解包 Option
,这样至少在不直接与 Java 代码接口的 Scala 代码内部,一切使用 Option
,而不是 null
。但是 play-json 专为 Scala 使用而设计,因此与 Scala-only 生态系统的其余部分一样,它只是假设 null
不存在。
当然,在 json 中,null
确实存在,并且可能有正当理由使用它(尤其是在与需要它的其他系统集成时)。所以 play-json 确实对它进行建模,但它以一种非常强类型的方式对其进行建模——没有什么会自动从 Scala 中的 null
变为 JSON 中的 null
,它总是很明确,这符合 Scala 是强类型的。此外,在 JSON 中使用 null
并不常见,因此大多数默认方法(即 writeNullable
方法)将 Option
映射到非-存在值,所以要写JSONnull
的时候要明确一点,如上图