为了通过circe将JSON节点转换为除JSON之外的其他格式(例如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 ,那么这一切都很好,这仅使该代码在放入时才能起作用包package io.circe
。
如何使用public circe API达到与上述相同的结果?
答案 0 :(得分: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继承的,并且可以说是不准确的。当我们谈论递归代数数据类型的变形(或折叠)时,我们指的是一个函数,在传入的函数的参数中看不到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值执行的实际操作很便宜,则可以预期{{1} }版本获得约Folder
两倍的吞吐量。顺便说一句,它也比内部构造函数上的模式匹配要快得多,至少在benchmarks we've done:
fold
那是2.12。我相信您应该会在2.11上看到更多的不同。
如果您真的想要模式匹配,circe-optics为案例类提取器提供了强大的替代方案:
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
这与原始版本的代码几乎完全相同,但是每个提取器都是Monocle Prism ,可以与the Monocle library中的其他光学元件组成。
(这种方法的缺点是您会丢失穷举性检查,但不幸的是这无济于事。)
当我第一次开始从事circe时,我在a document about some of my design decisions中写了以下内容:
在某些情况下,包括
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("") }
类型,我们不想鼓励用户将ADT叶子视为 具有有意义的类型。 JSON值“是”布尔值或字符串或 单位或io.circe.Json
或Seq[Json]
或JsonNumber
。简介JsonObject
,JString
等类型添加到公共API中 令人困惑。
我想要一个真正最小的API(尤其是避免暴露那些没有意义的类型的API),并且我想要腾出空间来优化JSON表示形式。 (我也完全不希望人们使用JSON AST,但这是一场失败的战斗。)我仍然认为隐藏构造函数是正确的决定,即使我没有真正利用过他们还没有优化(尽管如此),尽管这个问题很多。
答案 1 :(得分: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)
}