Подтвердить что ты не робот

Как декодировать ADT с циркой без объектов, несущих неоднозначность

Предположим, что у меня есть ADT:

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

Генерированный по умолчанию вывод для экземпляра Decoder[Event] в circe предполагает, что входной JSON будет включать объект-оболочку, который указывает, какой класс класса представлен:

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

Это поведение означает, что нам не нужно беспокоиться о двусмысленности, если два или более класса классов имеют одинаковые имена членов, но это не всегда то, что мы хотим - иногда мы знаем, что развернутая кодировка будет однозначной, или мы хотим устранить указывая порядок, в котором каждый класс должен быть опробован, или нам просто все равно.

Как я могу кодировать и декодировать мой Event ADT без оболочки (желательно без необходимости писать мои кодеры и декодеры с нуля)?

(Этот вопрос довольно часто встречается - см., например, эту дискуссию с Игорем Мазором о Гиттере сегодня утром.)

4b9b3361

Ответ 1

Перечисление конструкторов ADT

Самый простой способ получить представление, которое вы хотите, - использовать общий вывод для классов case, но явно определенных экземпляров для типа 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 _)
}

Обратите внимание, что мы должны вызывать widen (который предоставляется синтаксисом Cats Functor, который мы вносим в область с первым импортом) на декодерах, потому что класс типа Decoder не является ковариантным. Инвариантность классов типов цирцевых отношений - это вопрос некоторых разногласий (например, аргонавт перешел от инварианта к коварианту и обратно), но он обладает достаточными преимуществами, которые вряд ли могут измениться, что означает, что нам иногда нужны обходные пути.

Также стоит отметить, что наши явные экземпляры Encoder и Decoder будут иметь приоритет над генерируемыми экземплярами, которые мы могли бы получить из импорта io.circe.generic.auto._ (см. мои слайды здесь для обсуждения того, как эта приоритизация работает).

Мы можем использовать эти экземпляры следующим образом:

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}

Это работает, и если вам нужно указать порядок, с которым проектируются конструкторы ADT, в настоящее время это лучшее решение. Если перечислить такие конструкторы, как это, очевидно, не идеально, хотя даже если мы получим экземпляры класса case бесплатно.

Более общее решение

Как я отмечаю на Gitter, мы можем избежать суеты выписывания всех случаев с помощью модуля 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

И затем:

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}

Это будет работать для любого ADT в любом месте, где encodeAdtNoDiscr и decodeAdtNoDiscr находятся в области видимости. Если бы мы хотели, чтобы он был более ограниченным, мы могли бы заменить общий A нашими типами ADT в этих определениях, или мы могли бы сделать определения неявными и явно определить неявные экземпляры для ADT, которые мы хотим закодировать таким образом.

Основной недостаток этого подхода (помимо зависимости от цирковых фигур) заключается в том, что конструкторы будут проверяться в алфавитном порядке, что может быть не тем, что мы хотим, если у нас есть неоднозначные классы case (где имена и типы членов одинаковы).

Будущее

Модуль generic-extras обеспечивает в этом отношении более удобную конфигурацию. Мы можем написать следующее, например:

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

И затем:

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

Вместо объекта-оболочки в JSON у нас есть дополнительное поле, которое указывает на конструктор. Это не поведение по умолчанию, так как оно имеет некоторые странные угловые случаи (например, если один из наших классов case имел член с именем what_am_i), но во многих случаях он был разумным и поддерживался в generic-extras, поскольку этот модуль был введен.

Это все еще не дает нам именно то, что мы хотим, но оно ближе, чем поведение по умолчанию. Я также рассматриваю возможность изменения withDiscriminator, чтобы взять Option[String] вместо String, с None, указав, что мы не хотим, чтобы дополнительное поле указывало на конструктор, давая нам такое же поведение, как и наш цирке- форм в предыдущем разделе.

Если вы заинтересованы в том, чтобы это произошло, откройте проблему или (еще лучше) запрос pull.:)