播放JSON:使用未知密钥读取和验证JsObject

我正在使用几个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]](记住,“attributes”只是一个子文档).

然后有一个简单的映射,其中包含应该用于验证属性的键和数组类型的允许组合.编辑:此映射特定于每个请求(或者特定于每种类型的json文档).但您可以假设它始终在范围内可用.

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

因此,对于上面显示的JSON,Reads应该创建两个错误:

>“keyB”确实存在,但是类型错误(期望String,是boolean).
>“keyD”缺失(而keyC不需要,可以忽略).

我在创建必要的Reads时遇到了麻烦.从外部Reads的角度来看,我作为第一步尝试的第一件事:

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

我认为这是一个很好的第一步,因为如果JSON结构不是包含字符串和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 …)在编译时是未知的.后来我说这些键是用于验证所需的地图的一部分.这听起来像是一个矛盾,但事实是:必需的是每个文档/请求特定的,并且在编译时也不知道.但是您不必担心这一点,只需假设对于每个请求,范围中已经提供了正确的要求.

最佳答案 你对任务太困惑了

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将检查类型和必需)并使用Reads.pure(Attribute [A])跳过省略(如果不是必需的话) .然后元组转换为list(_.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)
点赞