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

问题描述 投票:2回答:2

为了将JSON节点转换为JSON之外的其他格式(如XML,CSV等),我想出了一个解决方案,我必须访问circe的内部数据结构。

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

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等)在circe中不是包私有的话,这一切都很好,花花公子,只有当它放在包package io.circe中才能使这个代码工作。

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

json xml scala scala-cats circe
2个回答
5
投票

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

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方法 - 该方法更灵活,但一般来说可能不那么惯用且更冗长)。如果你关心为什么我们做出设计决定而不是暴露构造函数,你可以跳到这个答案的结尾,但是这个问题出现了很多,所以我也想解决几个相关的问题第一。

关于命名的附注

请注意,此方法使用名称“fold”是从Argonaut继承的,并且可以说是不准确的。当我们讨论递归代数数据类型的catamorphisms(或folds)时,我们指的是一个函数,我们在传入的函数的参数中看不到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。这总让我有点不舒服,名字可能会在未来改变。

A side note about performance

在某些情况下,实例化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("")
}

这几乎与原始版本的代码完全相同,但这些提取器中的每一个都是Monocle棱镜,可以与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,但这更像是一场失败的战斗。)我仍然认为隐藏构造函数是正确的决定,即使我没有真正利用他们缺乏优化(但),即使这个问题出现了很多。


0
投票

您可以使用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)
}
© www.soinside.com 2019 - 2024. All rights reserved.