假设我有一个这样的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包含一个包装对象,该包装对象指示要表示的案例类:
Decoder[Event]
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进行编码和解码?
Event
(这个问题经常出现,例如,今天早上在Gitter上与Igor Mazor进行的讨论。)
获得所需表示的最直接方法是对案例类使用通用派生,但对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已从不变性变为协变并返回),但是它具有足够的好处,因此不太可能更改,这意味着我们有时需要这种解决方法。
widen
Functor
Decoder
另外值得一提的是我们的明确Encoder和Decoder实例将优先于一般衍生的情况下,我们会以其他方式从一开始io.circe.generic.auto._进口(见我的幻灯片在这里为如何优先工作的一些讨论)。
Encoder
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的隐式实例。
encodeAdtNoDiscr
decodeAdtNoDiscr
A
这种方法的主要缺点(除了额外的圆形依赖)是,构造函数将按字母顺序尝试,如果我们的模版类模棱两可(成员名称和类型相同,则可能不是我们想要的) )。
通用扩展模块在这方面提供了更多的可配置性。例如,我们可以编写以下代码:
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),但是在许多情况下,这是合理的,并且自引入该模块以来,它在通用扩展中得到了支持。
what_am_i
这仍然不能完全满足我们的需求,但是比默认行为要近。我还一直在考虑withDiscriminator采用a Option[String]代替a String,以None表明我们不需要一个额外的字段来指示构造函数,从而为我们提供了与上一节中的圆形实例相同的行为。
withDiscriminator
Option[String]
String
None
如果您有兴趣看到这种情况,请提出问题,或者(甚至更好)提出请求。:)