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

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)区分不同子类型。

  1. 首先确保你的依赖中包含circe-generic-extras(sbt示例,版本和你的circe其他模块保持一致):
libraryDependencies += "io.circe" %% "circe-generic-extras" % "0.14.6"
  1. 修改代码如下:
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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.05.13 07:55:18