【问题标题】:parse recursively nested Json structure with Play framework使用 Play 框架解析递归嵌套的 Json 结构
【发布时间】:2016-02-23 15:59:39
【问题描述】:

我们使用的是 Play 框架 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
                 }
               ]
             }
           }
         ]
       }
     }
   ]
  }
}

我们不知道响应结构会是什么样子,但我们唯一知道的是响应中的某处会有“桶”嵌套元素,正如您所见,a 中还有其他嵌套的“桶”顶级“桶”元素。另请注意buckets 数组内部的结构也不清楚,如果会有另一个子存储桶,则肯定子存储桶必须位于父级bucket 内部的某个位置——这样模式是一致的。

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

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

首先我想

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

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

有什么想法吗?

【问题讨论】:

  • @Reactormonk :虽然我更愿意继续使用 play json,因为我们已经在项目的其他地方大量使用了 play json。如果 argnaut 要为手头的问题提供优雅/高效的解决方案,我愿意使用 argnaut 来满足这个特殊要求。我只看过argonaut,但从未使用过它。您介意分享更多关于如何使用 argonaut 实现这一目标的详细信息吗?
  • @Reactormonk :刚刚在问题中添加了 argonaut 标签 - 如果您可以分享您的答案。
  • 使用 argonaut 是一个侧切线。你只是想递归地构建你的数据结构,对吧?写一个递归方法。
  • @RexKerr:当然。我的问题不在于如何编写递归方法;但是如何使用 JsPath (playframework.com/documentation/2.2.x/api/scala/…) 递归地查找“桶”- 我不知道路径的前期知识。

标签: scala playframework playframework-2.3 argonaut


【解决方案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 数组。因此,如果您需要该功能,我相信您必须为Reads[List[T]] 定义和使用play.api.libs.functional.Monoid 实例,或者以某种方式组合JsArray 的现有monoid 实例。

【讨论】:

    【解决方案2】:

    递归解析。像这样的东西(未测试):

    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
    }
    

    【讨论】:

      猜你喜欢
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 2014-01-05
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 2019-06-28
      相关资源
      最近更新 更多