JSON Schema 实战指南:校验、生成与前端应用技巧

长孙振莉 交互 阅读 1,072
赞 20 收藏
二维码
手机扫码查看
反馈

为什么选 JSON Schema?

上个月搞一个动态表单配置后台,需求挺杂:用户要能自定义字段类型、校验规则、联动逻辑,甚至支持嵌套结构。一开始我用纯 JS 对象硬编码校验逻辑,结果三天后就改不动了——加个新字段就得改三处地方,还容易漏。

JSON Schema 实战指南:校验、生成与前端应用技巧

后来想到 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 单独处理更灵活

以上是我踩坑后的总结,希望对你有帮助。有更优的实现方式欢迎评论区交流,比如怎么优雅处理条件校验,或者有没有好用的错误消息本地化方案?

本文章不代表JZTHEME立场,仅为作者个人观点 / 研究心得 / 经验分享,旨在交流探讨,供读者参考。
发表评论

暂无评论