I am trying to find a simple and efficient way to (de)serialize enums in Scala 3 using circe.
Consider the following example:
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
On serializing the data, it becomes
{
"id" : 1,
"type" : {
"SELL" : {
}
},
"amount" : "123.4"
}
instead of
{
"id" : 1,
"type" : "SELL",
"amount" : "123.4"
}
which is what I want.
I know that I can write a custom (de)serializer for this which will solve the issue for this particular instance like this:
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)
but I was looking for a generic solution that might work for any enum.
EDIT 1
One way of doing serialization, (deserialization does not work :/) is to make all enums extend a common trait and define encoder for all enums extending it. For the above example, it looks something like this.
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"}
However this does not extend to deserialization.
In Scala 3 you can use Mirrors to do the derivation directly:
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
It uses inline and Mirror to
Map[String, T] or Map[T, String]emap/contramap String codecs so that translation wouldn't be nested nor require discriminating fieldIt only works with enums made of case objects and it would fail if any case stored some nested data - for that use standard derivation procedure with discriminator fields or nested structure named after the nested type.
You could import stringEnumDecoder and stringEnumEncoder and make them given, although I would prefer to add them manually, since they are more of an exception than a rule.
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With