json scala algebraic-data-types circe generic-derivation

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).