(反)在 Scala 3 中将枚举序列化为字符串

(De)serialize enum as string in Scala 3

我正在尝试找到一种简单有效的方法来使用 circe 在 Scala 3 中(反)序列化枚举。

考虑以下示例:

import io.circe.generic.auto._
import io.circe.syntax._

enum OrderType:
  case BUY
  case SELL

case class Order(id: Int, `type`: OrderType, amount: String)
val order = Order(1, OrderType.SELL, "123.4")
order.asJson

在序列化数据时,它变成

{
  "id" : 1,
  "type" : {
    "SELL" : {
      
    }
  },
  "amount" : "123.4"
}

而不是

{
  "id" : 1,
  "type" : "SELL",
  "amount" : "123.4"
}

这就是我想要的。

我知道我可以为此编写一个自定义(反)序列化器,它将像这样解决这个特定实例的问题:

implicit val encodeOrderType: Encoder[OrderType] = (a: OrderType) =>
  Encoder.encodeString(a.toString)
  
implicit def decodeOrderType: Decoder[OrderType] = (c: HCursor) => for {
  v <- c.as[String]
} yield OrderType.valueOf(v)

但我一直在寻找可能适用于任何 enum 的通用解决方案。

编辑 1

进行序列化的一种方法(反序列化不起作用:/)是使所有枚举扩展一个共同特征并为所有扩展它的枚举定义编码器。对于上面的示例,它看起来像这样。

trait EnumSerialization

enum OrderType extends EnumSerialization:
  case BUY
  case SELL
  
enum MagicType extends EnumSerialization:
  case FIRE
  case WATER
  case EARTH
  case WIND

implicit def encodeOrderType[A <: EnumSerialization]: Encoder[A] = (a: A) => Encoder.encodeString(a.toString)

// This correctly serializes all instances of enum into a string
case class Order(id: Int, `type`: OrderType, amount: String)
val order     = Order(1, OrderType.SELL, "123.4")
val orderJson = order.asJson
// Serializes to { "id" : 1, "type" : "SELL", "amount" : "123.4"}

case class Magic(id: Int, magic: MagicType)
val magic = Magic(3, MagicType.WIND)
val magicJson = magic.asJson
// Serializes to { "id" : 3, "magic" : "WIND"}

但是这不会扩展到反序列化。

在 Scala 3 中,您可以使用 Mirrors 直接进行推导:

import io.circe._

import scala.compiletime.summonAll
import scala.deriving.Mirror

inline def stringEnumDecoder[T](using m: Mirror.SumOf[T]): Decoder[T] =
  val elemInstances = summonAll[Tuple.Map[m.MirroredElemTypes, ValueOf]]
    .productIterator.asInstanceOf[Iterator[ValueOf[T]]].map(_.value)
  val elemNames = summonAll[Tuple.Map[m.MirroredElemLabels, ValueOf]]
    .productIterator.asInstanceOf[Iterator[ValueOf[String]]].map(_.value)
  val mapping = (elemNames zip elemInstances).toMap
  Decoder[String].emap { name =>
    mapping.get(name).fold(Left(s"Name $name is invalid value"))(Right(_))
  }

inline def stringEnumEncoder[T](using m: Mirror.SumOf[T]): Encoder[T] =
  val elemInstances = summonAll[Tuple.Map[m.MirroredElemTypes, ValueOf]]
    .productIterator.asInstanceOf[Iterator[ValueOf[T]]].map(_.value)
  val elemNames = summonAll[Tuple.Map[m.MirroredElemLabels, ValueOf]]
    .productIterator.asInstanceOf[Iterator[ValueOf[String]]].map(_.value)
  val mapping = (elemInstances zip elemNames).toMap
  Encoder[String].contramap[T](mapping.apply)

enum OrderType:
  case BUY
  case SELL
object OrderType:
  given decoder: Decoder[OrderType] = stringEnumDecoder[OrderType]
  given encoder: Encoder[OrderType] = stringEnumEncoder[OrderType]
end OrderType
import io.circe.syntax._
import io.circe.generic.auto._

case class Order(id: Int, `type`: OrderType, amount: String)
val order = Order(1, OrderType.SELL, "123.4")
order.asJson
// {
//   "id" : 1,
//   "type" : "SELL",
//   "amount" : "123.4"
// }: io.circe.Json

它使用inlineMirror

  • 获取sum类型元素的类型列表
  • 获取这些类型的标签列表
  • 获取它们的值以创建Map[String, T]Map[T, String]
  • emap/contramap String 编解码器,这样翻译就不会嵌套也不需要区分字段

它仅适用于由 case object 组成的枚举,如果任何 case 存储了一些嵌套数据,它将失败 - 为此使用带有鉴别器字段的标准推导过程或以嵌套命名的嵌套结构类型。

您可以导入 stringEnumDecoderstringEnumEncoder 并给出它们,但我更愿意手动添加它们,因为它们更像是例外而不是规则。

有一个图书馆可以帮助您解决这个问题:circe-tagged-adt-codec-scala3

有了这个你的代码看起来像

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

enum OrderType derives JsonTaggedAdt.Encoder:
  case BUY
  case SELL
  
enum MagicType derives JsonTaggedAdt.Encoder:
  case FIRE
  case WATER
  case EARTH
  case WIND

尝试通过字符串构造 EncoderDecoder 实例

  import io.circe.*
  import io.circe.parser.*
  import io.circe.syntax.*

  enum OrderType:
    case BUY
    case SELL

  given Encoder[OrderType] = (a: OrderType) => a.toString.asJson
  given Decoder[OrderType] = (c: HCursor) =>
    Decoder.decodeString(c).flatMap { str =>
      Try(OrderType.valueOf(str)).toEither.leftMap { _ =>
        DecodingFailure(s"no enum value matched for $str", List(CursorOp.Field(str)))
      }
    }