web-dev-qa-db-fra.com

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

Supposons que j'ai un ADT comme celui-ci:

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 dérivation générique par défaut pour un Decoder[Event] instance in circe 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 déballé serait sans ambiguïté, ou nous voulons lever l'ambiguïté en spécifiant l'ordre chaque classe de cas doit être essayée, ou nous ne nous en soucions pas.

Comment puis-je encoder et décoder mon Event ADT sans le wrapper (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.)

26
Travis Brown

Énumération des constructeurs ADT

La façon la plus simple d'obtenir la représentation souhaitée est d'utiliser une dérivation générique pour les classes de cas mais des 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 la syntaxe Functor de Cats, que nous introduisons dans la portée avec la première importation) sur les décodeurs car la classe de type Decoder est pas covariant. L'invariance des classes de types de circe est une question de ne certaine controverse (Argonaut par exemple est passé d'invariant à covariant et inversement), mais il a suffisamment d'avantages qu'il est peu probable qu'il change, ce qui signifie que nous avons besoin de solutions de contournement comme cela de temps en temps.

Il convient également de noter que nos instances explicites Encoder et Decoder auront priorité sur les instances dérivées de manière générique que nous obtiendrions autrement du io.circe.generic.auto._ import (voir mes diapositives ici pour une discussion sur le fonctionnement de cette priorisation).

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 devez pouvoir spécifier l'ordre dans lequel les constructeurs ADT sont essayés, c'est actuellement la meilleure solution. Le fait d'énumérer les constructeurs comme celui-ci n'est évidemment pas idéal, même si nous obtenons gratuitement les instances de classe case.

Une solution plus générique

Comme je le note sur Gitter , nous pouvons éviter les tracas d'écrire tous les cas en utilisant le module circe-formes:

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 n'importe quel ADT partout où encodeAdtNoDiscr et decodeAdtNoDiscr sont dans la portée. Si nous voulions qu'il soit plus limité, nous pourrions remplacer le générique A par nos types ADT dans ces définitions, ou nous pourrions rendre les définitions non implicites et définir explicitement des instances implicites pour les ADT que nous voulons coder de cette façon. .

Le principal inconvénient de cette approche (en dehors de la dépendance des formes de cirque supplémentaires) est que les constructeurs seront essayés par 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 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 a des cas d'angle étranges (par exemple, si l'une de nos classes de cas avait un membre nommé what_am_i), mais dans de nombreux cas, c'est raisonnable et il est pris en charge dans les extras génériques depuis l'introduction de ce module.

Cela ne nous donne toujours pas exactement ce que nous voulons, mais c'est plus proche que le comportement par défaut. J'ai également envisagé de modifier withDiscriminator pour prendre un Option[String] au lieu d'un String, avec None indiquant que nous ne voulons pas d'un champ supplémentaire indiquant le constructeur, nous donnant le même comportement que nos instances de forme circulaire dans la section précédente.

Si cela vous intéresse, veuillez ouvrir n problème , ou (mieux encore) un pull request . :)

38
Travis Brown

Je dois gérer beaucoup d'ADT vers JSON récemment, alors décidez de maintenir ma propre bibliothèque d'extensions, qui fournit un moyen un peu différent de le résoudre à l'aide d'annotations et d'une macro:

Définitions ADT:

import org.latestbit.circe.adt.codec._


sealed trait TestEvent

@JsonAdt("my-event-1") 
case class MyEvent1(anyYourField : String /*, ...*/) extends TestEvent

@JsonAdt("my-event-2")
case class MyEvent2(anyOtherField : Long /*, ...*/) extends TestEvent


Usage:


import io.circe._
import io.circe.parser._
import io.circe.syntax._

// This example uses auto coding for case classes. 
// You decide here if you need auto/semi/custom coders for your case classes.
import io.circe.generic.auto._ 

// One import for this ADT/JSON codec
import org.latestbit.circe.adt.codec._

// Encoding

implicit val encoder : Encoder[TestEvent] = 
  JsonTaggedAdtCodec.createEncoder[TestEvent]("type")

val testEvent : TestEvent = TestEvent1("test")
val testJsonString : String = testEvent.asJson.dropNullValues.noSpaces

// Decoding
implicit val decoder : Decoder[TestEvent] = 
  JsonTaggedAdtCodec.createDecoder[TestEvent] ("type")

decode[TestEvent] (testJsonString) match {
   case Right(model : TestEvent) => // ...
}

Détails: https://github.com/abdolence/circe-tagged-adt-codec

0
abdolence