【问题标题】:How to derive a decoder semiautomatically for a list of some type with Circe?如何使用 Circe 为某种类型的列表半自动派生解码器?
【发布时间】:2020-04-30 13:26:32
【问题描述】:

我有一个隐式类,它将服务器的响应解码为 JSON,然后在正确的案例类中解码,以避免在测试过程中重复调用 .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"

如何半自动地为包含特定类型的列表派生解码器?

【问题讨论】:

    标签: scala circe


    【解决方案1】:

    不幸的是,这里的术语被过度使用了,因为我们在两种意义上使用“派生”:

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

    这个问题并不特定于 Circe,甚至 Scala。在写关于 Circe 的文章时,我通​​常尽量避免将第一种实例生成称为“派生”,而将第二种称为“泛型派生”以强调我们是通过对代数数据类型。

    我们有时使用同一个词来指代这两种类型类实例生成这一事实是一个问题,因为它们在 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$1@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$1@2717857a
    
    scala> deriveEncoder[List[Foo]].apply(List(Foo(1), Foo(2)))
    val res3: io.circe.Json =
    {
      "::" : [
        1,
        2
      ]
    }
    

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

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

    scala> Decoder[List[Foo]]
    val res4: io.circe.Decoder[List[Foo]] = io.circe.Decoder$$anon$44@5d40f590
    
    scala> Decoder.decodeList[Foo]
    val res5: io.circe.Decoder[List[Foo]] = io.circe.Decoder$$anon$44@2f936a01
    
    scala> Decoder.decodeList(decodeFoo)
    val res6: io.circe.Decoder[List[Foo]] = io.circe.Decoder$$anon$44@7f525e05
    

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


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

    【讨论】:

      猜你喜欢
      • 2020-05-16
      • 2018-09-20
      • 2017-11-27
      • 2010-10-29
      • 2021-11-11
      • 2020-09-26
      • 2016-08-19
      • 1970-01-01
      • 1970-01-01
      相关资源
      最近更新 更多