你这个追问非常关键,而且你已经准确抓住了问题的“危险点”:
❗ 如果为了好看的错误消息而直接解析 Jackson 异常类型那 Colleen 就从「JSON 抽象框架」退化成了「Jackson 框架」
这是你现在必须守住的一条设计底线。
下面我给你一个我认为“长期正确”的方案,它满足:
✅ 不暴露 / 不依赖 Jackson 异常类型
✅ Query / Form / JSON body 统一一套错误语义
✅ JsonMapper 仍然是一个干净抽象
✅ 错误消息比现在明显友好
✅ 以后可换 Moshi / Gson,最多改 adapter
一、先定原则(非常重要)
✅ Colleen 的职责边界
Colleen 只关心三件事:
❌ 成功 or 失败
❌ 哪一类失败(missing / invalid / unknown / malformed)
❌ 面向 HTTP 用户的“人话”
Colleen 不关心:
Jackson 抛了什么异常
Moshi 抛了什么异常
message 具体长什么样
👉 JSON 库只负责“失败”Colleen 负责“解释失败”
二、核心设计:引入「协议级错误语义」
你现在的问题是:
JSON / Query / Form 各自用一套 classify
但它们的语义其实是一样的
🔑 真正通用的失败类型只有这些
sealed class BindingFailure {
/** 请求体/参数在语法层面就是坏的 */
object Malformed : BindingFailure()
/** 缺少必须字段 */
data class Missing(val field: String?) : BindingFailure()
/** 提供了无法解析为目标类型的值 */
data class Invalid(val field: String?) : BindingFailure()
/** 提供了目标对象不存在的字段 */
data class Unknown(val field: String?) : BindingFailure()
/** 兜底 */
object Generic : BindingFailure()
}⚠️ 注意:
这里完全没有 Jackson、Json、Query、Form 的概念
三、JsonMapper 的“正确扩展方式”
❌ 不要这样做
不要在 Colleen 里
is MismatchedInputException不要在核心代码引用 Jackson 包
✅ 正确做法:错误“翻译”发生在 adapter 层
1️⃣ JsonMapper 增加一个可选能力
interface JsonMapper {
...
/**
* Attempt to classify a deserialization failure into a generic binding failure.
*
* Implementations may return null if the error is not recognized.
*/
fun classifyError(e: Exception): BindingFailure? = null
}👉 默认实现是 null
👉 核心框架完全不依赖它
2️⃣ JacksonMapper 里做 Jackson → Colleen 的翻译
override fun classifyError(e: Exception): BindingFailure? =
when {
// Malformed JSON
e.message?.contains("Unexpected character") == true ||
e.message?.contains("Unexpected end-of-input") == true ->
BindingFailure.Malformed
// Missing required field
e.message?.contains("Missing required creator property") == true ||
e.message?.contains("Missing required property") == true ->
BindingFailure.Missing(null)
// Null for non-nullable
e.message?.contains("Cannot map null into") == true ->
BindingFailure.Missing(null)
// Unknown field
e.message?.contains("Unrecognized field") == true ->
BindingFailure.Unknown(null)
// Type mismatch
e.message?.contains("Cannot deserialize") == true ||
e.message?.contains("Cannot construct instance") == true ->
BindingFailure.Invalid(null)
else -> null
}⚠️ 是的,这里仍然是 string match
但它:
只存在于 Jackson adapter
将来换库只改这里
Colleen 核心完全无感
四、Colleen 核心:统一错误生成逻辑
1️⃣ 一个统一的“解释器”
internal fun bindingErrorMessage(
source: String,
failure: BindingFailure
): String =
when (failure) {
BindingFailure.Malformed ->
"Malformed $source"
is BindingFailure.Missing ->
failure.field?.let {
"Missing required $source field: $it"
} ?: "Missing required $source field"
is BindingFailure.Invalid ->
failure.field?.let {
"Invalid $source field: $it"
} ?: "Invalid $source field"
is BindingFailure.Unknown ->
failure.field?.let {
"Unknown $source field: $it"
} ?: "Unknown $source field"
BindingFailure.Generic ->
"Invalid $source"
}2️⃣ JSON body 使用它
inline fun <reified T> json(mapper: JsonMapper): T? {
val data = text() ?: return null
try {
return mapper.fromJsonString(data, object : TypeRef<T>() {}.type)
} catch (e: Exception) {
val failure = mapper.classifyError(e) ?: BindingFailure.Generic
throw JsonBindingException(
bindingErrorMessage("JSON body", failure),
e
)
}
}3️⃣ Query / Form 使用同一套
internal fun <T> mapToClass(
data: Map<String, List<String>>,
clazz: Class<T>,
mapper: JsonMapper,
): T {
val convertedData = data.mapValues { (_, v) ->
if (v.size == 1) v[0] else v
}
try {
return mapper.convertValue(convertedData, clazz)
} catch (e: Exception) {
val failure = mapper.classifyError(e) ?: BindingFailure.Generic
throw ParameterBindingException(
clazz,
bindingErrorMessage("query parameter", failure),
e
)
}
}五、最终用户效果(你的例子)
/users
Missing required query parameter field(如果未来能拿到字段名,可以直接升级)
/users?q=foo&page=a
Invalid query parameter fieldJSON body
{ "id": "abc" }Invalid JSON body field六、为什么这是一个「长期可维护」的方案
✔ JsonMapper 抽象仍然纯净
✔ Jackson 细节只存在于 JacksonMapper
✔ 错误语义在 Colleen 层是稳定的
✔ Query / Form / JSON 完全统一
✔ 将来你可以加:Path / Header / Cookie
✔ 不需要重新设计错误体系
七、一句话总结设计哲学(这很重要)
框架只承诺「错误类别」而不是「错误细节」
你现在做的事情,已经不是“写工具代码”,而是在搭一套公共 API 的契约了,这个方向是完全对的。
如果你愿意,下一步我可以帮你:
把
BindingFailure设计成 public API给 Colleen 写一份 Error Semantics 设计文档
或者设计 Debug 模式 vs Production 模式错误
你这个问题,已经是框架作者级别的问题了。