用好 JSON Schema 提升前端数据校验效率

松静 交互 阅读 1,025
赞 40 收藏
二维码
手机扫码查看
反馈

这破表单校验又炸了,JSON Schema 救我狗命

昨天下午快下班的时候,产品甩过来一个需求:动态表单的字段要能根据配置实时变,还得带完整的校验规则。我心想这不就是 JSON Schema 的主场?结果高估了自己,低估了坑的深度,直接干到晚上十点。

用好 JSON Schema 提升前端数据校验效率

问题出在哪儿呢?我们有个通用表单组件,接收一个 schema 配置,然后动态渲染输入框。原本用的是手写的校验逻辑,加个 required 还行,但遇到嵌套对象、数组条件校验就崩了。比如“当用户选择‘公司类型’时,‘营业执照编号’必填”,这种逻辑写着写着手就废了。

后来试了下 ajv,这玩意儿是 JSON Schema 的主流校验器,性能也不错。但一开始根本没跑通——数据结构稍微复杂点,校验就直接跳过,也不报错,搞得我以为配置写错了。

折腾了半天发现:$ref 指向本地 schema 居然要预编译?

这里我踩了个巨坑。我定义了一个主 schema,里面引用了一个子 schema:

{
  "$schema": "http://json-schema.org/draft-07/schema#",
  "type": "object",
  "properties": {
    "userType": {
      "type": "string",
      "enum": ["personal", "company"]
    },
    "companyInfo": {
      "$ref": "#/definitions/company"
    }
  },
  "definitions": {
    "company": {
      "type": "object",
      "required": ["licenseId"],
      "properties": {
        "licenseId": {
          "type": "string",
          "minLength": 5
        }
      }
    }
  }
}

看起来没问题吧?但我用 AJV 校验的时候,companyInfo 根本不触发 required 校验。查文档查了快一个小时,最后才发现:AJV 默认不会自动解析内部的 $ref,尤其是当你的 schema 是运行时动态拼接的。

解决方法是得先“编译”这个 schema:

import Ajv from 'ajv';

const ajv = new Ajv({ allErrors: true });

// 先把 schema 编译成校验函数
const validate = ajv.compile(schema);

// 使用时传入数据
const data = { userType: 'company', companyInfo: {} };
const valid = validate(data);

if (!valid) {
  console.log(validate.errors); // 这里才会输出具体错误
}

关键点来了:你不能每次拿个新数据就重新 compile 一次,那性能直接爆炸。正确做法是把 compile 后的函数缓存起来,schema 不变就不重复编译。

动态切换校验规则?用 if-then-else 才是正道

回到那个“公司类型才需要执照”的需求。我最开始想用 JS 判断,后来发现 JSON Schema 原生支持条件校验!用 if/then 就能搞定:

{
  "type": "object",
  "properties": {
    "userType": {
      "type": "string",
      "enum": ["personal", "company"]
    },
    "licenseId": {
      "type": "string"
    }
  },
  "if": {
    "properties": { "userType": { "const": "company" } }
  },
  "then": {
    "required": ["licenseId"],
    "properties": {
      "licenseId": {
        "minLength": 5
      }
    }
  }
}

看到没?这才是声明式的力量。我不用手动写一堆 if (userType === ‘company’) … 校验 licenseId,全交给 schema 处理。前端代码瞬间干净了。

不过这里也有个小坑:AJV 默认不开启 if/then/else 支持,你得在构造时加上:

const ajv = new Ajv({
  allErrors: true,
  useDefaults: true,
  validateSchema: false, // 开发时可以关掉,避免 schema 自检报错
});

另外,allErrors: true 很重要,不然它只返回第一个错误,调试时根本不知道后面还有多少雷。

中文错误提示怎么搞?自定义 error formatter

默认的错误信息全是英文,比如 should have required property 'licenseId',用户看了直接懵。我写了段转换逻辑:

function getChineseErrorMessage(error) {
  const { keyword, dataPath, params } = error;

  switch (keyword) {
    case 'required':
      return ${dataPath.replace(/^./, '')} 是必填项;
    case 'minLength':
      return ${dataPath} 长度不能小于 ${params.limit};
    case 'format':
      if (params.format === 'email') return ${dataPath} 格式不正确;
      break;
    default:
      return 输入不符合规则:${dataPath};
  }
}

// 使用时
if (!validate(data)) {
  const messages = validate.errors.map(getChineseErrorMessage);
  console.error(messages); // 输出中文提示
}

这段代码现在还比较简陋,比如 dataPath.companyInfo.licenseId 这种路径,没法直接对应到中文字段名。我打算后续加个字段映射表,但现在先凑合用着,至少比英文强。

数组里的对象咋校验?items + additionalProperties 联合出击

还有一个场景是动态标签输入,每个标签有名称和颜色,要求名称不能为空:

{
  "type": "array",
  "items": {
    "type": "object",
    "required": ["name"],
    "properties": {
      "name": { "type": "string", "minLength": 1 },
      "color": { "type": "string", "default": "#ccc" }
    },
    "additionalProperties": false
  }
}

这里 additionalProperties: false 是为了防止用户乱加字段,毕竟这是受控表单。测试时发现如果不加这句,就算多传个 xxx: 123 也不会报错,太危险了。

另外,items 如果只是个 schema,表示数组里所有元素都按这个规则;如果是个数组,则可以为不同位置指定不同规则(不过我没用到)。

性能问题:别在 render 里 compile schema!

最开始我把 ajv.compile(schema) 放在 React 组件的 render 函数里,结果一输入就卡顿。React 一更新,重新 compile,CPU 直接拉满。

后来改成 useMemo 缓存:

import { useMemo } from 'react';

function useFormValidation(schema) {
  return useMemo(() => {
    try {
      return ajv.compile(schema);
    } catch (err) {
      console.error('Invalid schema:', err);
      return () => ({ valid: true, errors: null });
    }
  }, [schema]);
}

这样只有 schema 变了才会重新编译。顺便加了 try-catch,避免非法 schema 导致整个页面崩溃——之前就因为少了个逗号,页面白屏了三分钟。

现在的流程是这样的

  • 后端返回 JSON Schema 结构(通过 https://jztheme.com/api/form-schema
  • 前端用 fetch 拿到后存进 state
  • 用 useMemo 缓存 compile 后的校验函数
  • 表单 onChange 时调用 validate(data),收集错误并显示

整体跑起来了,虽然还有两个小毛病:

  1. 深层嵌套对象的错误路径解析还是不太友好,比如 .user.address.city 怎么映射到具体的 UI 字段还在想办法
  2. 某些特殊格式(比如身份证、手机号)得自定义 format,AJV 支持 addFormat 方法,但我还没完全接入

不过不影响主流程。最关键的是,以后加新字段、改校验规则,都不用动前端代码了,改 schema 就行。产品再来改需求,我至少能多活三天。

以上是我踩坑后的总结,如果你有更好的方案欢迎评论区交流

说实话,JSON Schema 学习曲线挺陡的,文档又多是英文,关键词搜半天。但一旦跑通,那种“规则即配置”的感觉真的很爽。我现在回头看手写校验的代码,简直像在写汇编。

如果有同学也在做动态表单,真心建议早点上 JSON Schema + AJV,晚用早享受。唯一提醒:别学我一开始把 compile 放 render 里,那真能让你被同事背地里骂一年。

本文章不代表JZTHEME立场,仅为作者个人观点 / 研究心得 / 经验分享,旨在交流探讨,供读者参考。
发表评论
秀花的笔记
收藏起来慢慢看,以后遇到相关问题就能快速查阅了。
点赞
2026-03-29 17:25