反序列化 JSON 分解缺失值和空值



我需要使用 play-json 解析 JSON 对象并区分缺失值、字符串值和空值。

因此,例如,我可能希望反序列化为以下 case 类:

case class MyCaseClass(
a: Option[Option[String]]
)

其中"a"的值表示:

  • 无 - 缺少"a" - 正常播放 json 行为
  • Some(
  • Some(String)) - "a" 有一个字符串值
  • 一些(无) - "a"具有空值

因此,预期行为的示例是:

{}
should deserialize to myCaseClass(None)
{
"a": null
} 
should deserialize as myCaseClass(Some(None))
{
"a": "a"
}
should deserialize as myCaseClass(Some(Some("a"))

我尝试编写自定义格式化程序,但是 formatNullable 和 formatNullableWithDefault 方法不区分缺失值和空值,因此我在下面编写的代码无法生成 Some(None) 结果

object myCaseClass {
implicit val aFormat: Format[Option[String]] = new Format[Option[String]] {
override def reads(json: JsValue): JsResult[Option[String]] = {
json match {
case JsNull => JsSuccess(None) // this is never reached
case JsString(value) => JsSuccess(Some(value))
case _ => throw new RuntimeException("unexpected type")
}
}
override def writes(codename: Option[String]): JsValue = {
codename match {
case None => JsNull
case Some(value) =>  JsString(value)
}
}
}
implicit val format = (
(__  "a").formatNullableWithDefault[Option[String]](None)
)(MyCaseClass.apply, unlift(MyCaseClass.unapply))
}

我在这里错过了一个技巧吗?我应该怎么做? 我非常愿意以 Option[Option[Sting]] 以外的其他方式对最终值进行编码,例如某种封装此值的案例类:

case class MyContainer(newValue: Option[String], wasProvided: Boolean)

我最近找到了一种方法来做到这一点。我正在使用 Play 2.6.11,但我猜该方法将转移到其他最新版本。

以下代码片段将三种扩展方法添加到JsPath中,以读取/写入/格式化Option[Option[A]]类型的字段。在每种情况下,缺少的字段映射到Nonenull映射到Some(None),非空值映射到原始海报请求的Some(Some(a))

import play.api.libs.json._
object tristate {
implicit class TriStateNullableJsPathOps(path: JsPath) {
def readTriStateNullable[A: Reads]: Reads[Option[Option[A]]] =
Reads[Option[Option[A]]] { value =>
value.validate[JsObject].flatMap { obj =>
path.asSingleJsResult(obj) match {
case JsError(_)           => JsSuccess(Option.empty[Option[A]])
case JsSuccess(JsNull, _) => JsSuccess(Option(Option.empty[A]))
case JsSuccess(json, _)   => json.validate[A]
.repath(path)
.map(a => Option(Option(a)))
}
}
}
def writeTriStateNullable[A: Writes]: OWrites[Option[Option[A]]] =
path.writeNullable(Writes.optionWithNull[A])
def formatTriStateNullable[A: Format]: OFormat[Option[Option[A]]] =
OFormat(readTriStateNullable[A], writeTriStateNullable[A])
}
}

与此线程中的先前建议一样,此方法要求您使用应用性 DSL 完整地写出 JSON 格式。不幸的是,它与Json.format宏不兼容,但它可以让您接近您想要的。下面是一个用例:

import play.api.libs.json._
import play.api.libs.functional.syntax._
import tristate._
case class Coord(col: Option[Option[String]], row: Option[Option[Int]])
implicit val format: OFormat[Coord] = (
(__  "col").formatTriStateNullable[String] ~
(__  "row").formatTriStateNullable[Int]
)(Coord.apply, unlift(Coord.unapply))

一些写作的例子:

format.writes(Coord(None, None))
// => {}
format.writes(Coord(Some(None), Some(None)))
// => { "col": null, "row": null }
format.writes(Coord(Some(Some("A")), Some(Some(1))))
// => { "col": "A", "row": 1 }

还有一些阅读的例子:

Json.obj().as[Coord]
// => Coord(None, None)
Json.obj(
"col" -> JsNull, 
"row" -> JsNull
).as[Coord]
// => Coord(Some(None), Some(None))
Json.obj(
"col" -> "A", 
"row" -> 1
).as[Coord]
// => Coord(Some(Some("A")), Some(Some(1)))

作为读者的额外练习,您可以将其与一点无形相结合,以自动派生编解码器并将Json.format宏替换为不同的单行(尽管编译时间更长)。

按照@kflorence关于OptionHandler的建议,我能够获得所需的行为。

implicit def optionFormat[T](implicit tf: Format[T]): Format[Option[T]] = Format(
tf.reads(_).map(r => Some(r)),
Writes(v => v.map(tf.writes).getOrElse(JsNull))
)
object InvertedDefaultHandler extends OptionHandlers {
def readHandler[T](jsPath: JsPath)(implicit r: Reads[T]): Reads[Option[T]] = jsPath.readNullable
override def readHandlerWithDefault[T](jsPath: JsPath, defaultValue: => Option[T])(implicit r: Reads[T]): Reads[Option[T]] = Reads[Option[T]] { json =>
jsPath.asSingleJson(json) match {
case JsDefined(JsNull) => JsSuccess(defaultValue)
case JsDefined(value)  => r.reads(value).repath(jsPath).map(Some(_))
case JsUndefined()     => JsSuccess(None)
}
}
def writeHandler[T](jsPath: JsPath)(implicit writes: Writes[T]): OWrites[Option[T]] = jsPath.writeNullable
}
val configuration = JsonConfiguration[Json.WithDefaultValues](optionHandlers = InvertedDefaultHandler)
case class RequestObject(payload: Option[Option[String]] = Some(None))
implicit val requestObjectFormat: OFormat[RequestObject] = Json.configured(configuration).format[RequestObject]
Json.parse(""" {} """).as[RequestObject] // RequestObject(None)
Json.parse(""" {"payload": null } """).as[RequestObject] // RequestObject(Some(None))
Json.parse(""" {"payload": "hello" } """).as[RequestObject] // RequestObject(Some(Some(hello)))

所以重要的部分是:

  • readHandlerWithDefault基本上翻转如何 与OptionHandlers.Default中的原始实现相比,JsDefined(JsNull)JsUndefined正在处理不存在和显式的空值
  • Json配置兼具Json.WithDefaultValuesoptionHandlers
  • 默认值的设置方式。请注意 RequestObject.payload 的默认值

不幸的是,我不知道如何自动实现您想要的。目前在我看来,您无法使用标准宏做到这一点。然而,令人惊讶的是,如果您可以交换null和"缺席"的情况,您可能会获得类似的结果(我同意这有点令人困惑)。

假设类Xxx定义为(默认值很重要 - 这将是null情况的结果)

case class Xxx(a: Option[Option[String]] = Some(None))

并提供以下隐式Reads

implicit val optionStringReads:Reads[Option[String]] = new Reads[Option[String]] {
override def reads(json: JsValue) = json match {
case JsNull => JsSuccess(None) // this is never reached
case JsString(value) => JsSuccess(Some(value))
case _ => throw new RuntimeException("unexpected type")
}
}
implicit val xxxReads = Json.using[Json.WithDefaultValues].reads[Xxx]

然后对于测试数据:

val jsonNone = "{}"
val jsonNull = """{"a":null}"""
val jsonVal = """{"a":"abc"}"""
val jsonValues = List(jsonNone, jsonNull, jsonVal)
jsonValues.foreach(jsonString => {
val jsonAst = Json.parse(jsonString)
val obj = Json.fromJson[Xxx](jsonAst)
println(s"'$jsonString' => $obj")
})

输出为

'{}' => JsSuccess(xxx(Some(None)),)
'{"a":null}' => JsSuccess(Xxx(None),)
'{"a":"abc"}' => JsSuccess(Xxx(Some(Some(abc))),)

所以

  • 不存在属性映射到Some(None)
  • null映射到None
  • 值映射到Some(Some(value))

对于开发人员来说,这很笨拙且有点出乎意料,但至少这区分了所有 3 个选择。交换null和"不存在"选择的原因是,我发现区分这些情况的唯一方法是将目标类中的值声明为Option并同时具有默认值,在这种情况下,默认值是"不存在"情况映射到的内容;不幸的是,您无法控制null映射到的值 - 它始终None.

最新更新