使用 circe 解码 JSON 对象时捕获未使用的字段

Capturing unused fields while decoding a JSON object with circe

假设我有一个案例 class 如下所示,我想将一个 JSON 对象解码到其中,所有未使用的字段都以特殊形式结束剩菜会员:

import io.circe.Json

case class Foo(a: Int, b: String, leftovers: Json)

在 Scala 中使用 circe 执行此操作的最佳方法是什么?

(注意:我见过这样的问题a few times,所以我为后人问答。)

您可以通过多种方式解决此问题。一种相当直接的方法是在解码后过滤掉您使用的密钥:

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

implicit val decodeFoo: Decoder[Foo] =
  Decoder.forProduct2[Int, String, (Int, String)]("a", "b")((_, _)).product(
    Decoder[JsonObject]
  ).map {
    case ((a, b), all) =>
      Foo(a, b, Json.fromJsonObject(all.remove("a").remove("b")))
  }

如您所愿:

scala> val doc = """{ "something": false, "a": 1, "b": "abc", "0": 0 }"""
doc: String = { "something": false, "a": 1, "b": "abc", "0": 0 }

scala> io.circe.jawn.decode[Foo](doc)
res0: Either[io.circe.Error,Foo] =
Right(Foo(1,abc,{
  "something" : false,
  "0" : 0
}))

这种方法的缺点是您必须维护代码以从它们的使用中单独删除您使用过的密钥,这很容易出错。另一种方法是使用 circe 的 state-monad-powered 解码工具:

import cats.data.StateT
import cats.instances.either._
import io.circe.{ ACursor, Decoder, Json }

implicit val decodeFoo: Decoder[Foo] = Decoder.fromState(
  for {
    a <- Decoder.state.decodeField[Int]("a")
    b <- Decoder.state.decodeField[String]("b")
    rest <- StateT.inspectF((_: ACursor).as[Json])
  } yield Foo(a, b, rest)
)

它的工作方式与之前的解码器相同(除了在解码失败时您会得到的错误有一些小差异):

scala> io.circe.jawn.decode[Foo](doc)
res1: Either[io.circe.Error,Foo] =
Right(Foo(1,abc,{
  "something" : false,
  "0" : 0
}))

后一种方法不需要您在多个地方更改使用的字段,而且它还有一个优点,即看起来更像您在 circe 中手动编写的任何其他解码器。