【问题标题】:Deserialize JSON distinguising missing and null values反序列化 JSON 以区分缺失值和空值
【发布时间】:2018-01-29 22:09:40
【问题描述】:

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

例如,我可能想反序列化为以下案例类:

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

'a' 的值是什么意思:

  • 无 - 缺少“a” - 正常的 play-json 行为
  • Some(Some(String)) - "a" 有一个字符串值
  • Some(None) - “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)

【问题讨论】:

  • 而且,是的 - 我知道我可以为整个对象编写一个完全自定义的读取方法。我试图避免这种情况。
  • 类型Option[Option[_]] 无论如何都很难理解。顺便说一句,我看不到这种 null/missing 区别的好处。
  • @cchantep .. 肯定有一点代码味道 - 但这些是我的要求。有一个看似合理的用例。无论如何,它的 json 解析是我正在努力解决的问题。出于这个问题的目的,如果我们认为 undefined 和 null 是 json 中的单独值......我如何在不自己遍历 json 的情况下解析它。
  • 您假设 undefined 或 null 是不同的,但它们都使用不同的格式:表示指定字段没有值的事实
  • 我绝对认为null vs 省略是两个完全不同的意图。不幸的是,在 JSON 规范中对此的歧义并没有减少,但直觉上,我认为执行@iandotkelly 在这里提出的建议是有意义的(尤其是在考虑 PATCH 时)。我在这里与核心 play-json 团队讨论了这个问题:discuss.lightbend.com/t/…

标签: json scala play-json


【解决方案1】:

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

下面的 sn-p 为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 宏(尽管编译时间较长)。

【讨论】:

【解决方案2】:

不幸的是,我不知道如何自动实现您想要的。现在在我看来,你不能用标准宏来做到这一点。然而令人惊讶的是,如果您可以交换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))),)

所以

  • absent 属性映射到Some(None)
  • null 映射到 None
  • 值映射到Some(Some(value))

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

【讨论】:

  • 是的......我确实考虑过我可能会交换 null 和 undefined 的情况并实现这样的目标。感谢您确认。
【解决方案3】:

根据@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 正在处理缺失和显式空值
  • JsonConfiguration 同时采用Json.WithDefaultValuesoptionHandlers
  • 如何设置默认值。注意 RequestObject.payload 的默认值

【讨论】:

    猜你喜欢
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2016-12-04
    • 1970-01-01
    • 1970-01-01
    • 2014-11-10
    相关资源
    最近更新 更多