使用 Circe 展平嵌套 JSON 个对象

Flattening nested JSON objects with 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 中的另一个常见问题解答。)

您可以使用递归方法在 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 值和 returns None 作为非 JSON 对象值作为信号该字段不需要展平,或者 Some 在 JSON 对象的情况下包含一系列展平字段。

如果我们有这样的 JSON 值:

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
  ]
}

请注意,flattenToFields 不是尾递归的,并且会溢出深度嵌套的 JSON 对象的堆栈,但可能直到你有几千层深度,所以它不太可能实践中的一个问题。您可以使它成为尾递归而不会有太多麻烦,但是在只有几层嵌套的常见情况下会以额外的开销为代价。

我提议 的变体。变化涉及 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"
}