使用 circe 递归地将 JSON 树转换为其他格式(XML、CSV 等)

Transform JSON tree to other format (XML, CSV etc.) recursively with circe

为了将 JSON 节点转换为 other 格式而不是 JSON(如 XML、CSV 等)与 circe I想出了一个解决方案,我必须访问 circe 的内部数据结构。

这是我的工作示例,它将 JSON 转换为 XML 字符串(不完美,但你明白了):

package io.circe

import io.circe.Json.{JArray, JBoolean, JNull, JNumber, JObject, JString}
import io.circe.parser.parse

object Sample extends App {

  def transformToXMLString(js: Json): String = js match {
    case JNull => ""
    case JBoolean(b) => b.toString
    case JNumber(n) => n.toString
    case JString(s) => s.toString
    case JArray(a) => a.map(transformToXMLString(_)).mkString("")
    case JObject(o) => o.toMap.map {
      case (k, v) => s"<${k}>${transformToXMLString(v)}</${k}>"
    }.mkString("")
  }

  val json =
    """{
      | "root": {
      |  "sampleboolean": true,
      |  "sampleobj": {
      |    "anInt": 1,
      |    "aString": "string"
      |  },
      |  "objarray": [
      |     {"v1": 1},
      |     {"v2": 2}
      |  ]
      | }
      |}""".stripMargin

  val res = transformToXMLString(parse(json).right.get)
  println(res)
}

结果:

<root><sampleboolean>true</sampleboolean><sampleobj><anInt>1</anInt><aString>string</aString></sampleobj><objarray><v1>1</v1><v2>2</v2></objarray></root>

如果低级 JSON 对象(如 JBoolean, JString, JObject 等)不是 package private 就可以了如果将上面的代码放在 package io.circe.

包中,则上面的代码有效

如何使用 public 圈 API 获得与上面相同的结果?

您可以使用 is* 方法测试类型,然后使用 as*

import io.circe._
import io.circe.parser.parse

object CirceToXml extends App {


  def transformToXMLString(js: Json): String = {
    if (js.isObject) {
      js.asObject.get.toMap.map {
        case (k, v) =>
          s"<$k>${transformToXMLString(v)}</${k}>"
      }.mkString
    } else if (js.isArray) {
      js.asArray.get.map(transformToXMLString).mkString
    } else if (js.isString) {
      js.asString.get
    } else {
      js.toString()
    }
  }

  val json =
    """{
      | "root": {
      |  "sampleboolean": true,
      |  "sampleobj": {
      |    "anInt": 1,
      |    "aString": "string"
      |  },
      |  "objarray": [
      |     {"v1": 1},
      |     {"v2": 2}
      |  ]
      | }
      |}""".stripMargin

  val res = transformToXMLString(parse(json).right.get)
  println(res)
}

Json 上的 fold 方法允许您非常简洁地执行此类操作(并且以一种强制穷举的方式,就像对密封特征进行模式匹配一​​样):

import io.circe.Json

def transformToXMLString(js: Json): String = js.fold(
  "",
  _.toString,
  _.toString,
  identity,
  _.map(transformToXMLString(_)).mkString(""),
  _.toMap.map {
    case (k, v) => s"<${k}>${transformToXMLString(v)}</${k}>"
  }.mkString("")
)

然后:

scala> import io.circe.parser.parse
import io.circe.parser.parse

scala> transformToXMLString(parse(json).right.get)
res1: String = <root><sampleboolean>true</sampleboolean><sampleobj><anInt>1</anInt><aString>string</aString></sampleobj><objarray><v1>1</v1><v2>2</v2></objarray></root>

与您的实现完全相同的结果,但字符少了一些,并且不依赖于实现的私有细节。

所以答案是 "use fold"(或另一个答案中建议的 asX 方法——该方法更灵活,但通常可能不那么惯用且更冗长)。如果你关心我们为什么设计决定不暴露构造函数,你可以跳到这个答案的末尾,但是这种问题经常出现,所以我也想解决一些相关的问题首先.

关于命名的附注

请注意,此方法使用的名称 "fold" 是从 Argonaut 继承而来的,可以说是不准确的。当我们谈论递归代数数据类型的变形(或折叠)时,我们指的是一个函数,我们在传入的函数的参数中看不到 ADT 类型。例如,列表折叠的签名看起来像这样:

def foldLeft[B](z: B)(op: (B, A) => B): B

