用 Circe 改造 JSON

Transforming JSON with Circe

假设我有一些这样的 JSON 数据:

{
    "data": {
        "title": "example input",
        "someBoolean": false,
        "innerData":  {
            "innerString": "input inner string",
            "innerBoolean": true,
            "innerCollection": [1,2,3,4,5]
        },
        "collection": [6,7,8,9,0]
    }
}

我想将它展平一点并转换或删除一些字段,以获得以下结果:

{
    "data": {
        "ttl": "example input",
        "bool": false,
        "collection": [6,7,8,9,0],
        "innerCollection": [1,2,3,4,5]
    }
}

如何使用 Circe 执行此操作?

(请注意,我将此作为常见问题解答提问,因为昨天 Circe Gitter channel. This specific example is from a question 中经常出现类似的问题。)

我有时会说 Circe 主要是一个用于编码和解码 JSON 的库,而不是用于转换 JSON 值的库,通常我建议映射到 Scala 类型然后定义它们之间的关系(正如 Andriy Plokhotnyuk 所建议的 here),但在许多情况下,使用游标编写转换工作得很好,在我看来,这种事情就是其中之一。

以下是我将如何实施此转换:

import io.circe.{DecodingFailure, Json, JsonObject}
import io.circe.syntax._

def transform(in: Json): Either[DecodingFailure, Json] = {
  val someBoolean = in.hcursor.downField("data").downField("someBoolean")
  val innerData = someBoolean.delete.downField("innerData")

  for {
    boolean    <- someBoolean.as[Json]
    collection <- innerData.get[Json]("innerCollection")
    obj        <- innerData.delete.up.as[JsonObject]
  } yield Json.fromJsonObject(
    obj.add("boolean", boolean).add("collection", collection)
  )
}

然后:

val Right(json) = io.circe.jawn.parse(
  """{
    "data": {
      "title": "example input",
      "someBoolean": false,
      "innerData":  {
        "innerString": "input inner string",
        "innerBoolean": true,
        "innerCollection": [1,2,3]
      },
      "collection": [6,7,8]
    }
  }"""
)

并且:

scala> transform(json)
res1: Either[io.circe.DecodingFailure,io.circe.Json] =
Right({
  "data" : {
    "title" : "example input",
    "collection" : [
      6,
      7,
      8
    ]
  },
  "boolean" : false,
  "collection" : [
    1,
    2,
    3
  ]
})

如果你以正确的方式看待它,我们的 transform 方法有点像解码器,我们实际上可以将它写成一个解码器(尽管我绝对建议不要将其隐式化):

import io.circe.{Decoder, Json, JsonObject}
import io.circe.syntax._

val transformData: Decoder[Json] = { c =>
  val someBoolean = c.downField("data").downField("someBoolean")
  val innerData = someBoolean.delete.downField("innerData")

  (
    innerData.delete.up.as[JsonObject],
    someBoolean.as[Json],
    innerData.get[Json]("innerCollection")
  ).mapN(_.add("boolean", _).add("collection", _)).map(Json.fromJsonObject)
}

在某些情况下,如果您想将转换作为需要解码器的管道的一部分来执行,这会很方便:

scala> io.circe.jawn.decode(myJsonString)(transformData)
res2: Either[io.circe.Error,io.circe.Json] =
Right({
  "data" : {
    "title" : "example input",
    "collection" : [ ...

不过,这也可能令人困惑,我考虑过向 Circe 添加某种 Transformation 类型,它可以封装这样的转换,而不会怀疑地重新利用 Decoder 类型 class.

transform 方法和此解码器的一个好处是,如果输入数据不具有预期的形状,则产生的错误将包含指向问题的历史记录。