Enumerating the ADT constructors
The most straightforward way to get the representation you want is to use generic derivation for the case classes but explicitly defined instances for the ADT type:
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 _)
}
Note that we have to call widen
(which is provided by Cats's Functor
syntax, which we bring into scope with the first import) on the decoders because the Decoder
type class is not covariant. The invariance of circe's type classes is a matter of some controversy (Argonaut for example has gone from invariant to covariant and back), but it has enough benefits that it's unlikely to change, which means we need workarounds like this occasionally.
It's also worth noting that our explicit Encoder
and Decoder
instances will take precedence over the generically-derived instances we'd otherwise get from the io.circe.generic.auto._
import (see my slides here for some discussion of how this prioritization works).
We can use these instances like this:
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构造函数的try 顺序,这是目前最好的解决方案.尽管如此,像这样枚举构造函数显然并不理想,即使我们免费获得case类实例.
A more generic solution
As I note on Gitter, we can avoid the fuss of writing out all the cases by using the circe-shapes module:
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}
这将适用于范围内encodeAdtNoDiscr
和decodeAdtNoDiscr
范围内的任何ADT.如果我们希望它受到更多的限制,我们可以在那些定义中用我们的ADT类型替换泛型A
,或者我们可以将定义设为非隐式,并显式地为我们希望以这种方式编码的ADT定义隐式实例.
这种方法的主要缺点(除了额外的Circe-Shape依赖关系之外)是构造函数将按字母顺序进行try ,如果我们有不明确的case类(其中成员名称和类型相同),这可能不是我们想要的.
The future
The generic-extras module provides a little more configurability in this respect. We can write the following, for example:
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中的包装器对象,我们有一个额外的字段来指示构造函数.这不是默认行为,因为它有一些奇怪的角例(例如,如果我们的某个Case类有一个名为what_am_i
的成员),但在许多情况下它是合理的,而且自从引入该模块以来,它在泛型附加中得到了支持.
这仍然没有得到我们想要的东西,但比默认行为更接近.我还在考虑将withDiscriminator
更改为Option[String]
,而不是String
,其中None
表示我们不需要额外的字段来指示构造函数,从而使我们的行为与上一节中的Circe-Shape实例相同.
如果你有兴趣看到这一切的发生,请打开an issue,或者(更好的)pull request.:)