用Play框架解析递归嵌套的Json结构

时间:2016-02-23 15:59:40

标签: scala playframework playframework-2.3 argonaut

我们正在使用Play framework 2.3.4。从其中一个API我们对第三方服务进行Web服务调用 - 返回响应的结构是动态的,可能会发生变化。只有在响应JSON中静态的子结构才是特定元素并嵌套在其中。例如。

{
 "response": 
  {
   "someElement1": "",
   "element2": true,
   "buckets": [
     {
       "key": "keyvalue",
       "docCount": 10,
       "somethingElse": {
         "buckets": [
           {
             "key": "keyvalue1",
             "docCount": 5,
             "somethingElseChild": {
               "buckets": [
                 {
                   "key": "Tan",
                   "docCount": 1
                 }
               ]
             }
           },
           {
             "key": "keyvalue2",
             "docCount": 3,
             "somethingElseChild": {
               "buckets": [
                 {
                   "key": "Ban",
                   "docCount": 6
                 }
               ]
             }
           }
         ]
       }
     }
   ]
  }
}

我们不知道响应结构会是什么样子,但我们知道的唯一事情就是会有"桶和#34;嵌套元素在响应中的某个位置,你可以看到还有其他嵌套的#34;桶#34;在顶级水平"水桶"元件。另请注意,buckets数组中的结构也不清楚,如果有另一个子存储桶,则确定子存储桶必须位于父bucket内的某个位置 - 这样模式是一致的。

什么是解析这种递归结构并以递归方式填充Bucket类后的最佳方法?

case class Bucket(key:String,docCount, subBuckets: List[Bucket] ) 

首先我想

val json = Json.parse(serviveResponse)
val buckets = (json \ "response" \\ "buckets") 

但这不会带来buckets递归而不是正确的遍历方式。

有什么想法吗?

2 个答案:

答案 0 :(得分:1)

要制作Reads[T] for a recursive type T,您必须

  • 将其定义为lazy val
  • 在需要递归解析的地方使用lazyRead
  • 并手动向其传递Reads[T]对象或其衍生物。

当然,您必须知道buckets元素可能出现在哪些路径上,并且还要考虑其中的任何路径。您可以使用orElse尝试多条路径。

对于Bucket的定义,Reads可能如下所示:

import play.api.libs.json._
import play.api.libs.functional.syntax._

implicit lazy val readBucket: Reads[Bucket] = (
  (__ \ "key").read[String] and
  (__ \ "docCount").read[Int] and
  (
    (__ \ "somethingElse" \ "buckets").lazyRead(Reads.list(readBucket)) orElse
    (__ \ "somethingElseChild" \ "buckets").lazyRead(Reads.list(readBucket)) orElse
    Reads.pure(List.empty[Bucket])
  )
) (Bucket.apply _)

您可以通过将公共部分提取到函数来简化它,例如:

def readsBucketsAt(path: JsPath): Reads[List[Bucket]] =
  (path \ "buckets").lazyRead(Reads.list(readBucket))

/* ... 
  readsBucketsAt(__ \ "somethingElse") orElse
  readsBucketsAt(__ \ "somethingElseChild") orElse
  Reads.pure(List.empty[Bucket])
... */

此示例并未考虑在单个存储桶内的不同路径上可能合并多个buckets数组。因此,如果您需要该功能,我相信您必须为play.api.libs.functional.Monoid定义和使用Reads[List[T]]实例,或以某种方式合并JsArray的现有monoid实例。

答案 1 :(得分:0)

递归解析。这样的事情(未经测试):

case class Bucket(key: String, count: Int, sub: List[Bucket])

def FAIL = throw new Exception  // Put better error-handling here

def getBucket(js: JsValue): Bucket = js match {
  case o: JsObject =>
    val key = (o \ "key") match {
      case JsString(s) => s
      case _ => FAIL
    }
    val count = (o \ "docCount") match {
      case JsNumber(n) => n.toInt
      case _ => FAIL
    }
    val sub = (o \ "buckets") match {
      case a: JsArray => a.value.toList.map(getBucket)
      case _ => Nil
    }
    Bucket(key, count, sub)
  case _ => throw new Exception
}