为什么你的 API 总是悄咪咪地接受脏数据?Jackson 默认太"宽容"了,本文教你如何打造严格解析。
一、问题:Jackson 有多"宽容"?
来看看真实系统中的例子:
json
// 你期望的
{ "count": 10 }
// 客户端发送的(Jackson 居然接受了!)
{ "count": "10" } // 字符串当数字
{ "count": 10.0 } // 浮点当整数
{ "count": "" } // 空字符串变 0 或 null
{ "age": 25, "naem": "Tom" } // 拼写错误被忽略
这就是所谓的"技术债务" ------ 当时不报错,后来数据全是坑。
二、什么是"严格"?
| 规则 | 说明 |
|---|---|
| 未知字段失败 | 客户端不能偷偷传多余字段 |
| 类型错误失败 | 字符串 vs 数字,必须精确匹配 |
| 无自动转换 | "10" → 10,禁用! |
| 数字校验 | NaN/Infinity 不允许 |
| 错误响应一致 | 客户端能统一处理 |
三、实战:Spring Boot 严格配置
3.1 核心配置
java
@Configuration
public class StrictJacksonConfig {
@Bean
public Jackson2ObjectMapperBuilderCustomizer strictJacksonCustomizer() {
return builder -> builder.postConfigurer(this::configureStrictness);
}
private void configureStrictness(ObjectMapper mapper) {
// 1) 未知字段直接失败
mapper.enable(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES);
// 2) 原始类型不能为 null
mapper.enable(DeserializationFeature.FAIL_ON_NULL_FOR_PRIMITIVES);
// 3) 整数不能接受字符串/浮点/空字符串
mapper.coercionConfigFor(LogicalType.Integer)
.setCoercion(CoercionInputShape.String, CoercionAction.Fail)
.setCoercion(CoercionInputShape.Float, CoercionAction.Fail)
.setCoercion(CoercionInputShape.EmptyString, CoercionAction.Fail);
// 4) 浮点数同理
mapper.coercionConfigFor(LogicalType.Float)
.setCoercion(CoercionInputShape.String, CoercionAction.Fail)
.setCoercion(CoercionInputShape.EmptyString, CoercionAction.Fail);
// 5) 布尔值同理
mapper.coercionConfigFor(LogicalType.Boolean)
.setCoercion(CoercionInputShape.String, CoercionAction.Fail)
.setCoercion(CoercionInputShape.EmptyString, CoercionAction.Fail);
}
}
3.2 配置后的效果
| 场景 | 配置前 | 配置后 |
|---|---|---|
{ "count": "10" } |
✅ 接受 | ❌ 400 错误 |
{ "count": 10.0 } |
✅ 接受 | ❌ 400 错误 |
{ "count": "" } |
✅ 变成 null/0 | ❌ 400 错误 |
{ "unknown": 123 } |
✅ 忽略 | ❌ 400 错误 |
四、特殊场景:严格数字
有些边界情况 Jackson 解决不了:
| 边界 | 示例 |
|---|---|
| 前导/尾随空格 | " 10 " |
| 奇怪格式 | +10, 01 |
| 科学计数法 | 1e3 |
| 超大整数 | 超出 int 范围 |
4.1 严格整数反序列化器
java
public class StrictIntDeserializer extends JsonDeserializer<Integer> {
@Override
public Integer deserialize(JsonParser p, DeserializationContext ctxt)
throws IOException {
JsonNode node = p.getCodec().readTree(p);
// 只接受真正的 JSON 整数
if (!node.isInt()) {
throw JsonMappingException.from(p, "Expected an integer number");
}
return node.intValue();
}
}
使用:
java
public record CreateOrderRequest(
@JsonDeserialize(using = StrictIntDeserializer.class)
Integer count
) {}
五、特殊场景:金额字段
金额是最容易出问题的:
方案 A:字符串(推荐)
json
{ "amount": "12.34" }
严格 BigDecimal 反序列化器:
java
public class StrictBigDecimalStringDeserializer extends JsonDeserializer<BigDecimal> {
@Override
public BigDecimal deserialize(JsonParser p, DeserializationContext ctxt)
throws IOException {
JsonNode node = p.getCodec().readTree(p);
// 必须是字符串
if (!node.isTextual()) {
throw JsonMappingException.from(p, "Expected a decimal string like \"12.34\"");
}
String raw = node.textValue();
if (raw == null || raw.isBlank()) {
throw JsonMappingException.from(p, "Amount must not be blank");
}
// 格式校验:只能 digits + 可选小数点 + 最多2位小数
if (!raw.matches("^\\d+(\\.\\d{1,2})?$")) {
throw JsonMappingException.from(p, "Amount format must be \"12\" or \"12.34\"");
}
return new BigDecimal(raw);
}
}
方案 B:整数(分)
json
{ "amountCents": 1234 }
六、统一错误响应
严格解析必须有统一的错误格式:
json
{
"status": 400,
"error": "invalid.json",
"message": "Malformed JSON or invalid field type",
"details": [
{ "field": "count", "reason": "Expected an integer number" }
]
}
实现:
java
@RestControllerAdvice
public class JsonErrorHandler {
@ExceptionHandler(HttpMessageNotReadableException.class)
public ResponseEntity<ApiErrorResponse> handleNotReadable(
HttpMessageNotReadableException ex,
HttpServletRequest req) {
ApiErrorResponse body = ApiErrorResponse.of(
400,
"invalid.json",
"Malformed JSON or invalid field type",
req.getRequestURI(),
null,
List.of(ApiErrorDetail.of(null, rootCauseMessage(ex)))
);
return ResponseEntity.badRequest().body(body);
}
}
七、测试锁死
防止配置随时间"漂移":
java
@WebMvcTest
class StrictParsingTest {
@Autowired
private MockMvc mockMvc;
@Test
void shouldRejectStringAsInteger() throws Exception {
mockMvc.perform(post("/api/orders")
.contentType("application/json")
.content("""
{ "count": "10" }
"""))
.andExpect(status().isBadRequest());
}
@Test
void shouldRejectUnknownField() throws Exception {
mockMvc.perform(post("/api/orders")
.contentType("application/json")
.content("""
{ "count": 10, "unknown": "test" }
"""))
.andExpect(status().isBadRequest());
}
}
八、总结
| 步骤 | 操作 |
|---|---|
| 1 | 关闭宽容模式 |
| 2 | 严格数字校验 |
| 3 | 金额字段专用解析器 |
| 4 | 统一错误响应 |
| 5 | 测试锁死 |
核心原则 :越早失败,成本越低。
💡 提示 :如果你使用的是 Spring Boot 3.x,别忘了引入
jakarta.validation做额外校验!