【问题标题】:Transform JSON tree to other format (XML, CSV etc.) recursively with circe使用 circe 递归地将 JSON 树转换为其他格式(XML、CSV 等)
【发布时间】:2019-07-31 04:19:00
【问题描述】:

为了使用 circe 将 JSON 节点转换为 JSON 以外的 other 格式(如 XML、CSV 等),我想出了一个解决方案,我必须访问 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,那么这一切都很好而且花花公子,只有将上面的代码放在包 @ 中才能工作987654324@.

如何使用公共 circe API 实现与上述相同的结果?

【问题讨论】:

    标签: json xml scala scala-cats circe


    【解决方案1】:

    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>
    

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

    所以答案是“使用fold”(或其他答案中建议的asX 方法——该方法更灵活,但通常可能不那么惯用且更冗长)。如果您关心我们为什么做出不公开构造函数的设计决定,您可以跳到这个答案的末尾,但是这种问题出现了很多,所以我也想解决一些相关的问题首先。

    关于命名的附注

    请注意,此方法使用的名称“折叠”是从 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 为您提供了一个强大的案例类提取器替代方案:

    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 的其他光学元件组合。

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

    为什么不只是案例类

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

    在某些情况下,包括最重要的 io.circe.Json 类型,我们不想鼓励用户将 ADT 叶子视为 具有有意义的类型。 JSON 值“是”布尔值或字符串或 单位或Seq[Json]JsonNumberJsonObject。介绍 像JStringJNumber 等类型进入公共 API 混淆事物。

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

    【讨论】:

    • 非常感谢您提供非常详细的答案。有足够的方法可供选择!
    【解决方案2】:

    您可以使用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)
    }
    

    【讨论】:

    • 请不要这样做。至少,如果您要使用asX,请正确使用它——Option 上的模式匹配或其他东西,而不是调用.get
    猜你喜欢
    • 2021-07-01
    • 1970-01-01
    • 1970-01-01
    • 2022-11-30
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2017-01-11
    • 2020-08-25
    相关资源
    最近更新 更多