反序列化JSON,区分缺失值和空值

时间:2018-01-29 22:09:40

标签: json scala play-json

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

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

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

'a'的值表示:

  • 无 - “a”缺失 - 正常的play-json behavipr
  • 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类:

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

3 个答案:

答案 0 :(得分:1)

不幸的是,我不知道如何自动实现你想要的。现在,在我看来,你不能用标准的宏来做到这一点。但令人惊讶的是,如果你可以交换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(部分(无)))   
' {" a":null}' => JsSuccess(XXX(无),)   
' {" a":" abc"}' => JsSuccess(XXX(一些(部分(ABC))))

所以

  • 缺席属性已映射到Some(None)
  • null已映射到None
  • 值已映射到Some(Some(value))

这对开发人员来说是笨拙而且有点出乎意料,但至少这可以区分所有3种选择。 null和"缺席"交换的选择是我发现区分这些情况的唯一方法是将目标类中的值声明为Option并同时使用默认值,在这种情况下,默认值是"缺席" case被映射到;很遗憾,您无法控制null映射到的值 - 它总是None

答案 1 :(得分:1)

我最近找到了执行此操作的合理方法。我正在使用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宏。 / p>

答案 2 :(得分:1)

按照@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 基本上翻转了如何 与 JsDefined(JsNull)
  • 中的原始实现相比,JsUndefinedOptionHandlers.Default 正在处理缺失和显式空值
  • 同时采用 Json.WithDefaultValuesoptionHandlers 的 JsonConfiguration
  • 如何设置默认值。注意 RequestObject.payload 的默认值