JSON Schema 实战指南:校验、生成与前端应用技巧
为什么选 JSON Schema?
上个月搞一个动态表单配置后台,需求挺杂:用户要能自定义字段类型、校验规则、联动逻辑,甚至支持嵌套结构。一开始我用纯 JS 对象硬编码校验逻辑,结果三天后就改不动了——加个新字段就得改三处地方,还容易漏。
后来想到 JSON Schema,之前在 Swagger 里见过,但没实操过。查了下文档,发现它天生适合描述数据结构和校验规则,还能生成 UI(虽然我们没用那部分)。关键是:规则和 UI 解耦,前端只负责解析 schema 并渲染,后端也能用同一份 schema 做校验,省事。
核心代码就这几行
先装个主流库 ajv,然后写个简单的验证函数:
import Ajv from 'ajv';
const ajv = new Ajv({ allErrors: true });
function validateData(schema, data) {
const valid = ajv.validate(schema, data);
if (!valid) {
return ajv.errors.map(err => ({
field: err.instancePath || 'root',
message: err.message
}));
}
return null;
}
Schema 长这样(简化版):
{
"type": "object",
"properties": {
"email": {
"type": "string",
"format": "email",
"errorMessage": "请输入有效的邮箱"
},
"age": {
"type": "integer",
"minimum": 18,
"errorMessage": "年龄不能小于18岁"
}
},
"required": ["email"]
}
调用时直接传 schema 和用户输入的数据,返回错误列表。看起来挺清爽,对吧?
又踩坑了:自定义错误消息
问题来了:AJV 默认的错误消息是英文的,比如 must be string,客户肯定不认。官方文档说可以用 ajv-errors 插件,但折腾半天发现它要求每个字段必须加 errorMessage 属性,而且不支持中文标点(当时版本有 bug)。
后来我干脆自己写了个映射表,把 AJV 的 keyword 转成中文:
const ERROR_MAP = {
'type': '类型不正确',
'format': '格式无效',
'minimum': '值不能小于{minimum}',
'required': '该字段必填'
};
function formatError(err) {
const msg = ERROR_MAP[err.keyword] || err.message;
// 简单替换占位符,比如 {minimum} → 实际值
return msg.replace(/{(w+)}/g, (_, key) => err.params[key] || '');
}
这样至少能统一提示语。不过要注意,AJV 的 params 结构得看具体 keyword,比如 format 的 params 是 {format: 'email'},而 minimum 是 {comparison: '>=', limit: 18},得单独处理。这部分我偷懒没全做,只覆盖了常用字段,反正客户目前只用到这些。
最大的坑:性能问题
项目中期,产品突然加了个需求:表单字段可能多达 100+,而且要实时校验(用户输一个字就触发)。我一开始每次输入都重新跑整个 schema 校验,结果输入框一卡一卡的,Chrome DevTools 显示 validate 函数占了 80% 的 CPU。
查了 AJV 文档,发现它支持编译 schema 成校验函数缓存起来。赶紧改:
// 缓存 compiled validators
const validatorCache = new Map();
function getValidator(schema) {
const key = JSON.stringify(schema);
if (!validatorCache.has(key)) {
validatorCache.set(key, ajv.compile(schema));
}
return validatorCache.get(key);
}
function validateData(schema, data) {
const validate = getValidator(schema);
const valid = validate(data);
// ...后续处理
}
这下好多了,但还有问题:schema 本身很大(含嵌套对象、数组),JSON.stringify 在 key 生成时也耗时。后来改成用 hash 库生成 schema 的哈希值当 key,性能提升明显。不过 hash 计算也有开销,最终方案是:只在 schema 变更时重新编译,日常校验复用同一个 validate 函数。
另外,实时校验没必要全量跑。我加了个逻辑:只校验当前修改的字段及其依赖字段。比如改了省份,才校验城市下拉框。这部分靠业务层维护依赖关系,JSON Schema 本身不支持,但配合使用效果不错。
嵌套结构的校验头疼
项目里有个“联系人列表”字段,是数组,每个元素是个对象,包含姓名、电话等。Schema 写起来不难:
{
"type": "array",
"items": {
"type": "object",
"properties": {
"name": { "type": "string" },
"phone": { "type": "string", "pattern": "^1[3-9]\d{9}$" }
},
"required": ["name"]
}
}
但错误提示时,AJV 返回的 instancePath 是 /0/name 这种,前端得解析出索引 0 和字段 name,才能高亮对应输入框。我写了个工具函数拆分路径:
function parseInstancePath(path) {
if (!path) return [];
return path.split('/').slice(1).map(part => {
// 处理数字索引,比如 '0' → 0
return isNaN(part) ? part : parseInt(part, 10);
});
}
// 用法: parseInstancePath('/0/name') → [0, 'name']
这样就能递归定位到具体字段。不过如果嵌套更深(比如数组里再套对象),定位逻辑会更复杂,好在项目里最多两层,勉强应付过去了。
回顾与反思
整体来说,用 JSON Schema 是对的。它让校验逻辑集中管理,后端也能复用(他们用 Python 的 jsonschema 库),减少前后端不一致的问题。动态表单的扩展性也变好了,加新字段基本不用动前端代码。
但有几个地方没做到完美:
- 自定义错误消息还是有点糙,有些 edge case 没覆盖,比如联合类型(anyOf)的错误提示不太友好
- 实时校验的优化依赖业务层,如果 schema 本身有复杂依赖(比如 A 字段值影响 B 字段的 required 状态),还得手动处理,JSON Schema 本身不支持条件 required(虽然可以用
if/then,但写起来很啰嗦) - 调试 schema 时,AJV 的错误信息不够直观,经常得打日志看具体哪一行挂了
如果重来一次,可能会考虑用 Zod 或 Yup 这类更现代的 schema 库,它们对 TypeScript 支持更好,错误消息也更友好。不过 AJV 胜在轻量、标准兼容,适合我们这种需要前后端共用 schema 的场景。
最后的小建议
如果你也在搞动态表单,JSON Schema 值得试试,但别指望它解决所有问题。重点注意三点:
- 一定要缓存编译后的 validator,否则性能会崩
- 错误消息尽早统一处理,别等到 QA 提一堆“提示不友好”才改
- 复杂联动逻辑别硬塞进 schema,用 JS 单独处理更灵活
以上是我踩坑后的总结,希望对你有帮助。有更优的实现方式欢迎评论区交流,比如怎么优雅处理条件校验,或者有没有好用的错误消息本地化方案?

暂无评论