一尘不染

如何在不区分对象的情况下带圆圈解码ADT

json

假设我有一个这样的ADT:

sealed trait Event

case class Foo(i: Int) extends Event
case class Bar(s: String) extends Event
case class Baz(c: Char) extends Event
case class Qux(values: List[String]) extends Event

circe中Decoder[Event]实例的默认通用派生期望输入JSON包含一个包装对象,该包装对象指示要表示的案例类:

scala> import io.circe.generic.auto._, io.circe.parser.decode, io.circe.syntax._
import io.circe.generic.auto._
import io.circe.parser.decode
import io.circe.syntax._

scala> decode[Event]("""{ "i": 1000 }""")
res0: Either[io.circe.Error,Event] = Left(DecodingFailure(CNil, List()))

scala> decode[Event]("""{ "Foo": { "i": 1000 }}""")
res1: Either[io.circe.Error,Event] = Right(Foo(1000))

scala> (Foo(100): Event).asJson.noSpaces
res2: String = {"Foo":{"i":100}}

这种行为意味着,如果两个或多个案例类具有相同的成员名称,我们就不必担心歧义,但这并不总是我们想要的—有时我们知道展开的编码将是明确的,或者我们想通过指定顺序来消除歧义每个案例类都应该尝试,否则我们不在乎。

如何在Event没有包装的情况下(最好不必从头开始编写编码器和解码器)对ADT进行编码和解码?

(这个问题经常出现,例如,今天早上在Gitter上与Igor
Mazor
进行的讨论。)


阅读 177

收藏
2020-07-27

共1个答案

一尘不染

枚举ADT构造函数

获得所需表示的最直接方法是对案例类使用通用派生,但对ADT类型使用明确定义的实例:

import cats.syntax.functor._
import io.circe.{ Decoder, Encoder }, io.circe.generic.auto._
import io.circe.syntax._

sealed trait Event

case class Foo(i: Int) extends Event
case class Bar(s: String) extends Event
case class Baz(c: Char) extends Event
case class Qux(values: List[String]) extends Event

object Event {
  implicit val encodeEvent: Encoder[Event] = Encoder.instance {
    case foo @ Foo(_) => foo.asJson
    case bar @ Bar(_) => bar.asJson
    case baz @ Baz(_) => baz.asJson
    case qux @ Qux(_) => qux.asJson
  }

  implicit val decodeEvent: Decoder[Event] =
    List[Decoder[Event]](
      Decoder[Foo].widen,
      Decoder[Bar].widen,
      Decoder[Baz].widen,
      Decoder[Qux].widen
    ).reduceLeft(_ or _)
}

请注意,由于类型类不是协变的,因此必须在解码器上调用widen(由Cats的Functor语法提供,这在第一次导入时就纳入了范围)Decoder。circe类型类的不变性是一个有争议的问题(例如,Argonaut已从不变性变为协变并返回),但是它具有足够的好处,因此不太可能更改,这意味着我们有时需要这种解决方法。

另外值得一提的是我们的明确EncoderDecoder实例将优先于一般衍生的情况下,我们会以其他方式从一开始io.circe.generic.auto._进口(见我的幻灯片在这里为如何优先工作的一些讨论)。

我们可以像这样使用这些实例:

scala> import io.circe.parser.decode
import io.circe.parser.decode

scala> decode[Event]("""{ "i": 1000 }""")
res0: Either[io.circe.Error,Event] = Right(Foo(1000))

scala> (Foo(100): Event).asJson.noSpaces
res1: String = {"i":100}

这是可行的,并且如果您需要能够指定尝试ADT构造函数的顺序,则它是当前最佳的解决方案。即使我们免费获得case类实例,必须枚举这样的构造函数显然也不理想。

更通用的解决方案

正如我在Gitter上所指出的,我们可以通过使用circe-
shapes模块来避免写出所有情况的麻烦:

import io.circe.{ Decoder, Encoder }, io.circe.generic.auto._
import io.circe.shapes
import shapeless.{ Coproduct, Generic }

implicit def encodeAdtNoDiscr[A, Repr <: Coproduct](implicit
  gen: Generic.Aux[A, Repr],
  encodeRepr: Encoder[Repr]
): Encoder[A] = encodeRepr.contramap(gen.to)

implicit def decodeAdtNoDiscr[A, Repr <: Coproduct](implicit
  gen: Generic.Aux[A, Repr],
  decodeRepr: Decoder[Repr]
): Decoder[A] = decodeRepr.map(gen.from)

sealed trait Event

case class Foo(i: Int) extends Event
case class Bar(s: String) extends Event
case class Baz(c: Char) extends Event
case class Qux(values: List[String]) extends Event

然后:

scala> import io.circe.parser.decode, io.circe.syntax._
import io.circe.parser.decode
import io.circe.syntax._

scala> decode[Event]("""{ "i": 1000 }""")
res0: Either[io.circe.Error,Event] = Right(Foo(1000))

scala> (Foo(100): Event).asJson.noSpaces
res1: String = {"i":100}

这对于任何ADT任何地方工作,encodeAdtNoDiscr并且decodeAdtNoDiscr都在范围之内。如果我们希望它受到更大的限制,则可以A在这些定义中用ADT类型替换泛型,或者可以使定义成为非隐式的,并明确定义要以这种方式编码的ADT的隐式实例。

这种方法的主要缺点(除了额外的圆形依赖)是,构造函数将按字母顺序尝试,如果我们的模版类模棱两可(成员名称和类型相同,则可能不是我们想要的) )。

未来

通用扩展模块在这方面提供了更多的可配置性。例如,我们可以编写以下代码:

import io.circe.generic.extras.auto._
import io.circe.generic.extras.Configuration

implicit val genDevConfig: Configuration =
  Configuration.default.withDiscriminator("what_am_i")

sealed trait Event

case class Foo(i: Int) extends Event
case class Bar(s: String) extends Event
case class Baz(c: Char) extends Event
case class Qux(values: List[String]) extends Event

然后:

scala> import io.circe.parser.decode, io.circe.syntax._
import io.circe.parser.decode
import io.circe.syntax._

scala> (Foo(100): Event).asJson.noSpaces
res0: String = {"i":100,"what_am_i":"Foo"}

scala> decode[Event]("""{ "i": 1000, "what_am_i": "Foo" }""")
res1: Either[io.circe.Error,Event] = Right(Foo(1000))

代替JSON中的包装器对象,我们有一个额外的字段来指示构造函数。这不是默认行为,因为它有一些奇怪的极端情况(例如,如果我们的一个案例类有一个名为的成员what_am_i),但是在许多情况下,这是合理的,并且自引入该模块以来,它在通用扩展中得到了支持。

这仍然不能完全满足我们的需求,但是比默认行为要近。我还一直在考虑withDiscriminator采用a Option[String]代替a
String,以None表明我们不需要一个额外的字段来指示构造函数,从而为我们提供了与上一节中的圆形实例相同的行为。

如果您有兴趣看到这种情况,请提出问题,或者(甚至更好)提出请求。:)

2020-07-27