Jackson 严格解析:拒绝"温柔"的 JSON

32 阅读3分钟

为什么你的 API 总是悄咪咪地接受脏数据?Jackson 默认太"宽容"了,本文教你如何打造严格解析。


一、问题:Jackson 有多"宽容"?

来看看真实系统中的例子:

// 你期望的
{ "count": 10 }

// 客户端发送的(Jackson 居然接受了!)
{ "count": "10" }        // 字符串当数字
{ "count": 10.0 }        // 浮点当整数
{ "count": "" }          // 空字符串变 0 或 null
{ "age": 25, "naem": "Tom" }  // 拼写错误被忽略

这就是所谓的"技术债务" —— 当时不报错,后来数据全是坑。


二、什么是"严格"?

规则说明
未知字段失败客户端不能偷偷传多余字段
类型错误失败字符串 vs 数字,必须精确匹配
无自动转换"10" → 10,禁用!
数字校验NaN/Infinity 不允许
错误响应一致客户端能统一处理

三、实战:Spring Boot 严格配置

3.1 核心配置

@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 严格整数反序列化器

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();
    }
}

使用:

public record CreateOrderRequest(
    @JsonDeserialize(using = StrictIntDeserializer.class)
    Integer count
) {}

五、特殊场景:金额字段

金额是最容易出问题的:

方案 A:字符串(推荐)

{ "amount": "12.34" }

严格 BigDecimal 反序列化器:

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:整数(分)

{ "amountCents": 1234 }

六、统一错误响应

严格解析必须有统一的错误格式:

{
  "status": 400,
  "error": "invalid.json",
  "message": "Malformed JSON or invalid field type",
  "details": [
    { "field": "count", "reason": "Expected an integer number" }
  ]
}

实现:

@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);
    }
}

七、测试锁死

防止配置随时间"漂移":

@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 做额外校验!