很多朋友把“能被 JSON.parse
成对象”当成“数据没问题”。但是一到线上:一个小小的 null
、一个多余字段、一个写错格式的时间串,就能产生严重的问题。JSON 校验 做的除了能不能解析,更重要的是要验证是“结构对不对、值合不合理、能不能放心往下游传 ”。
下面,我尽量把落地要点、常见坑和多语言方案一次说清,配一套可复制的脚手架。
1. 目标边界:校验到底在查什么
语法 :是不是合法 JSON。对应 JSON.parse
/ Jackson / json.loads
。标准见 RFC 8259 与 ECMA-404(两个标准都在定义 JSON 的语法,彼此兼容)。(IETF Datatracker , Ecma International )
结构/类型/约束 :对象里有哪些字段、类型是什么、取值范围/正则/枚举、数组长度、是否允许未知字段。推荐用 JSON Schema (最新版规范见 json-schema.org )。(JSON Schema )
业务规则 :跨字段/跨对象的规则,比如“有 discount
就必须有 couponCode
且 discount <= price
”。这一层通常要在代码里自定义校验器(Schema 里也能写一部分,比如 if/then/else
、dependentRequired
)。(JSON Schema )
语法过关 ≠ 有效数据 。校验的职责是把“输入不确定性”挡在系统边界之外。
2. 先有一个能跑的 Schema
以一个订单片段为例(可直接拷贝到你的项目里):
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 { "$schema" : "https://json-schema.org/draft/2020-12/schema" , "$id" : "https://example.com/schemas/order.json" , "type" : "object" , "required" : [ "id" , "items" , "price" , "createdAt" ] , "additionalProperties" : false , "properties" : { "id" : { "type" : "string" , "minLength" : 16 } , "items" : { "type" : "array" , "minItems" : 1 , "items" : { "type" : "object" , "required" : [ "sku" , "qty" ] , "properties" : { "sku" : { "type" : "string" } , "qty" : { "type" : "integer" , "minimum" : 1 } } , "additionalProperties" : false } } , "price" : { "type" : "number" , "minimum" : 0 } , "discount" : { "type" : "number" , "minimum" : 0 } , "couponCode" : { "type" : "string" } , "createdAt" : { "type" : "string" , "format" : "date-time" } } , "allOf" : [ { "if" : { "required" : [ "discount" ] } , "then" : { "required" : [ "couponCode" ] } } , { "if" : { "properties" : { "discount" : { "const" : 0 } } } , "then" : { "not" : { "required" : [ "couponCode" ] } } } ] }
几点说明:
additionalProperties: false
限制不允许未知字段 ;如果你要向前兼容,先放开,再逐步收紧。
format: "date-time"
是 RFC 3339 格式;不少校验库默认不强制 校验 format
,要显式开启(后文给具体库的开关)。(JSON Schema )
3. 多语言落地脚手架
3.1 Node.js(Ajv)
Ajv 支持到 2020-12/2019-09 草案,速度很快、生态成熟。(ajv.js.org , GitHub )
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 npm i ajv ajv-formats import Ajv from "ajv" ; import addFormats from "ajv-formats" ; import schema from "./order.schema.json" assert { type : "json" }; const ajv = new Ajv({ allErrors: true , // 一次性报全错,便于前端展示 strict: true , // 严格模式,避免歧义 removeAdditional: false , // 不要自动删除未知字段,先让问题显形 coerceTypes: false // 不要把 "123" 强转成数字,避免脏数据混进来 }); addFormats(ajv, { mode: "fast" }); // 开启 date-time 等 format 校验 const validate = ajv.compile(schema); export function assertValidOrder(data) { const ok = validate(data); if (!ok) { const msg = ajv.errorsText(validate.errors, { separator: "\n" }); const details = validate.errors?.map(e => ({ path: e.instancePath || "/" , keyword: e.keyword, message: e.message })); const err = new Error(msg); err.details = details; throw err; } return data; }
3.2 Java(networknt json-schema-validator)
轻量、无框,常配合 Jackson 使用;支持到 2020-12。建议缓存 JsonSchema
实例。(GitHub )
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 <dependency > <groupId > com.networknt</groupId > <artifactId > json-schema-validator</artifactId > <version > 1.5.8</version > </dependency > ObjectMapper mapper = new ObjectMapper(); JsonSchemaFactory factory = JsonSchemaFactory.getInstance(SpecVersion.VersionFlag.V202012); JsonSchema schema = factory.getSchema( YourClass.class.getResourceAsStream("/schemas/order.json")); public void assertValidOrder(JsonNode node) { Set<ValidationMessage > errors = schema.validate(node); if (!errors.isEmpty()) { // 组装友好的错误格式 throw new IllegalArgumentException(errors.toString()); } }
小提示(Java):金额用 BigDecimal
;Jackson 里可以 DeserializationFeature.USE_BIG_DECIMAL_FOR_FLOATS
,避免精度丢失。
文档/JavaDoc:(doc.networknt.com , javadoc.io )
3.3 Python(jsonschema / Pydantic)
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 from jsonschema import validate, Draft202012Validator, ValidationErrorimport jsonschema = json.load(open ("order.schema.json" )) Draft202012Validator.check_schema(schema) def assert_valid_order (data: dict ): try : validate(instance=data, schema=schema, cls=Draft202012Validator) except ValidationError as e: raise ValueError(f"{list (e.path)} : {e.message} " ) from e from typing import List , Optional from pydantic import BaseModel, Field, ValidationError, field_validatorclass Item (BaseModel ): sku: str qty: int = Field(ge=1 ) class Order (BaseModel ): id : str = Field(min_length=16 ) items: List [Item] price: float = Field(ge=0 ) discount: Optional [float ] = Field(default=None , ge=0 ) couponCode: Optional [str ] = None @field_validator("couponCode" ) def coupon_required_if_discount (cls, v, info ): d = info.data.get("discount" ) if d is not None and d > 0 and not v: raise ValueError("couponCode required when discount > 0" ) if d == 0 and v: raise ValueError("couponCode must be absent when discount == 0" ) return v def assert_valid_order_json (json_str: str ): try : return Order.model_validate_json(json_str) except ValidationError as e: raise ValueError(e) from e
4. 错误信息与用户体验:别只丢 “Bad Request”
好错误 应该至少包含:path
(出错位置)、expected
(期待什么)、actual
(实际是什么)、hint
(修正建议)。
比如:
1 2 3 4 5 6 7 8 { "code" : "VALIDATION_FAILED" , "errors" : [ { "path" : "/items/0/qty" , "expected" : "integer >= 1" , "actual" : 0 , "hint" : "若是删除请移除此项" } , { "path" : "/createdAt" , "expected" : "RFC3339 date-time" , "actual" : "2024/06/01 12:00" } ] , "requestId" : "a1b2c3" }
前端/工具里则把 path
聚合展示,配“定位到字段”的按钮,体验会好很多。
5. 常见坑
评论与尾逗号 :JSON 不允许 注释和尾逗号,别把 JSONC 当 JSON。标准里写得很清楚。(IETF Datatracker , Ecma International )
format
没生效 :不少库默认不校验 date-time
等 format
,像 Ajv 需要 ajv-formats
显式开启。(ajv.js.org )
additionalProperties
忘关 :上线后发现一堆“幽灵字段”随请求飘进来,导致下游落库失败/缓存 miss。
oneOf
多匹配 :oneOf
里两个分支都能匹配会报错;要么加判别字段(discriminator
),要么用更严格的约束。(JSON Schema )
精度问题 :金额类别用十进制;JS 端不要把金额当 number
参与计算,上下游统一单位与类型。
深层嵌套/超大 JSON :大于几十 MB 的 JSON 建议流式 校验(Jackson streaming、Node Transform 流),别一次全载入内存。
Schema 与代码漂移 :Schema 更新没同步到服务,最终“校验通过但代码崩”。解决:契约测试 + CI 校验 。
6. 性能与大文件:怎么才能减少 CPU 负载
编译 & 复用 :像 Ajv 会把 Schema 编译 成函数;Java 也要把 JsonSchema
缓存起来,别每次解析。(GitHub )
流式处理 :NDJSON(每行一个 JSON)可以一行一行读+校验;超大数组考虑分块校验。
采样 :低风险场景先按比例采样校验,观察错误率与类型,再决定是否全量拦截。
指标 :至少打两类指标:校验耗时 、失败率 (按 keyword
/path
维度分桶)。
7. 版本化与演进:老版本兼容处理
Schema 版本号 :$id
带版本,或路径分版本 /schemas/order/2025-08-01.json
。(JSON Schema )
加字段 :先非必填上线一版(提供默认值/后端兜底)→ 观察 → 再改成必填。
删字段 :先在 Schema 里标记弃用(文档+日志告警)→ 一段时间后再 additionalProperties: false
收口。
向前/向后兼容策略 :入口严格、出口宽松(对外响应更宽容,有助于灰度)。
8. 一个落地小清单
所有入口(API/Webhook/消息)统一加 Schema 校验
additionalProperties
有明确态度(要么关掉要么白名单)
format
显式开启并约定时区/格式
错误返回统一结构,包含 path/expected/actual/hint
CI:Schema 自检 + 契约测试(示例负样本 & 正样本)
观测:失败率、关键字分布、Top N 出错路径
Schema 版本化 & 变更公告
9. 延伸:Schema ≠ 业务全部
别把业务校验全塞进 Schema。像“库存是否足够”“用户是否有权限”这类需要上下文 的校验,应该放在服务逻辑里,并和 Schema 校验分层。
10. 参考与进一步阅读