(反)在 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
它使用inline
和Mirror
到
- 获取sum类型元素的类型列表
- 获取这些类型的标签列表
- 获取它们的值以创建
Map[String, T]
或Map[T, String]
emap
/contramap
String
编解码器,这样翻译就不会嵌套也不需要区分字段
它仅适用于由 case object
组成的枚举,如果任何 case
存储了一些嵌套数据,它将失败 - 为此使用带有鉴别器字段的标准推导过程或以嵌套命名的嵌套结构类型。
您可以导入 stringEnumDecoder
和 stringEnumEncoder
并给出它们,但我更愿意手动添加它们,因为它们更像是例外而不是规则。
有一个图书馆可以帮助您解决这个问题: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
尝试通过字符串构造 Encoder
和 Decoder
实例
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)))
}
}
我正在尝试找到一种简单有效的方法来使用 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
它使用inline
和Mirror
到
- 获取sum类型元素的类型列表
- 获取这些类型的标签列表
- 获取它们的值以创建
Map[String, T]
或Map[T, String]
emap
/contramap
String
编解码器,这样翻译就不会嵌套也不需要区分字段
它仅适用于由 case object
组成的枚举,如果任何 case
存储了一些嵌套数据,它将失败 - 为此使用带有鉴别器字段的标准推导过程或以嵌套命名的嵌套结构类型。
您可以导入 stringEnumDecoder
和 stringEnumEncoder
并给出它们,但我更愿意手动添加它们,因为它们更像是例外而不是规则。
有一个图书馆可以帮助您解决这个问题: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
尝试通过字符串构造 Encoder
和 Decoder
实例
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)))
}
}