不是这个:

def foldLeft[B](z: B)(op: (List[A], A) => B): B

因为io.circe.Json是一个递归的ADT,它的fold方法真的应该是这样的:

def properFold[X](
  jsonNull: => X,
  jsonBoolean: Boolean => X,
  jsonNumber: JsonNumber => X,
  jsonString: String => X,
  jsonArray: Vector[X] => X,
  jsonObject: Map[String, X] => X
): X

而不是:

def fold[X](
  jsonNull: => X,
  jsonBoolean: Boolean => X,
  jsonNumber: JsonNumber => X,
  jsonString: String => X,
  jsonArray: Vector[Json] => X,
  jsonObject: JsonObject => X
): X

但在实践中前者似乎用处不大,所以circe只提供了后者(如果要递归,就得自己动手),沿用Argonaut的说法fold。这个一直让我有点不适应,以后可能会改名字。

关于性能的附注

在某些情况下,实例化 fold 期望的六个函数可能非常昂贵,因此 circe 还允许您将这些操作捆绑在一起:

import io.circe.{ Json, JsonNumber, JsonObject }

val xmlTransformer: Json.Folder[String] = new Json.Folder[String] {
    def onNull: String = ""
  def onBoolean(value: Boolean): String = value.toString
  def onNumber(value: JsonNumber): String = value.toString
  def onString(value: String): String = value
  def onArray(value: Vector[Json]): String =
    value.map(_.foldWith(this)).mkString("")
  def onObject(value: JsonObject): String = value.toMap.map {
    case (k, v) => s"<${k}>${transformToXMLString(v)}</${k}>"
  }.mkString("")
}

然后:

scala> parse(json).right.get.foldWith(xmlTransformer)
res2: String = <root><sampleboolean>true</sampleboolean><sampleobj><anInt>1</anInt><aString>string</aString></sampleobj><objarray><v1>1</v1><v2>2</v2></objarray></root>

使用 Folder 的性能优势会因您使用的是 2.11 还是 2.12 而有所不同,但如果您对 JSON 值执行的实际操作很便宜,您可以预计 Folder 版本的吞吐量大约是 fold 的两倍。顺便说一下,它也比内部构造函数的模式匹配快得多,至少在 benchmarks we've done:

Benchmark                           Mode  Cnt      Score    Error  Units
FoldingBenchmark.withFold          thrpt   10   6769.843 ± 79.005  ops/s
FoldingBenchmark.withFoldWith      thrpt   10  13316.918 ± 60.285  ops/s
FoldingBenchmark.withPatternMatch  thrpt   10   8022.192 ± 63.294  ops/s

那是在 2.12 上。我相信您应该在 2.11 上看到更多不同。

关于光学的旁注

如果您真的想要模式匹配,circe-optics 为您提供了 case class 提取器的高性能替代方案:

import io.circe.Json, io.circe.optics.all._

def transformToXMLString(js: Json): String = js match {
    case `jsonNull` => ""
  case jsonBoolean(b) => b.toString
  case jsonNumber(n) => n.toString
  case jsonString(s) => s.toString
  case jsonArray(a) => a.map(transformToXMLString(_)).mkString("")
  case jsonObject(o) => o.toMap.map {
    case (k, v) => s"<${k}>${transformToXMLString(v)}</${k}>"
  }.mkString("")
}

这与您的原始版本几乎完全相同,但每个提取器都是一个单片眼镜 棱镜,可以与 the Monocle library 中的其他光学器件组合。

(这种方法的缺点是你失去了详尽检查,但不幸的是,这无济于事。)

为什么不只是 case classes

当我第一次开始做 circe 时,我在 a document about some of my design decisions 中写了以下内容:

In some cases, including most significantly here the io.circe.Json type, we don't want to encourage users to think of the ADT leaves as having meaningful types. A JSON value "is" a boolean or a string or a unit or a Seq[Json] or a JsonNumber or a JsonObject. Introducing types like JString, JNumber, etc. into the public API just confuses things.

我想要一个真正最小的 API(尤其是避免暴露无意义类型的 API)并且我想要优化 JSON 表示的空间。 (我也根本不希望人们使用 JSON AST,但这更像是一场失败的战斗。)我仍然认为隐藏构造函数是正确的决定,即使我没有还没有真正利用他们在优化方面的缺席,尽管这个问题经常出现。