Comment décoder un ADT avec circe sans objets d'ambiguïté

Supposons que j'ai une ADT comme ceci:

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 valeur par défaut générique de dérivation pour un Decoder[Event] exemple circé s'attend à ce que le JSON d'entrée inclue un objet wrapper qui indique quelle classe de cas est représentée:

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}}

ce comportement signifie que nous n'avons jamais à nous soucier des ambiguïtés si deux ou plusieurs classes de cas ont les mêmes noms de membres, mais ce n'est pas toujours ce que nous voulons-parfois, nous savons que l'encodage libre serait sans ambiguïté, ou nous voulons désambiguer en spécifiant l'ordre dans lequel chaque classe de cas doit être essayée, ou nous nous en fichons.

Comment puis-je encoder et décoder mes Event ADT sans l'emballage (de préférence sans avoir à écrire mes encodeurs et décodeurs à partir de zéro)?

(Cette question revient assez souvent-voir, par exemple, cette discussion avec Igor Mazor sur Gitter ce matin.)

23
demandé sur Travis Brown 2017-02-10 20:38:18

1 réponses

l'Énumération de l'ADT constructeurs

la façon la plus simple d'obtenir la représentation que vous voulez est d'utiliser la dérivation générique pour les classes de cas mais les instances explicitement définies pour le type 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 _)
}

notez que nous devons appeler widen (qui est fourni par Cats Functor syntaxe, que nous apportons dans la portée avec la première importation) sur les décodeurs parce que le Decoder la classe de type n'est pas covariante. L'invariance des classes types de circe est question de une certaine controverse (Argonaut par exemple est passé de invariant à covariant et retour), mais il a assez d'avantages qu'il est peu probable de changer, ce qui signifie que nous avons besoin de solutions de travail comme ceci de temps en temps.

il est également intéressant de noter que notre explicite Encoder et Decoder les instances auront priorité sur les instances dérivées de façon générique que nous obtiendrions autrement de io.circe.generic.auto._ importer (voir mes photos ici pour une discussion sur la façon dont ceci la priorisation des travaux).

Nous pouvons utiliser ces instances comme ceci:

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}

cela fonctionne, et si vous avez besoin de pouvoir spécifier l'ordre dans lequel les constructeurs ADT sont essayés, c'est actuellement la meilleure solution. Avoir à énumérer les constructeurs comme ceci n'est évidemment pas idéal, bien que, même si nous obtenons les instances de classe de CAs gratuitement.

une solution plus générique

comme je le note sur Gitter, nous pouvons éviter les tracas de l'écriture de tous les cas à l'aide de la circé-formes module:

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

puis:

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}

cela fonctionnera pour tout ADT n'importe où qui encodeAdtNoDiscr et decodeAdtNoDiscr sont dans la portée. Si nous le voulions, nous pourrions remplacer le générique A avec nos types ADT dans ces définitions, ou nous pourrions rendre les définitions non implicites et définir explicitement les instances implicites pour les ADTs que nous voulons encoder de cette façon.

Le principal inconvénient de cette l'approche (en dehors de la dépendance supplémentaire de circe-shapes) est que les constructeurs seront essayés dans l'ordre alphabétique, ce qui peut ne pas être ce que nous voulons si nous avons des classes de cas ambiguës (où les noms et les types de membres sont les mêmes).

L'avenir

le module generic-extras offre un peu plus de configurabilité à cet égard. Nous pouvons écrire ce qui suit, par exemple:

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

puis:

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

au Lieu d'un objet wrapper dans le JSON, nous avons un champ supplémentaire qui indique le constructeur. Ce n'est pas le comportement par défaut car il y a des cas de coin bizarres (par exemple si l'une de nos classes de cas avait un membre nommé what_am_i), mais dans de nombreux cas, il est raisonnable et il a été pris en charge dans generic-extras depuis que ce module a été introduit.

cela ne nous donne toujours pas exactement ce que nous voulons, mais c'est plus proche que le comportement par défaut. J'ai aussi été envisage de modifier withDiscriminator prendre Option[String] au lieu de StringNone indiquant que nous ne voulons pas d'un champ supplémentaire indiquant le constructeur, nous donnant le même comportement que notre circé-formes instances dans la section précédente.

Si vous êtes intéressé à voir cela se produire, s'il vous plaît ouvrir problème, ou (encore mieux) pull request. :)

31
répondu Travis Brown 2017-02-10 17:51:19