如何使用 Circe 半自动地为某种类型的列表派生解码器?

How to derive a decoder semiautomatically for a list of some type with Circe?

我有一个隐含的 class 将服务器的响应解码为 JSON 和后者在正确的情况下 class 以避免重复调用 .as.getOrElse 所有测试:

implicit class RouteTestResultBody(testResult: RouteTestResult) {
  def body: String = bodyOf(testResult)
  def decodedBody[T](implicit d: Decoder[T]): T =
    decode[Json](body)
      .fold(err => throw new Exception(s"Body is not a valid JSON: $body"), identity)
      .as[T]
      .getOrElse(throw new Exception(s"JSON doesn't have the right shape: $body"))
}

当然,这要靠我们通过一个解码器:

import io.circe.generic.semiauto.deriveDecoder

val result: RouteTestResult = ...
result.decodedBody(deriveDecoder[SomeType[AnotherType])

大部分时间都有效,但当响应是列表时失败:

result.dedoceBody(deriveDecoder[List[SomeType]])
// throws "JSON doesn't have the right shape"

如何半自动地为内部具有特定类型的列表派生解码器?

不幸的是,这里的术语太多了,因为我们在两种意义上使用 "deriving":

  • 提供实例List[A] 给出了 A.
  • 的实例
  • 为案例 class 或密封特征层次结构提供实例,为所有成员类型提供实例。

这个问题不是 Circe 或 Scala 特有的。在撰写有关 Circe 的文章时,我通​​常尽量避免将第一种实例生成称为 "derivation",而将第二种实例称为 "generic derivation" 以强调我们是通过泛型生成实例的代数数据类型的表示。

我们有时使用同一个词来指代两种类型 class 实例生成的事实是一个问题,因为它们在 Scala 中通常是非常不同的机制。在 Circe 中,为 List[A] 提供编码器或解码器实例的东西是 A 类型中的一种方法。比如circe-core中的object Decoder我们有这样一个方法:

implicit def decodeList[A](implicit decodeA: Decoder[A]): Decoder[List[A]] = ...

因为此方法定义在 Decoder 伴随对象中,如果您在具有隐式 Decoder[A] 的上下文中请求隐式 Decoder[List[A]],编译器将找到并使用 decodeList。您不需要任何导入或额外的定义。例如:

scala> case class Foo(i: Int)
class Foo

scala> import io.circe.Decoder, io.circe.parser
import io.circe.Decoder
import io.circe.parser

scala> implicit val decodeFoo: Decoder[Foo] = Decoder[Int].map(Foo(_))
val decodeFoo: io.circe.Decoder[Foo] = io.circe.Decoder$$anon@6e992c05

scala> parser.decode[List[Foo]]("[1, 2, 3]")
val res0: Either[io.circe.Error,List[Foo]] = Right(List(Foo(1), Foo(2), Foo(3)))

如果我们在这里去除隐含的机制,它看起来像这样:

scala> parser.decode[List[Foo]]("[1, 2, 3]")(Decoder.decodeList(decodeFoo))
val res1: Either[io.circe.Error,List[Foo]] = Right(List(Foo(1), Foo(2), Foo(3)))

请注意,我们可以将第一种推导替换为第二种,它仍然可以编译:

scala> import io.circe.generic.semiauto.deriveDecoder
import io.circe.generic.semiauto.deriveDecoder

scala> parser.decode[List[Foo]]("[1, 2, 3]")(deriveDecoder[List[Foo]])
val res2: Either[io.circe.Error,List[Foo]] = Left(DecodingFailure(CNil, List()))

这可以编译,因为 Scala 的 List 是一种代数数据类型,它具有通用表示,circe-generic 可以为其创建实例。但是,此输入的解码失败,因为此表示不会产生我们期望的编码。我们可以推导出对应的编码器,看看这个编码长什么样子:

scala> import io.circe.Encoder, io.circe.generic.semiauto.deriveEncoder
import io.circe.Encoder
import io.circe.generic.semiauto.deriveEncoder

scala> implicit val encodeFoo: Encoder[Foo] = Encoder[Int].contramap(_.i)
val encodeFoo: io.circe.Encoder[Foo] = io.circe.Encoder$$anon@2717857a

scala> deriveEncoder[List[Foo]].apply(List(Foo(1), Foo(2)))
val res3: io.circe.Json =
{
  "::" : [
    1,
    2
  ]
}

所以我们实际上看到 List:: 案例 class,这基本上不是我们想要的。

如果您需要显式提供 Decoder[List[Foo]],解决方案是使用 Decoder.apply "summoner" 方法,或者显式调用 Decoder.decodeList

scala> Decoder[List[Foo]]
val res4: io.circe.Decoder[List[Foo]] = io.circe.Decoder$$anon@5d40f590

scala> Decoder.decodeList[Foo]
val res5: io.circe.Decoder[List[Foo]] = io.circe.Decoder$$anon@2f936a01

scala> Decoder.decodeList(decodeFoo)
val res6: io.circe.Decoder[List[Foo]] = io.circe.Decoder$$anon@7f525e05

这些都提供完全相同的实例,您应该选择哪个是品味问题。


作为脚注,我考虑过 circe-generic 中的特殊外壳 List,因此 deriveDecoder[List[X]] 无法编译,因为它几乎从来不是您想要的(但看起来像可能是,尤其是因为我们谈论实例派生的方式令人困惑)。我通常不喜欢这样的特殊情况,但我认为在这种情况下这可能是正确的做法,因为这个问题经常出现。