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

Author: Travis Brown, 2017-02-10

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

 31
Author: Travis Brown,
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