【问题标题】:Play JSON: Reading and validating a JsObject with unknown keys播放 JSON:读取和验证具有未知键的 JsObject
【发布时间】:2016-04-19 21:59:10
【问题描述】:

我正在阅读使用多个 Reads[T] 实现的嵌套 JSON 文档,但是,我坚持使用以下子对象:

{
    ...,
    "attributes": {
        "keyA": [1.68, 5.47, 3.57],
        "KeyB": [true],
        "keyC": ["Lorem", "Ipsum"]
     },
     ...
}

键(“keyA”、“keyB”...)以及键的数量在编译时是未知的,并且可能会有所不同。键的值始终是JsArray 实例,但大小和类型不同(但是,特定数组的所有元素必须具有相同 JsValue 类型)。

单个属性的 Scala 表示:

case class Attribute[A](name: String, values: Seq[A])
// 'A' can only be String, Boolean or Double

目标是创建一个Reads[Seq[Attribute]],在转换整个文档时可用于“属性”字段(请记住,“属性”只是一个子文档)。

然后有一个简单的映射,其中包含应用于验证属性的键和数组类型的允许组合。编辑:此地图特定于每个请求(或者更确切地说,特定于每种类型的 json 文档)。但是你可以假设它在作用域中总是可用的。

val required = Map(
  "KeyA" -> "Double",
  "KeyB" -> "String",
  "KeyD" -> "String",
)

所以在上面显示的 JSON 的情况下,Reads 应该会产生两个错误:

  1. “keyB”确实存在,但类型错误(应为字符串,为布尔值)。
  2. “keyD”缺失(而 keyC 不需要,可以忽略)。

我无法创建必要的Reads。作为第一步我尝试的第一件事,从外部Reads的角度来看:

...
(__ \ "attributes").reads[Map[String, JsArray]]...
...

我认为这是很好的第一步,因为如果 JSON 结构不是包含 Strings 和 JsArrays 作为键值对的对象,那么 Reads 将失败并显示正确的错误消息。它有效,但是:我不知道如何从那里继续。当然我可以创建一个将Map 转换为Seq[Attribute] 的方法,但是这个方法应该返回一个JsResult,因为还有更多的验证要做。

我尝试的第二件事:

  val attributeSeqReads = new Reads[Seq[Attribute]] {
    def reads(json: JsValue) = json match {
      case JsObject(fields) => processAttributes(fields)
      case _ => JsError("attributes not an object")
    }
    def processAttributes(fields: Map[String, JsValue]): JsResult[Seq[Attribute]] = {
      // ...
    }
  }

这个想法是在processAttributes 中手动验证地图的每个元素。但我认为这太复杂了。任何帮助表示赞赏。

编辑澄清:

在文章的开头我说过键(keyA,keyB...)在编译时是未知的。后来我说这些键是映射required 的一部分,用于验证。这听起来很矛盾,但问题是:required 特定于每个文档/请求,并且在编译时也不知道。但是您不必担心这一点,只需假设对于每个请求,正确的required 已经在范围内可用。

