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