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

时间:2016-04-16 22:58:44

标签: json scala playframework playframework-2.0

我正在使用多个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]],可以在转换整个文档时用于"属性" -field(请记住,"属性"只是一个子文档)。

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

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

因此,对于上面显示的JSON,Reads应该会产生两个错误:

  1. " KEYB"确实存在,但具有错误的类型(期望的String,是布尔值)。
  2. " keyD"缺少(而不需要keyC,可以忽略)。
  3. 我在创建必要的Reads时遇到了问题。从外部Reads

    的角度来看,我作为第一步尝试的第一件事
    ...
    (__ \ "attributes").reads[Map[String, JsArray]]...
    ...
    

    我认为这是一个很好的第一步,因为如果JSON结构不是包含String s和JsArray s作为键值对的对象,那么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

1 个答案:

答案 0 :(得分:1)

你对任务感到困惑

  

密钥(" keyA"," keyB" ...)以及密钥数量在编译时是未知的,可能会有所不同

因此,密钥及其类型的数量是事先已知的和最终的?

  

因此,在上面显示的JSON的情况下,Reads应该创建两个   错误:

     
      
  1. " KEYB"确实存在,但有错误的类型(期望的字符串,是   布尔值)。

  2.   
  3. " keyD"缺少(而不需要keyC,可以忽略)。

  4.   

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

您可以为Reads[Attribute]的每个密钥实施Reads.list(Reads.of[A])(此读取将检查类型和要求),并使用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)