Scala Circe报错:找不到io.circe.Encoder[SapHealth]隐式值,求解
解决circe无法为SapHealth trait找到Encoder的问题
这个错误的核心原因很明确:你的build变量类型是父类型SapHealth,但你只提供了**子类型SapHealthAccepted**的隐式Encoder,circe无法自动为父trait生成编解码器——因为它需要明确知道如何处理trait的所有子类型(这里还有SapHealthRejected)。
为什么原代码不工作?
你手动定义的encodeFieldType是Encoder[SapHealthAccepted],但当你调用build.asJson时,build的静态类型是SapHealth,编译器会寻找Encoder[SapHealth]的隐式实例,而不是子类的Encoder。circe不会自动将子类的Encoder提升为父类的,因为父trait可能包含多个子类型,必须显式处理所有情况。
解决方案:为ADT(代数数据类型)生成编解码器
circe提供了两种常用方式来处理trait类型的序列化:
方案1:使用circe-generic-extras(推荐,更简洁)
这个模块专门用于处理ADT的序列化,支持通过判别字段(比如type)区分不同子类型。
- 首先确保你的依赖中包含
circe-generic-extras(sbt示例,版本和你的circe其他模块保持一致):
libraryDependencies += "io.circe" %% "circe-generic-extras" % "0.14.6"
- 修改代码如下:
import java.time.Instant import io.circe.{Decoder, Encoder} import io.circe.generic.extras.semiauto._ import io.circe.generic.extras.Configuration import io.circe.syntax._ // 配置判别字段,用于区分不同的SapHealth子类型 implicit val customConfig: Configuration = Configuration.default .withDiscriminator("type") // 生成的JSON会包含"type"字段,值为子类型的类名(比如"SapHealthAccepted") .withSnakeCaseMemberNames // 自动将驼峰命名转为蛇形命名,省得手动重命名字段 trait SapHealth {} case class SapHealthRejected(reason: String) extends SapHealth case class SapHealthAccepted(sapId: String, requestedAt: Long) extends SapHealth object SapHealth { // 为父trait生成Encoder和Decoder,自动处理所有子类型 implicit val sapHealthEncoder: Encoder[SapHealth] = deriveEncoder[SapHealth] implicit val sapHealthDecoder: Decoder[SapHealth] = deriveDecoder[SapHealth] private val build: SapHealth = SapHealthAccepted(SapmockActor.system.name, Instant.now().getEpochSecond) val create: String = build.asJson.noSpaces }
这样生成的JSON会自动包含判别字段,同时字段名转为蛇形(比如sapId变成sap_id),完全符合你的需求。
方案2:手动编写Encoder/Decoder(适合自定义逻辑)
如果你不想引入额外依赖,可以手动为SapHealth编写编解码器,处理所有子类型:
import java.time.Instant import io.circe.{Decoder, Encoder, HCursor, Json} import io.circe.generic.auto._ import io.circe.syntax._ trait SapHealth {} case class SapHealthRejected(reason: String) extends SapHealth case class SapHealthAccepted(sapId: String, requestedAt: Long) extends SapHealth object SapHealth { // 手动实现SapHealth的Encoder implicit val sapHealthEncoder: Encoder[SapHealth] = Encoder.instance { case accepted: SapHealthAccepted => // 手动将字段名转为蛇形 accepted.asJson.mapObject(_.renameKeys(Map( "sapId" -> "sap-id", "requestedAt" -> "requested_at" ))) case rejected: SapHealthRejected => rejected.asJson } // 对应的Decoder(如果需要反序列化的话) implicit val sapHealthDecoder: Decoder[SapHealth] = Decoder.instance { c: HCursor => // 通过字段判断子类型:检查是否存在"sap-id"字段 c.get[String]("sap-id").asOpt match { case Some(_) => // 先反序列化为蛇形字段的结构,再映射回驼峰命名的case class c.as[SapHealthAccepted](Decoder.forProduct2("sap-id", "requested_at")(SapHealthAccepted.apply)) case None => c.as[SapHealthRejected] } } private val build: SapHealth = SapHealthAccepted(SapmockActor.system.name, Instant.now().getEpochSecond) val create: String = build.asJson.noSpaces }
总结
无论哪种方案,核心都是为父类型SapHealth提供明确的隐式Encoder/Decoder,而不是只给子类型提供。方案1更简洁且易于维护,推荐使用。
内容的提问来源于stack exchange,提问作者softshipper
相关产品推荐
相关产品推荐

