【问题标题】:Play [Scala]: How to flatten a JSON object播放 [Scala]:如何展平 JSON 对象
【发布时间】:2014-06-17 21:18:06
【问题描述】:

鉴于以下 JSON...

{
  "metadata": {
    "id": "1234",
    "type": "file",
    "length": 395
  }
}

...如何将其转换为

{
  "metadata.id": "1234",
  "metadata.type": "file",
  "metadata.length": 395
}

发送。

【问题讨论】:

    标签: json scala playframework


    【解决方案1】:

    您可以使用 Play 的 JSON transformers 非常简洁地做到这一点。以下是我的想法,我相信它可以大大改进:

    import play.api.libs.json._
    
    val flattenMeta = (__ \ 'metadata).read[JsObject].flatMap(
      _.fields.foldLeft((__ \ 'metadata).json.prune) {
        case (acc, (k, v)) => acc andThen __.json.update(
          Reads.of[JsObject].map(_ + (s"metadata.$k" -> v))
        )
      }
    )
    

    然后:

    val json = Json.parse("""
      {
        "metadata": {
          "id": "1234",
          "type": "file",
          "length": 395
        }
      }
    """)
    

    还有:

    scala> json.transform(flattenMeta).foreach(Json.prettyPrint _ andThen println)
    {
      "metadata.id" : "1234",
      "metadata.type" : "file",
      "metadata.length" : 395
    }
    

    如果您想在树中的其他位置处理 metadata 字段,只需更改路径即可。


    请注意,在这里使用转换器可能有点过头了——参见例如Pascal Voitot 在this thread 中的意见,他提出以下建议:

    (json \ "metadata").as[JsObject].fields.foldLeft(Json.obj()) {
      case (acc, (k, v)) => acc + (s"metadata.$k" -> v)
    }
    

    它不是可组合的,你可能不想在实际代码中使用as,但它可能就是你所需要的。

    【讨论】:

    • 美丽的 Json 转换器 :)
    • 谢谢@JulienLafont,但实际上我并不特别喜欢它——我觉得代码仍然有点模糊了意图。可能有更好的方法。
    • 对我来说仍然很紧;包裹在一个传递参数的函数中 - def doDotNotation(jsPath: JsPath, prefix: String): JsObject。使用递归将点符号添加到嵌套 JsObjects 很有用
    【解决方案2】:

    这绝对不是微不足道的,但可以通过尝试递归地展平它。我尚未对此进行彻底测试,但它适用于您的示例以及我使用数组提出的其他一些基本示例:

    object JsFlattener {
    
        def apply(js: JsValue): JsValue = flatten(js).foldLeft(JsObject(Nil))(_++_.as[JsObject])
    
        def flatten(js: JsValue, prefix: String = ""): Seq[JsValue] = {
            js.as[JsObject].fieldSet.toSeq.flatMap{ case (key, values) =>
                values match {
                    case JsBoolean(x) => Seq(Json.obj(concat(prefix, key) -> x))
                    case JsNumber(x) => Seq(Json.obj(concat(prefix, key) -> x))
                    case JsString(x) => Seq(Json.obj(concat(prefix, key) -> x))
                    case JsArray(seq) => seq.zipWithIndex.flatMap{ case (x, i) => flatten(x, concat(prefix, key + s"[$i]")) }  
                    case x: JsObject => flatten(x, concat(prefix, key))
                    case _ => Seq(Json.obj(concat(prefix, key) -> JsNull))
                }
            }
        }
    
        def concat(prefix: String, key: String): String = if(prefix.nonEmpty) s"$prefix.$key" else key
    
    }
    

    JsObject 有一个 fieldSet 方法,它返回一个 Set[(String, JsValue)],我映射了它,与 JsValue 子类匹配,并从那里继续递归消费。

    您可以通过将JsValue 传递给apply 来使用此示例:

    val json = Json.parse("""
        {
          "metadata": {
            "id": "1234",
            "type": "file",
            "length": 395
          }
        }
    """
    JsFlattener(json)
    

    我们将把它作为练习留给读者,让代码看起来更漂亮。

    【讨论】:

    • 我认为“js.as[JsObject]”在没有传递对象时会失败。 "seq.zipWithIndex.flatMap{ case (x, i) => flatten(x..." 仅当数组的每个元素都是对象时才有效。 "x": ["a","b 的任何实例"] 将抛出异常,因为 flatten(x...) 传递的是 JsString,而不是 JsObject。
    • 我是 scala 新手,如何调用这个函数,以及我们为 JsValue 使用哪些导入库。如果我们传递 Dataframe,那么我们应该在这段代码中进行哪些更改?
    【解决方案3】:

    这是我对这个问题的看法,基于@Travis Brown 的第二个解决方案。

    它递归地遍历 json 并在每个键前面加上其父键。

    def flatten(js: JsValue, prefix: String = ""): JsObject = js.as[JsObject].fields.foldLeft(Json.obj()) {
        case (acc, (k, v: JsObject)) => {
            if(prefix.isEmpty) acc.deepMerge(flatten(v, k))
            else acc.deepMerge(flatten(v, s"$prefix.$k"))
        }
        case (acc, (k, v)) => {
            if(prefix.isEmpty) acc + (k -> v)
            else acc + (s"$prefix.$k" -> v)
        }
    }
    

    变成这样:

    {
      "metadata": {
        "id": "1234",
        "type": "file",
        "length": 395
      },
      "foo": "bar",
      "person": {
        "first": "peter",
        "last": "smith",
        "address": {
          "city": "Ottawa",
          "country": "Canada"
        }
      }
    }
    

    进入这个:

    {
      "metadata.id": "1234",
      "metadata.type": "file",
      "metadata.length": 395,
      "foo": "bar",
      "person.first": "peter",
      "person.last": "smith",
      "person.address.city": "Ottawa",
      "person.address.country": "Canada"
    }
    

    【讨论】:

      【解决方案4】:

      @Trev 在这里有最好的解决方案,完全通用和递归,但它缺少数组支持的案例。我想要在这种情况下有效的东西:

      转这个:

      {
        "metadata": {
          "id": "1234",
          "type": "file",
          "length": 395
        },
        "foo": "bar",
        "person": {
          "first": "peter",
          "last": "smith",
          "address": {
            "city": "Ottawa",
            "country": "Canada"
          },
          "kids": ["Bob", "Sam"]
        }
      }
      

      进入这个:

      {
        "metadata.id": "1234",
        "metadata.type": "file",
        "metadata.length": 395,
        "foo": "bar",
        "person.first": "peter",
        "person.last": "smith",
        "person.address.city": "Ottawa",
        "person.address.country": "Canada",
        "person.kids[0]": "Bob",
        "person.kids[1]": "Sam"
      }
      

      我已经做到了,这似乎可行,但似乎过于冗长。任何有助于使这个漂亮的帮助将不胜感激。

      def flatten(js: JsValue, prefix: String = ""): JsObject = js.as[JsObject].fields.foldLeft(Json.obj()) {
        case (acc, (k, v: JsObject)) => {
          val nk = if(prefix.isEmpty) k else s"$prefix.$k"
          acc.deepMerge(flatten(v, nk))
        }
        case (acc, (k, v: JsArray)) => {
          val nk = if(prefix.isEmpty) k else s"$prefix.$k"
          val arr = flattenArray(v, nk).foldLeft(Json.obj())(_++_)
          acc.deepMerge(arr)
        }
        case (acc, (k, v)) => {
          val nk = if(prefix.isEmpty) k else s"$prefix.$k"
          acc + (nk -> v)
        }
      }
      
      def flattenArray(a: JsArray, k: String = ""): Seq[JsObject] = {
        flattenSeq(a.value.zipWithIndex.map {
          case (o: JsObject, i: Int) =>
            flatten(o, s"$k[$i]")
          case (o: JsArray, i: Int) =>
            flattenArray(o, s"$k[$i]")
          case a =>
            Json.obj(s"$k[${a._2}]" -> a._1)
        })
      }
      
      def flattenSeq(s: Seq[Any], b: Seq[JsObject] = Seq()): Seq[JsObject] = {
        s.foldLeft[Seq[JsObject]](b){
          case (acc, v: JsObject) =>
            acc:+v
          case (acc, v: Seq[Any]) =>
            flattenSeq(v, acc)
        }
      }
      

      【讨论】:

      • 对于多个文件中的大量json字符串如何调用该方法?
      • 嘿,我也在寻找类似的解决方案,但我希望根据 N 个元素将我的 JSON 拆分为 N 个 json。这是我的问题,但仍未得到解答。:stackoverflow.com/questions/51668341/…
      【解决方案5】:

      感谢 m-z,它非常有帮助。 (我对 Scala 不太熟悉。)

      我想为“flatten”添加一行,使用原始 JSON 数组,如“{metadata: ["aaa", "bob"]}"。

        def flatten(js: JsValue, prefix: String = ""): Seq[JsValue] = {
      
          // JSON primitive array can't convert to JsObject
          if(!js.isInstanceOf[JsObject]) return Seq(Json.obj(prefix -> js))
      
          js.as[JsObject].fieldSet.toSeq.flatMap{ case (key, values) =>
            values match {
              case JsBoolean(x) => Seq(Json.obj(concat(prefix, key) -> x))
              case JsNumber(x) => Seq(Json.obj(concat(prefix, key) -> x))
              case JsString(x) => Seq(Json.obj(concat(prefix, key) -> x))
              case JsArray(seq) => seq.zipWithIndex.flatMap{ case (x, i) => flatten(x, concat(prefix, key + s"[$i]")) }
              case x: JsObject => flatten(x, concat(prefix, key))
              case _ => Seq(Json.obj(concat(prefix, key) -> JsNull))
            }
          }
        }
      

      【讨论】:

        【解决方案6】:

        在之前的解决方案的基础上,已经尝试将代码简化一点

          def getNewKey(oldKey: String, newKey: String): String = {
            if (oldKey.nonEmpty) oldKey + "." + newKey else newKey
          }
        
          def flatten(js: JsValue, prefix: String = ""): JsObject = {
            if (!js.isInstanceOf[JsObject]) return Json.obj(prefix -> js)
            js.as[JsObject].fields.foldLeft(Json.obj()) {
              case (o, (k, value)) => {
                o.deepMerge(value match {
                  case x: JsArray => x.as[Seq[JsValue]].zipWithIndex.foldLeft(o) {
                    case (o, (n, i: Int)) => o.deepMerge(
                      flatten(n.as[JsValue], getNewKey(prefix, k) + s"[$i]")
                    )
                  }
                  case x: JsObject => flatten(x, getNewKey(prefix, k))
                  case x => Json.obj(getNewKey(prefix, k) -> x.as[JsValue])
                })
              }
            }
          }
        

        【讨论】:

        • 我如何调用该方法。对 scala 和函数式编程来说非常新。我阅读了 JSON 文件,那我该怎么办?
        猜你喜欢
        • 2013-01-11
        • 1970-01-01
        • 2021-03-05
        • 2018-10-28
        • 2020-12-08
        • 2012-09-17
        • 1970-01-01
        • 2021-03-07
        • 2019-05-25
        相关资源
        最近更新 更多