【问题讨论】:

    标签: json scala playframework playframework-2.0


    【解决方案1】:

    你被任务搞糊涂了

    键(“keyA”、“keyB”...)以及键的数量在编译时是未知的,并且可能会有所不同

    那么key的数量和它们的类型是事先知道的和最终的?

    所以在上面显示的 JSON 的情况下,读取应该创建两个 错误:

    1. “keyB”确实存在,但类型错误(应为字符串,是 布尔值)。

    2. “keyD”缺失(而 keyC 不需要,可以忽略)。

    您的主要任务只是检查可用性和合规性?

    您可以使用Reads.list(Reads.of[A]) 为您的每个键实现Reads[Attribute](此读取将检查类型和必需)并使用Reads.pure(Attribute[A]) 跳过省略(如果不需要)。然后元组转换为列表(_.productIterator.toList),你会得到Seq[Attribute]

    val r = (
      (__ \ "attributes" \ "keyA").read[Attribute[Double]](list(of[Double]).map(Attribute("keyA", _))) and
        (__ \ "attributes" \ "keyB").read[Attribute[Boolean]](list(of[Boolean]).map(Attribute("keyB", _))) and
        ((__ \ "attributes" \ "keyC").read[Attribute[String]](list(of[String]).map(Attribute("keyC", _))) or Reads.pure(Attribute[String]("keyC", List()))) and 
        (__ \ "attributes" \ "keyD").read[Attribute[String]](list(of[String]).map(Attribute("keyD", _)))        
      ).tupled.map(_.productIterator.toList)
    
    scala>json1: play.api.libs.json.JsValue = {"attributes":{"keyA":[1.68,5.47,3.57],"keyB":[true],"keyD":["Lorem","Ipsum"]}}
    
    scala>res37: play.api.libs.json.JsResult[List[Any]] = JsSuccess(List(Attribute(keyA,List(1.68, 5.47, 3.57)), Attribute(KeyB,List(true)), Attribute(keyC,List()), Attribute(KeyD,List(Lorem, Ipsum))),)   
    
    scala>json2: play.api.libs.json.JsValue = {"attributes":{"keyA":[1.68,5.47,3.57],"keyB":[true],"keyC":["Lorem","Ipsum"]}}    
    
    scala>res38: play.api.libs.json.JsResult[List[Any]] = JsError(List((/attributes/keyD,List(ValidationError(List(error.path.missing),WrappedArray())))))    
    
    scala>json3: play.api.libs.json.JsValue = {"attributes":{"keyA":[1.68,5.47,3.57],"keyB":["Lorem"],"keyC":["Lorem","Ipsum"]}}    
    
    scala>res42: play.api.libs.json.JsResult[List[Any]] = JsError(List((/attributes/keyD,List(ValidationError(List(error.path.missing),WrappedArray()))), (/attributes/keyB(0),List(ValidationError(List(error.expected.jsboolean),WrappedArray())))))
    

    如果您将拥有超过 22 个属性,您将遇到另一个问题:具有超过 22 个属性的元组。

    用于运行时的动态属性

    灵感来自“Reads.traversableReads[F[_], A]”

    def attributesReads(required: Map[String, String]) = Reads {json =>
      type Errors = Seq[(JsPath, Seq[ValidationError])]
    
      def locate(e: Errors, idx: Int) = e.map { case (p, valerr) => (JsPath(idx)) ++ p -> valerr }
    
      required.map{
        case (key, "Double") => (__ \  key).read[Attribute[Double]](list(of[Double]).map(Attribute(key, _))).reads(json)
        case (key, "String") => (__ \ key).read[Attribute[String]](list(of[String]).map(Attribute(key, _))).reads(json)
        case (key, "Boolean") => (__ \ key).read[Attribute[Boolean]](list(of[Boolean]).map(Attribute(key, _))).reads(json)
        case _ => JsError("")
      }.iterator.zipWithIndex.foldLeft(Right(Vector.empty): Either[Errors, Vector[Attribute[_ >: Double with String with Boolean]]]) {
          case (Right(vs), (JsSuccess(v, _), _)) => Right(vs :+ v)
          case (Right(_), (JsError(e), idx)) => Left(locate(e, idx))
          case (Left(e), (_: JsSuccess[_], _)) => Left(e)
          case (Left(e1), (JsError(e2), idx)) => Left(e1 ++ locate(e2, idx))
        }
      .fold(JsError.apply, { res =>
        JsSuccess(res.toList)
      })
    }
    
    (__ \ "attributes").read(attributesReads(Map("keyA" -> "Double"))).reads(json)
    
    scala> json: play.api.libs.json.JsValue = {"attributes":{"keyA":[1.68,5.47,3.57],"keyB":[true],"keyD":["Lorem","Ipsum"]}}
    
    scala> res0: play.api.libs.json.JsResult[List[Attribute[_ >: Double with String with Boolean]]] = JsSuccess(List(Attribute(keyA,List(1.68, 5.47, 3.57))),/attributes)
    

    【讨论】:

    • 对不起,我编辑了我的帖子。允许的键特定于每个 json 文档。请求 A 可能包含与汽车相关的属性(重量、马力、颜色),而请求 B 可能包含与国家相关的属性(名称、国旗、总统、人口......)。我上面用于验证的地图required 是专门为每种类型的文档加载的。因此,在编译时不知道密钥和验证信息,但在运行时,我知道特定类型的 json dosument 允许哪些属性。
    • 好的。您如何为“不需要”属性实现键 -> 类型?
    • 你的意思是可选属性?暂时不需要。 required 中的所有内容都必须是传入 json 的一部分。如果 json 包含不需要的其他属性键,它们就会被忽略。
    • 感谢您的努力,安德烈。我很忙,但我会在接下来的几天内调查一下。
    • 如果我回答了您的问题,请标记为已回答。
    猜你喜欢
    • 1970-01-01
    • 1970-01-01
    • 2016-07-05
    • 2018-05-03
    • 1970-01-01
    • 2017-06-04
    • 1970-01-01
    • 2014-03-29
    相关资源
    最近更新 更多