这是一个非常典型、也非常“高级”的 Moshi + 泛型陷阱,你的整体设计是对的,问题点非常集中、也很好解释。
结论先行(一句话版)
测试 5 失败的根本原因是:你在 MoshiCodec.toJson 中使用了 Any::class.java 进行序列化,导致 Moshi 丢失了泛型信息(T),而 Jackson 在序列化阶段不会丢。
所以:
反序列化阶段你是“对的”
序列化阶段 Moshi 被你“喂错了类型”
Jackson 因为内部模型不同,刚好“宽容地成功了”
为什么「只有测试 5」会失败?
测试 5 的类型是什么?
ApiResponse<List<User>>这是一个嵌套泛型:
外层:
ApiResponse<T>内层:
List<User>
关键问题代码(真正的 bug 点)
override fun toJson(value: Any?): String {
moshi.adapter(Any::class.java).toJson(value)
}以及:
moshi.adapter(Any::class.java).toJson(sink, value)这行代码在 Moshi 中意味着什么?
“请你用
Any的 adapter 来序列化这个对象”
而 Moshi 对 Any 的处理策略是:
Map→Map<String, Any?>List→List<Any?>自定义对象 → 反射成
Map<String, Any?>泛型参数直接丢失
换句话说
ApiResponse<List<User>>
↓
ApiResponse<List<LinkedHashMap<String, Any>>>为什么前 4 个测试没问题?
因为它们不依赖“精确泛型结构”:
测试
是否依赖泛型精度
原因
User
❌
非泛型
List
⚠️
顶层 List,可恢复
Map<String, User>
⚠️
Key 固定
Post
❌
非泛型
但 测试 5 是「泛型包泛型」:
ApiResponse<T>
T = List<User>一旦中间 T 被擦成 Any,信息不可逆。
为什么 Jackson 全部能过?
Jackson 的行为是:
mapper.writeValueAsString(value)Jackson 在运行时会:
通过 Kotlin module
读取对象的 实际属性签名
不依赖调用方提供的 Type
所以 Jackson 能“猜对”,而 Moshi 不能靠猜。
核心设计原则(非常重要)
Moshi 是「类型驱动的 JSON 库」Jackson 是「对象驱动的 JSON 库」
你的设计是 Codec 抽象层完全正确,但 Moshi 必须拿到 Type。
简洁、优雅、工程化的解决方案 ✅
✅ 方案一(推荐):toJson 也传入 Type
这是最干净、最对称、最框架级的方案。
1️⃣ 修改接口
interface JsonCodec {
fun toJson(value: Any?, type: Type): String
fun toJson(value: Any?, type: Type, output: OutputStream)
fun <T> fromJson(json: String, type: Type): T?
fun <T> fromJson(input: InputStream, type: Type): T?
}2️⃣ Moshi 实现(关键点)
override fun toJson(value: Any?, type: Type): String {
return try {
moshi.adapter<Any>(type).toJson(value)
} catch (e: Exception) {
throw JsonException("Failed to serialize to JSON", e)
}
}3️⃣ Response 侧保持优雅
inline fun <reified T> json(value: T): Response {
setHeader("Content-Type", "application/json; charset=utf-8")
codec.toJson(value, object : TypeRef<T>() {}.type, outputStream)
return this
}4️⃣ 测试 5 立刻通过 🎯
因为 Moshi 拿到了:
ApiResponse<List<User>>而不是:
Any❌ 不推荐但你可能想到的方案
❌ 使用 PolymorphicJsonAdapterFactory
解决的是 多态
不是 泛型信息
会引入
$type字段,破坏 JSON
❌ 序列化时先转 Map 再转回
本质仍然丢类型
性能和语义都更差
你这个设计,其实已经非常接近「生产级」
你已经正确地做了:
✅ Codec 抽象
✅ TypeRef
✅ reified API
✅ 流式支持
✅ Moshi / Jackson 可插拔
唯一缺的一步就是:👉 序列化阶段也要和反序列化一样“尊重 Type”
一句话总结
Moshi 不会“帮你猜泛型”,
Jackson 会。
框架作者应该选择“不猜”。
如果你愿意,下一步我可以帮你把这个 JSON 模块直接打磨成类似 Ktor / Jooby 级别的 API(比如自动 Content-Negotiation)。