使用Circe展平嵌套的JSON对象

时间:2019-09-20 10:04:14

标签: json scala circe

假设我有一个像这样的JSON对象:

{
   "foo": true,
   "bar": {
      "baz": 1,
      "qux": {
        "msg": "hello world",
        "wow": [null]
      }
   }
}

我想将其递归展平到单个层,并用下划线合并键:

{
   "foo": true,
   "bar_baz": 1,
   "baz_qux_msg": "hello world",
   "baz_qux_wow": [null]
}

如何使用Circe来做到这一点?

(注意:这是Circe Gitter channel中的另一个常见问题解答。)

2 个答案:

答案 0 :(得分:5)

您可以使用递归方法在Circe中轻松完成此任务:

import io.circe.Json

def flatten(combineKeys: (String, String) => String)(value: Json): Json = {
  def flattenToFields(value: Json): Option[Iterable[(String, Json)]] =
    value.asObject.map(
      _.toIterable.flatMap {
        case (k, v) => flattenToFields(v) match {
          case None => List(k -> v)
          case Some(fields) => fields.map {
            case (innerK, innerV) => combineKeys(k, innerK) -> innerV
          }
        }
      }
    )

  flattenToFields(value).fold(value)(Json.fromFields)
}

在此,我们内部的flattenToFields方法采用每个JSON值,如果它是非JSON对象值,则返回None,以表明该字段不需要展平,或者返回{{1 }}在JSON对象的情况下包含一系列扁平字段。

如果我们有这样的JSON值:

Some

我们可以验证val Right(doc) = io.circe.jawn.parse("""{ "foo": true, "bar": { "baz": 1, "qux": { "msg": "hello world", "wow": [null] } } }""") 符合我们的要求:

flatten

请注意,scala> flatten(_ + "_" + _)(doc) res1: io.circe.Json = { "foo" : true, "bar_baz" : 1, "bar_qux_msg" : "hello world", "bar_qux_wow" : [ null ] } 不是尾部递归的,并且会为深层嵌套的JSON对象溢出堆栈,但可能要等到几千个级别时才会溢出,因此在实践中这不太可能成为问题。您可以使它尾部递归而不会带来太多麻烦,但是会在您只有几层嵌套的常见情况下以额外的开销为代价。

答案 1 :(得分:0)

我提出了 solution by Travis Brown 的变体。变化涉及 JSON 列表中的对象,即如何处理

{
  "foo": true,
  "bar": {
    "baz": 1,
    "qux": {
      "msg": "hello world",
      "wow": [{"x": 1, "y": 2}, {"x": 3, "y": 4}]
    }
  }
}

递归处理列表中对象的一种可能解决方案是以下实现,其中将列表中的位置作为额外的关键部分

def flattenDeep(combineKeys: (String, String) => String)(value: Json): Json = {
  def flattenToFields(value: Json): Option[Iterable[(String, Json)]] = {
    value.fold(
      jsonNull = None,
      jsonNumber = _ => None,
      jsonString = _ => None,
      jsonBoolean = _ => None,
      jsonObject = { obj =>
        val fields = obj.toIterable.flatMap {
          case (field, json) =>
            flattenToFields(json).fold(Iterable(field -> json)) {
              _.map {
                case (innerField, innerJson) =>
                  combineKeys(field, innerField) -> innerJson
              }
            }
        }
        Some(fields)
      },
      jsonArray = { array =>
        val fields = array.zipWithIndex.flatMap {
          case (json, index) =>
            flattenToFields(json).fold(Iterable(index.toString -> json)) {
              _.map {
                case (innerField, innerJson) =>
                  combineKeys(index.toString, innerField) -> innerJson
              }
            }
        }
        Some(fields)
      }
    )
  }
  flattenToFields(value).fold(value)(Json.fromFields)
}

通过这个实现,上面的例子被扁平化为:

{
  "foo" : true,
  "bar_baz" : 1,
  "bar_qux_msg" : "hello world",
  "bar_qux_wow_0_x" : 1,
  "bar_qux_wow_0_y" : 2,
  "bar_qux_wow_1_x" : 3,
  "bar_qux_wow_1_y" : 4
}

对于更深的嵌套结构,仍然可以使用平面表示,例如

{
  "foo": true,
  "bar": {
    "baz": 1,
    "qux": {
      "msg": "hello world",
      "wow": [
        {
          "x": 1,
          "y": 2
        },
        {
          "x": 3,
          "y": 4
        }
      ],
      "deeper": [
        {
          "alpha": {
            "h": 12,
            "m": 1
          },
          "beta": [ "a", "b", "c" ]
        },
        {
          "alpha": {
            "h": 21,
            "m": 0
          },
          "beta": [ "z" ]
        }
      ]
    }
  }
}

将被压扁成

{
  "foo" : true,
  "bar_baz" : 1,
  "bar_qux_msg" : "hello world",
  "bar_qux_wow_0_x" : 1,
  "bar_qux_wow_0_y" : 2,
  "bar_qux_wow_1_x" : 3,
  "bar_qux_wow_1_y" : 4,
  "bar_qux_deeper_0_alpha_h" : 12,
  "bar_qux_deeper_0_alpha_m" : 1,
  "bar_qux_deeper_0_beta_0" : "a",
  "bar_qux_deeper_0_beta_1" : "b",
  "bar_qux_deeper_0_beta_2" : "c",
  "bar_qux_deeper_1_alpha_h" : 21,
  "bar_qux_deeper_1_alpha_m" : 0,
  "bar_qux_deeper_1_beta_0" : "z"
}