很多朋友把“能被 JSON.parse 成对象”当成“数据没问题”。但是一到线上:一个小小的 null、一个多余字段、一个写错格式的时间串,就能产生严重的问题。JSON 校验 做的除了能不能解析,更重要的是要验证是“结构对不对、值合不合理、能不能放心往下游传 ”。
下面,我尽量把落地要点、常见坑和多语言方案一次说清,配一套可复制的脚手架。
语法 :是不是合法 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 )
语法过关 ≠ 有效数据 。校验的职责是把“输入不确定性”挡在系统边界之外。
 
以一个订单片段为例(可直接拷贝到你的项目里):
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 ) 
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; } 
轻量、无框,常配合 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 )
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 
好错误 应该至少包含: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 聚合展示,配“定位到字段”的按钮,体验会好很多。
评论与尾逗号 :JSON 不允许 注释和尾逗号,别把 JSONC 当 JSON。标准里写得很清楚。(IETF Datatracker , Ecma International )format 没生效date-time 等 format,像 Ajv 需要 ajv-formats 显式开启。(ajv.js.org )additionalProperties 忘关oneOf 多匹配oneOf 里两个分支都能匹配会报错;要么加判别字段(discriminator),要么用更严格的约束。(JSON Schema )精度问题 :金额类别用十进制;JS 端不要把金额当 number 参与计算,上下游统一单位与类型。深层嵌套/超大 JSON :大于几十 MB 的 JSON 建议流式 校验(Jackson streaming、Node Transform 流),别一次全载入内存。Schema 与代码漂移 :Schema 更新没同步到服务,最终“校验通过但代码崩”。解决:契约测试 + CI 校验 。 
编译 & 复用 :像 Ajv 会把 Schema 编译 成函数;Java 也要把 JsonSchema 缓存起来,别每次解析。(GitHub )流式处理 :NDJSON(每行一个 JSON)可以一行一行读+校验;超大数组考虑分块校验。采样 :低风险场景先按比例采样校验,观察错误率与类型,再决定是否全量拦截。指标 :至少打两类指标:校验耗时 、失败率 (按 keyword/path 维度分桶)。 
Schema 版本号 :$id 带版本,或路径分版本 /schemas/order/2025-08-01.json。(JSON Schema )加字段 :先非必填上线一版(提供默认值/后端兜底)→ 观察 → 再改成必填。删字段 :先在 Schema 里标记弃用(文档+日志告警)→ 一段时间后再 additionalProperties: false 收口。向前/向后兼容策略 :入口严格、出口宽松(对外响应更宽容,有助于灰度)。 
所有入口(API/Webhook/消息)统一加 Schema 校验  
additionalProperties 有明确态度(要么关掉要么白名单)format 显式开启并约定时区/格式错误返回统一结构,包含 path/expected/actual/hint 
CI:Schema 自检 + 契约测试(示例负样本 & 正样本) 
观测:失败率、关键字分布、Top N 出错路径 
Schema 版本化 & 变更公告 
 
别把业务校验全塞进 Schema。像“库存是否足够”“用户是否有权限”这类需要上下文 的校验,应该放在服务逻辑里,并和 Schema 校验分层。