播放 JSON:读取并验证具有未知键的 JsObject

Play JSON: Reading and validating a JsObject with unknown keys

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

{
    ...,
    "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]],在转换整个文档时可用于 "attributes" 字段(记住,"attributes" 只是一个子文档) .

然后是一个简单的映射,其中包含应该用于验证属性的允许的键和数组类型组合。编辑:此地图 特定于每个请求 (或者更确切地说,特定于每种类型的 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] 的方法,但该方法应该 return 为 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 内手动验证地图的每个元素。但我认为这太复杂了。感谢任何帮助。

编辑澄清:

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

你对任务太迷茫了

The keys ("keyA", "keyB"...) as well as the amount of keys are not known at compile time and can vary

所以键的数量和类型是事先知道的,最后的?

So in the case of the JSON shown above, the Reads should create two errors:

  1. "keyB" does exist, but has the wrong type (expected String, was boolean).

  2. "keyD" is missing (whereas keyC is not needed and can be ignored).

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

您可以使用 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)