为什么要在边界校验
信任边界处校验一次,比在每个函数里到处判空更省心。JSON Schema 把「这个字段必须存在、必须是数字、范围是多少」写成一份可机器校验的契约。
常用关键字
| 关键字 | 作用 |
|---|---|
| required | 必填字段 |
| type | 基础类型(string/number/object/array…) |
| format | 语义格式(email/date-time/uuid) |
| minimum / maximum | 数值范围 |
| pattern | 正则约束 |
两个坑
- Schema 不是安全边界:它挡不住业务逻辑漏洞和注入,SQL 注入仍要靠参数化查询;
- 别只校验类型:字符串长度、数组上限也要限,否则可能撑爆存储或内存。
动手试试
格式化与检查 JSON:JSON 格式化。
实战案例:三个 schema 没拦住的脏数据
- 只校验类型、没校验格式:
type: string拦不住空串或错误邮箱,应补format、minLength与pattern。 - 放过了未声明字段:默认允许额外字段,客户端多传的内容会被静默写库。设
additionalProperties: false能尽早暴露契约不一致。 - 共享 schema 而没有版本:复用
$ref的 schema 一改,所有依赖方同时受影响。给 schema 加版本号,变更走兼容评审。
常见问题(FAQ)
JSON Schema 能替代业务校验吗?不能,它只管结构;跨字段规则(如结束时间必须晚于开始时间)仍要业务代码。校验失败怎么返回更好?返回字段路径与原因列表,便于前端定位,别只给一句“参数错误”。前端校验就够了吗?不够,前端校验只是体验优化,服务端必须独立校验。oneOf 和 anyOf 怎么选?需要知道“命中了哪个分支”时用 oneOf,纯结构约束用 anyOf 更宽松。
分层落地:在哪一层校验
同一份 schema 可以复用在多个位置,但职责不同:
- 网关 / 边缘:只做粗校验(体积、必填、显而易见的类型错误),尽早拒掉恶意流量;
- 服务入口:做完整的结构校验并返回字段级错误,这是唯一必须严格执行的一层;
- 数据库前:依赖约束(非空、唯一、长度)作为最后一道防线,防止绕过应用写入的脏数据;
- 前端:复用同一份 schema 做即时提示,提升体验但不作为安全边界。
版本化与兼容演进
- 加字段:新增可选字段是兼容变更;若字段必填,先给默认值再逐步收紧;
- 改类型 / 改名:属于破坏性变更,应新版本并行,并在旧版本返回弃用告警;
- 共享 schema 要版本号:把 schema 当作接口契约的一部分随接口一起版本化,避免一处修改影响所有依赖方;
- 用代码生成而不是手抄:由 schema 生成 TS 类型或校验代码,避免文档、类型与实现三处不一致。
常见误区
- 把 schema 当业务规则引擎:跨字段逻辑与外部查询仍需代码;
- 只校验请求不校验响应:上游返回的数据同样可能不符合预期,关键依赖应校验响应以防脏数据扩散;
- 错误提示不定位:只返回“参数错误”会让调用方反复试错,应返回字段路径与原因列表。
常用关键字速查
- 类型与范围:
type、enum、minimum/maximum、minLength/maxLength; - 结构:
properties、required、additionalProperties、items、minItems; - 组合:
allOf全部满足、anyOf任一满足、oneOf恰好一个满足、not取反; - 条件:
if/then/else表达“当某字段存在时必须同时满足另一约束”; - 复用:
$defs配$ref定义公共片段,避免复制粘贴造成漂移。
性能注意点
大规模校验时注意两点:一是把编译后的校验器缓存起来,避免每次请求都重新编译 schema;二是避免过深的 oneOf 组合,它们可能导致组合爆炸。对超高吞吐路径,可先做轻量字段检查,仅在必要时才走完整校验。
与类型系统的配合
校验 schema 与静态类型是两件事:类型在编译期防止“写错”,schema 在运行期防止“收到错”。两者都应由同一份定义生成,否则会出现类型声明允许、运行时却拒绝的割裂。定期用真实流量样本跑一遍校验,能发现声明与现实的偏差。