Suppose I've got an ADT like this:

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

对于circe中的Decoder[Event]实例,默认的泛型派生期望输入JSON包含一个包装器对象,该对象指示表示哪个 case 类:

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

This behavior means that we never have to worry about ambiguities if two or more case classes have the same member names, but it's not always what we want—sometimes we know the unwrapped encoding would be unambiguous, or we want to disambiguate by specifying the order each case class should be tried, or we just don't care.

我如何在没有包装器的情况下编码和解码我的Event ADT(最好不用从头开始编写编码器和解码器)?

(这个问题经常出现-例如,参见今天上午在Gitter上的this discussion with Igor Mazor.)

推荐答案

Enumerating the ADT constructors

The most straightforward way to get the representation you want is to use generic derivation for the case classes but explicitly defined instances for the ADT type:

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

Note that we have to call widen (which is provided by Cats's Functor syntax, which we bring into scope with the first import) on the decoders because the Decoder type class is not covariant. The invariance of circe's type classes is a matter of some controversy (Argonaut for example has gone from invariant to covariant and back), but it has enough benefits that it's unlikely to change, which means we need workarounds like this occasionally.

It's also worth noting that our explicit Encoder and Decoder instances will take precedence over the generically-derived instances we'd otherwise get from the io.circe.generic.auto._ import (see my slides here for some discussion of how this prioritization works).

We can use these instances like this:

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构造函数的try 顺序,这是目前最好的解决方案.尽管如此,像这样枚举构造函数显然并不理想,即使我们免费获得case类实例.

A more generic solution

As I note on Gitter, we can avoid the fuss of writing out all the cases by using the circe-shapes 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

然后:

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}

这将适用于范围内encodeAdtNoDiscrdecodeAdtNoDiscr范围内的任何ADT.如果我们希望它受到更多的限制,我们可以在那些定义中用我们的ADT类型替换泛型A,或者我们可以将定义设为非隐式,并显式地为我们希望以这种方式编码的ADT定义隐式实例.

这种方法的主要缺点(除了额外的Circe-Shape依赖关系之外)是构造函数将按字母顺序进行try ,如果我们有不明确的case类(其中成员名称和类型相同),这可能不是我们想要的.

The future

The generic-extras module provides a little more configurability in this respect. We can write the following, for example:

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的成员),但在许多情况下它是合理的,而且自从引入该模块以来,它在泛型附加中得到了支持.

这仍然没有得到我们想要的东西,但比默认行为更接近.我还在考虑将withDiscriminator更改为Option[String],而不是String,其中None表示我们不需要额外的字段来指示构造函数,从而使我们的行为与上一节中的Circe-Shape实例相同.

如果你有兴趣看到这一切的发生,请打开an issue,或者(更好的)pull request.:)

Json相关问答推荐

使用JSONata将具有相同键的字典合并

在T—SQL中将STR_AGG与JSON_ARRAY结合起来

删除JSON文件的特定内容

jq 对特定键进行过滤并将值整理到单个 csv 单元格中

Oracle JSON 查询中的动态列列表

使用 Groovy 将 XML 转换为 JSON

我需要在 mongodb compass 中检索索引(编号 1)信息

如果 jq 数组中的字符串是对象或字符串,则获取值

使用 Ansible 解析来自 Juniper 交换机的 JSON 输出

将 js Array() 转换为 JSON 对象以用于 JQuery .ajax

JSON.NET 中特定对象的自定义转换

Django - 异常处理最佳实践和发送自定义错误消息

将 CoffeeScript 项目转换为 JavaScript(不缩小)?

如何将有向无环图 (DAG) 存储为 JSON?

如何按键查找特定的 JSON 值?

在android中读取Json数组

你如何在 Arrays of Arrays 上 OPENJSON

通过 JSON 发送 64 位值的公认方式是什么?

如何向 json IAM 策略添加 comments ?

C++:使用 nlohmann json 从文件中读取 json 对象