Cómo decodificar un ADT con circe sin desambiguar objetos
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 Decoder[Event]
en circe espera que el JSON de entrada incluya un objeto wrapper que indique qué clase case 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 tenemos que preocuparnos por ambigüedades si dos o más clases case tienen los mismos nombres de miembro, pero no siempre es lo que queremos-a veces sabemos que la codificación sin envolver sería inequívoca, o queremos desambiguar por especificando el orden de cada clase de caso debe ser tratado, o simplemente no nos importa.
¿Cómo puedo codificar y decodificar mi Event
ADT sin el wrapper (preferiblemente sin tener que escribir mis codificadores y decodificadores desde cero)?
(Esta pregunta surge con bastante frecuencia-ver por ejemplo esta discusión con Igor Mazor en Gitter esta mañana.)
1 answers
Enumerando los constructores ADT
La forma más sencilla de obtener la representación que desea es usar derivación genérica para las clases case pero instancias definidas explícitamente 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 tenemos que llamar a widen
(que es proporcionado por la sintaxis Functor
de Cats, que traemos al ámbito con la primera importación) en los decodificadores porque la clase de tipo Decoder
no es covariante. La invariancia de las clases tipo de circe es una cuestión de algunos controversy (Argonaut, por ejemplo, ha pasado de invariante 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 Encoder
y Decoder
tendrán prioridad sobre las instancias derivadas genéricamente que de otra manera obtendríamos de la importación io.circe.generic.auto._
(vea mis diapositivas aquí para una discusión de cómo funciona esta priorización).
Podemos usar estas instancias como esta:
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 ADT, actualmente es la mejor solución. Sin embargo, tener que enumerar los constructores como este obviamente no es ideal, incluso si obtenemos las instancias de la clase case de forma gratuita.
Una solución más genérica
Como observo en Gitter , podemos evitar el alboroto de escribir todos los casos utilizando el módulo circe-shapes:
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 en el alcance. Si queremos que sea más limitado, podríamos reemplazar el genérico A
con nuestros tipos ADT en esas definiciones, o podríamos hacer que las definiciones no sean implícitas y definir instancias implícitas explícitamente para los ADT que queremos codificados de esta manera.
El principal inconveniente de este enfoque (aparte de la dependencia extra circe-shapes) es que los constructores se intentarán en orden alfabético, 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 generic-extras proporciona un poco más de configurabilidad en este sentido. 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 luego:
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 wrapper en el JSON tenemos un campo extra que indica el constructor. Este no es el comportamiento predeterminado, ya que tiene algunos casos de esquina 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 ha sido soportado en generic-extras desde que ese módulo fue introducido.
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 withDiscriminator
para tomar un Option[String]
en lugar de un String
, con None
indicando que no queremos un campo adicional que indique el constructor, dándonos el mismo comportamiento que nuestras instancias de circe-shapes en el sección anterior.
Si está interesado en ver que esto suceda, abra un problema, o (incluso mejor) una solicitud de extracción . :)
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/ajaxhispano.com/template/agent.layouts/content.php on line 61
2017-02-10 17:51:19