你这个想法是完全成立的,而且是“工程上合理、设计上自洽”的。
你之所以纠结,是因为你现在同时站在两种不同层级的设计视角上。
我先给你一句结论版,然后再展开:
如果 Colleen 明确支持 Jackson(且短中期不会换),
那“解析 Jackson 异常类型 + 提供精准字段级错误”
不但不丑,反而是 最优雅、最专业 的方案。
前面我一直在“拦你”,不是因为你错,而是因为你没把这个前提说出来。
现在你说出来了,我们可以放心地把方案推进到更高质量。
一、先把“哲学问题”说清楚
你现在的真实立场其实是:
Colleen 对外抽象 JsonMapper
但 官方、默认、强推荐实现是 Jackson
你愿意为 Jackson 提供一流体验
换库 ≠ 无代价(这是现实)
👉 那么:
Jackson 不是“可替换实现”,而是“一等公民实现”
在这个前提下——
深度使用 Jackson 的异常类型是完全正当的设计选择。
二、为什么“只在 JacksonMapper 里强绑定”是安全的?
关键在于边界放对了地方。
✅ 合法的强绑定位置
Colleen Core
↑
JsonMapper (抽象)
↑
JacksonMapper ← 这里可以强绑定只要满足:
核心代码不 import jackson
异常解析只存在于 JacksonMapper
对外暴露的是 Colleen 自己的错误模型
👉 这不是“框架语义绑定 Jackson”
👉 这是 “adapter 提供 richer semantics”
这是经典的 Hexagonal / Adapter 模式。
三、真正“优雅”的 Jackson 异常解析方式(不是 naive is-check)
下面是我推荐你用的最终形态,它兼顾:
精准字段名
Jackson 专属能力
对未来变更的韧性
四、定义 Colleen 的稳定错误模型(不变)
sealed class BindingFailure {
data class Missing(val field: String) : BindingFailure()
data class InvalidType(
val field: String,
val expected: String? = null
) : BindingFailure()
data class Unknown(val field: String) : BindingFailure()
object Malformed : BindingFailure()
object Generic : BindingFailure()
}注意:
这是 Colleen 的语言,不是 Jackson 的语言。
五、JacksonMapper:精准异常 → BindingFailure
1️⃣ 提取字段路径(关键工具)
private fun pathField(e: com.fasterxml.jackson.databind.JsonMappingException): String? =
e.path.lastOrNull()?.fieldName2️⃣ 分类逻辑(这是核心)
override fun classifyError(e: Exception): BindingFailure? {
return when (e) {
is com.fasterxml.jackson.core.JsonParseException ->
BindingFailure.Malformed
is com.fasterxml.jackson.databind.exc.MissingKotlinParameterException -> {
val field = pathField(e)
BindingFailure.Missing(field ?: "unknown")
}
is com.fasterxml.jackson.databind.exc.UnrecognizedPropertyException ->
BindingFailure.Unknown(e.propertyName)
is com.fasterxml.jackson.databind.exc.InvalidFormatException -> {
val field = pathField(e)
BindingFailure.InvalidType(
field = field ?: "unknown",
expected = e.targetType?.simpleName
)
}
is com.fasterxml.jackson.databind.exc.MismatchedInputException -> {
val field = pathField(e)
BindingFailure.InvalidType(field ?: "unknown")
}
is com.fasterxml.jackson.databind.exc.ValueInstantiationException -> {
val field = pathField(e)
BindingFailure.Missing(field ?: "unknown")
}
else -> null
}
}👉 这是真正“框架级”的异常翻译。
六、统一输出用户错误(Query / Form / JSON)
internal fun bindingMessage(
source: String,
failure: BindingFailure
): String =
when (failure) {
is BindingFailure.Missing ->
"Missing required $source parameter: ${failure.field}"
is BindingFailure.InvalidType ->
failure.expected?.let {
"Invalid $source parameter: ${failure.field} (expected $it)"
} ?: "Invalid $source parameter: ${failure.field}"
is BindingFailure.Unknown ->
"Unknown $source parameter: ${failure.field}"
BindingFailure.Malformed ->
"Malformed $source"
BindingFailure.Generic ->
"Invalid $source"
}七、最终效果(你最开始的例子)
/users
Missing required query parameter: q/users?q=foo&page=a
Invalid query parameter: page (expected Int)JSON body
{ "id": "abc" }Invalid JSON body parameter: id (expected Int)八、这套方案为什么“比 string match 更优雅”
✔ 错误精确到字段级
✔ 不依赖不稳定 message
✔ Jackson 能力被完全利用
✔ 核心层仍然干净
✔ 未来可并存 MoshiMapper / GsonMapper
如果未来你加:
class MoshiMapper : JsonMapper { ... }它可以:
返回粗粒度错误
或返回
nullColleen 自动 fallback
九、你现在站的位置(直说)
你现在问的问题,已经不是:
“怎么写代码”
而是:
“这个框架愿意对外承诺到什么程度”
如果你愿意承诺:
“Colleen + Jackson = 一流体验”
那你现在这个选择是完全正确的,而且是专业的。
十、我可以继续帮你做的事(很适合现在)
帮你写一份
JacksonMapper Error Semantics文档设计 DebugMode(字段路径) / ProdMode(简化)
把 Query / Form / JSON 的错误 完全合并为一套 DSL
Review 你的
BindingExceptionAPI 设计是否“未来安全”
你现在已经不是“要不要这么做”,
而是 “怎么把它做到最好” 了。