使用鉴别器编码 ADT case classes,即使按 case class 键入
Encoding ADT case classes with a discriminator, even when typed as the case class
假设我在 Scala 中有一个 ADT:
sealed trait Base
case class Foo(i: Int) extends Base
case class Baz(x: String) extends Base
我想将这种类型的值编码成如下所示的 JSON:
{ "Foo": { "i": 10000 }}
{ "Baz": { "x": "abc" }}
幸运的是,这正是 circe 的泛型推导提供的编码!
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"}}
问题是 circe 使用的编码器取决于我们正在编码的表达式的静态类型。这意味着如果我们尝试直接解码 类 中的一种情况,我们将丢失鉴别器:
scala> Foo(10000).asJson.noSpaces
res2: String = {"i":10000}
scala> Baz("abc").asJson.noSpaces
res3: String = {"x":"abc"}
…但我想要 Base
编码,即使静态类型是 Foo
。我知道我可以为所有情况定义显式实例 类,但在某些情况下我可能有很多实例,我不想枚举它们。
(请注意,这是一个出现过几次的问题—e.g. here。)
可以通过为仅委托给 Base
解码器的基类型的子类型定义一个实例来相当直接地做到这一点:
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
}
它按预期工作:
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}}
不幸的是 encodeBaseSubtype
不能在 Base
伴随对象中定义,从那时起它会被 deriveEncoder
宏拾取,导致循环定义(和堆栈溢出等)。我想我在某个时候想出了一个可怕的解决方法来解决这个问题——我会试着找到它,如果我找到了,它会作为另一个答案。
假设我在 Scala 中有一个 ADT:
sealed trait Base
case class Foo(i: Int) extends Base
case class Baz(x: String) extends Base
我想将这种类型的值编码成如下所示的 JSON:
{ "Foo": { "i": 10000 }}
{ "Baz": { "x": "abc" }}
幸运的是,这正是 circe 的泛型推导提供的编码!
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"}}
问题是 circe 使用的编码器取决于我们正在编码的表达式的静态类型。这意味着如果我们尝试直接解码 类 中的一种情况,我们将丢失鉴别器:
scala> Foo(10000).asJson.noSpaces
res2: String = {"i":10000}
scala> Baz("abc").asJson.noSpaces
res3: String = {"x":"abc"}
…但我想要 Base
编码,即使静态类型是 Foo
。我知道我可以为所有情况定义显式实例 类,但在某些情况下我可能有很多实例,我不想枚举它们。
(请注意,这是一个出现过几次的问题—e.g. here。)
可以通过为仅委托给 Base
解码器的基类型的子类型定义一个实例来相当直接地做到这一点:
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
}
它按预期工作:
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}}
不幸的是 encodeBaseSubtype
不能在 Base
伴随对象中定义,从那时起它会被 deriveEncoder
宏拾取,导致循环定义(和堆栈溢出等)。我想我在某个时候想出了一个可怕的解决方法来解决这个问题——我会试着找到它,如果我找到了,它会作为另一个答案。