如何为含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
相关产品推荐
相关产品推荐

