json - Cómo descodificar un ADT con circe sin desambiguar objetos
scala algebraic-data-types (1)
Enumerar los constructores de ADT
La forma más sencilla de obtener la representación que desea es usar la derivación genérica para las clases de casos pero instancias explícitamente definidas para el tipo 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 _)
}
Tenga en cuenta que debemos llamar a widen
(que se proporciona mediante la sintaxis del Functor
de Cats, que llevamos al alcance con la primera importación) en los decodificadores porque la clase de tipo Decoder
no es covariante. La invariancia de las clases de tipos de circe es una cuestión de controversia (Argonaut, por ejemplo, ha pasado de ser invariable a covariante y viceversa), pero tiene suficientes beneficios que es poco probable que cambie, lo que significa que necesitamos soluciones como esta ocasionalmente.
También vale la pena señalar que nuestras instancias explícitas de Encoder
y Decoder
tendrán prioridad sobre las instancias genéricamente derivadas que de otro modo obtendríamos de la importación io.circe.generic.auto._
(vea mis diapositivas here para obtener información sobre cómo funciona esta priorización ).
Podemos usar estos casos así:
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}
Esto funciona, y si necesita poder especificar el orden en que se prueban los constructores de ADT, actualmente es la mejor solución. Sin embargo, tener que enumerar a los constructores como este no es ideal, aunque obtengamos las instancias de clase de caso de forma gratuita.
Una solución más genérica.
Como observo en Gitter , podemos evitar la molestia de escribir todos los casos utilizando el módulo de circe-formas:
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
Y entonces:
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}
Esto funcionará para cualquier ADT en cualquier lugar que encodeAdtNoDiscr
y decodeAdtNoDiscr
estén dentro del alcance. Si quisiéramos que fuera más limitado, podríamos reemplazar la A
genérica con nuestros tipos de ADT en esas definiciones, o podríamos hacer las definiciones no implícitas y definir instancias implícitas explícitamente para las ADT que queremos codificar de esta manera.
El principal inconveniente de este enfoque (aparte de la dependencia adicional de formas circulares) es que los constructores se probarán en orden alfabético, lo que puede no ser lo que queremos si tenemos clases de casos ambiguas (donde los nombres y tipos de miembros son los mismos ).
El futuro
El módulo genérico-extras proporciona un poco más de configurabilidad a este respecto. Podemos escribir lo siguiente, por ejemplo:
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
Y entonces:
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))
En lugar de un objeto contenedor en el JSON tenemos un campo adicional que indica el constructor. Este no es el comportamiento predeterminado, ya que tiene algunos casos extraños (por ejemplo, si una de nuestras clases de casos tenía un miembro llamado what_am_i
), pero en muchos casos es razonable y se ha admitido en genéricos-extras desde que se introdujo ese módulo.
Esto todavía no nos da exactamente lo que queremos, pero está más cerca que el comportamiento predeterminado. También he estado considerando cambiar con withDiscriminator
para tomar una Option[String]
lugar de una String
, y None
indica que no queremos que un campo adicional indique el constructor, lo que nos da el mismo comportamiento que nuestras instancias de formas withDiscriminator
en la withDiscriminator
anterior. sección.
Si está interesado en que esto suceda, abra un problema o (incluso mejor) una solicitud de extracción . :)
Supongamos que tengo un ADT como este:
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
La derivación genérica predeterminada para una instancia de Decoder[Event]
en circe espera que el JSON de entrada incluya un objeto de envoltura que indique qué clase de caso está representada:
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}}
Este comportamiento significa que nunca debemos preocuparnos por las ambigüedades si dos o más clases de casos tienen los mismos nombres de miembros, pero no siempre es lo que queremos, a veces sabemos que la codificación desenvuelta no sería ambigua, o queremos desambiguar especificando el orden Cada clase de caso debe ser juzgada, o simplemente no nos importa.
¿Cómo puedo codificar y decodificar mi ADT de Event
sin la envoltura (preferiblemente sin tener que escribir mis codificadores y decodificadores desde cero)?
(Esta pregunta surge con bastante frecuencia; consulte, por ejemplo, esta discusión con Igor Mazor en Gitter esta mañana).