You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

如何为含ADT成员的类编写Circe字符串编解码器?

使用Circe实现自定义字符串编解码器的方案

问题场景

定义如下Scala case class和ADT:

case class Something(s: String, foo: Foo, bar: Bar)

trait Bar
case object A extends Bar
case object B extends Bar

期望序列化Something后的JSON格式如下:

{
  "s": "smstring",
  "foo": {...somefoo...},
  "bar": "aaa" // 对应Bar.A,Bar.B则为"bbb"
}

需要实现对应的序列化/反序列化编解码器,同时询问:是否可以通过派生Something的Codec后,用contramap等方法修改来实现?

最简实现方案

1. 实现Bar的自定义Codec

Bar是ADT,需要手动定义字符串与实例的映射逻辑,这是实现的核心:

import io.circe.{Encoder, Decoder, Codec}
import io.circe.generic.semiauto._

trait Bar
case object A extends Bar
case object B extends Bar

implicit val barCodec: Codec[Bar] = Codec.from(
  // 解码逻辑:字符串转Bar实例
  Decoder[String].emap {
    case "aaa" => Right(A)
    case "bbb" => Right(B)
    case invalid => Left(s"无效的Bar值: $invalid")
  },
  // 编码逻辑:Bar实例转字符串
  Encoder[String].contramap {
    case A => "aaa"
    case B => "bbb"
  }
)

2. 自动派生Something的Codec

只要Bar的Codec处于隐式作用域内,就能直接用半自动化方式派生Something的Codec,无需额外修改:

case class Something(s: String, foo: Foo, bar: Bar)

// 假设Foo已存在对应的隐式Codec
implicit val somethingCodec: Codec[Something] = deriveCodec[Something]

这样序列化时bar字段会自动转换为"aaa"/"bbb",反序列化时也能正确将字符串解析为对应的Bar实例。

关于派生后修改Codec的可行性

确实可以通过contramap(编码端)和map(解码端)修改已派生的Something Codec,但这种方式代码冗余且容易出错,远不如先定义Bar的Codec再派生的方式简洁。

举个强行修改的示例(不推荐):

// 先派生默认Codec(此时Bar会被序列化为对象形式,如{"A":{}})
val defaultSomethingCodec: Codec[Something] = deriveCodec[Something]

// 修改编码逻辑:替换bar字段为目标字符串
val modifiedEncoder: Encoder[Something] = Encoder.encodeJsonObject.contramap { something =>
  val originalObj = defaultSomethingCodec.encodeObject(something)
  val barStr = something.bar match { case A => "aaa"; case B => "bbb" }
  originalObj.add("bar", Encoder[String].apply(barStr))
}

// 修改解码逻辑:解析bar字符串为Bar实例
val modifiedDecoder: Decoder[Something] = Decoder.decodeJsonObject.emap { obj =>
  for {
    barStr <- obj.get[String]("bar").toEither
    bar <- barStr match {
      case "aaa" => Right(A)
      case "bbb" => Right(B)
      case invalid => Left(s"无效的bar值: $invalid")
    }
    // 临时替换bar字段为默认格式,再用原始解码器解析
    tempObj = obj.add("bar", Encoder[Bar].apply(bar))
    something <- defaultSomethingCodec.decodeJson(tempObj.asJson).toEither
  } yield something
}

val modifiedSomethingCodec: Codec[Something] = Codec.from(modifiedDecoder, modifiedEncoder)

显然这种绕路的方式没必要,最简方案还是先实现Bar的自定义Codec,再自动派生Something的Codec。

内容的提问来源于stack exchange,提问作者Ostap Strashevskii

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.08.22 03:06:22