Suppose I have a ADT in Scala:
sealed trait Base
case class Foo(i: Int) extends Base
case class Baz(x: String) extends Base
I want to encode values of this type into the JSON that looks like the following:
{ "Foo": { "i": 10000 }}
{ "Baz": { "x": "abc" }}
Which luckily is exactly the encoding circe's generic derivation provides!
scala> import io.circe.generic.auto._, io.circe.syntax._
import io.circe.generic.auto._
import io.circe.syntax._
scala> val foo: Base = Foo(10000)
foo: Base = Foo(10000)
scala> val baz: Base = Baz("abc")
baz: Base = Baz(abc)
scala> foo.asJson.noSpaces
res0: String = {"Foo":{"i":10000}}
scala> baz.asJson.noSpaces
res1: String = {"Baz":{"x":"abc"}}
The problem is that the encoder circe uses depends on the static type of the expression we're encoding. This means that if we try to decode one of the case classes directly, we lose the discriminator:
scala> Foo(10000).asJson.noSpaces
res2: String = {"i":10000}
scala> Baz("abc").asJson.noSpaces
res3: String = {"x":"abc"}
…but I want the Base
encoding even when the static type is Foo
. I know I can define explicit instances for all of the case classes, but in some cases I might have a lot of them, and I don't want to have to enumerate them.
(Note that this is a question that's come up a few times—e.g. here.)
It is possible to do this fairly straightforwardly by defining an instance for subtypes of the base type that just delegates to the Base
decoder:
import cats.syntax.contravariant._
import io.circe.ObjectEncoder, io.circe.generic.semiauto.deriveEncoder
sealed trait Base
case class Foo(i: Int) extends Base
case class Baz(x: String) extends Base
object Base {
implicit val encodeBase: ObjectEncoder[Base] = deriveEncoder
}
object BaseEncoders {
implicit def encodeBaseSubtype[A <: Base]: ObjectEncoder[A] = Base.encodeBase.narrow
}
It works as expected:
scala> import BaseEncoders._
import BaseEncoders._
scala> import io.circe.syntax._
import io.circe.syntax._
scala> Foo(10000).asJson.noSpaces
res0: String = {"Foo":{"i":10000}}
scala> (Foo(10000): Base).asJson.noSpaces
res1: String = {"Foo":{"i":10000}}
Unfortunately encodeBaseSubtype
can't be defined in the Base
companion object, since then it'd be picked up by the deriveEncoder
macro, resulting in a cyclic definition (and stack overflows, etc.). I think I came up with a kind of horrible workaround for this problem at some point—I'll try to find it and post it as another answer if I do.