ARTICLE DETAIL

资讯详情

深耕网站视觉设计与运营推广的一线实战洞察。

用 Zod 做运行时数据校验:从安装到跑通订单校验场景的实操指南

用 Zod 做运行时数据校验:从安装到跑通订单校验场景的实操指南 用 Zod 做运行时数据校验从安装到跑通订单校验场景的实操指南【免费下载链接】zodTypeScript-first schema validation with static type inference项目地址: https://gitcode.com/GitHub_Trending/zo/zod上周一个线上事故的根因很无聊接口在某个字段上多返回了null前端代码按必非空直接解构页面白屏排查四十分钟后才定位到是类型只做了编译期声明、没有运行时校验。我用 Zod 把这类边界数据重新过了一遍——它是一个 TypeScript 优先的运行时 schema 校验库用几行代码声明数据结构.parse()既完成运行时校验又把z.infer推导出的静态类型直接喂给下游类型和校验规则从此不再漂移。项目定位与选型判断Zod 做的事很简单你把数据结构声明成 schema它负责验证输入 推导类型这两件事z.infer拿到的类型就是 schema 本身校验规则和类型定义不会分叉。它和同类库Joi、Yup、io-ts的核心差异有三点零第三方依赖。Zod 包本身没有任何 dependenciesJoi 的测试里甚至把 Zod 当作零依赖参照物来对比体积这在整个校验库圈子里是独一份。类型是推导出来的不是写出来的。z.infer等价于z.output输入侧类型还可以单独用z.input取transform、default这类会改变输出形态的 API 不会污染你的类型系统。不可变 API。.min()、.optional()、.extend()都返回新实例原 schema 不变在模块间共享 schema 不用担心被谁悄悄改了。什么场景值得用TypeScript 项目里所有数据从外部进来的边界——用户输入、HTTP 请求体、第三方 API 响应、配置文件、localStorage。这些地方的数据在编译期没有任何类型承诺Zod 在这里价值最大。什么场景不必用纯内部、完全由 TypeScript 构造的数据结构加一层 parse 是纯开销运行时本身就是 JavaScript 而不是 TypeScript 的项目它类型即产出的核心卖点用不上。另外如果你的代码库还在用 Zod v3 的旧习惯仓库保留了 v3 兼容路径zod/v3子入口升级可以分步做。五分钟上手先装依赖包管理器任选npm install zod # 或者 pnpm add zod # 或者 yarn add zod最小闭环示例覆盖定义 → 验证 → 拿到结果import * as z from zod; // 定义订单号校验规则非空字符串最长 32 位 const OrderNo z.string().min(1).max(32); // parse校验通过则返回强类型结果否则抛 ZodError const orderNo OrderNo.parse(SO-20260830-001); // safeParse不抛异常返回判别联合 { success, data | error } const result OrderNo.safeParse(12345); if (!result.success) { console.log(result.error.issues); // → [{ expected: string, code: invalid_type, path: [], message: Invalid input: expected string, received number }] }验证不通过时你会看到什么parse抛出的ZodError里err.issues是结构化数组每条 issue 带expected期望类型、code错误码、path出错位置的字段路径和message可读描述。path是数组形式所以嵌套字段会精确到[items, 1, quantity]这种深度直接定位到第 2 个商品的 quantity不需要你自己在错误里做字符串匹配。完整错误结构见 错误处理文档。核心能力拆解上面是单字段的字符串规则。真实业务里你会反复用到下面三类能力。对象 schema 与严格模式z.object把字段规则组合成结构体这是 Zod 里出现频率最高的 API。import * as z from zod; const Order z.object({ orderNo: z.string().min(1), // 订单号必传 amount: z.number().min(0), // 订单金额非负 remark: z.string().optional(), // 备注可选不传时为 undefined }); // 注意v4 默认是 non-strict未知字段会被静默丢弃 const data Order.parse({ orderNo: SO-001, amount: 99, debug: true }); // { orderNo: SO-001, amount: 99 } // debug 被丢掉了没有任何报错这里最容易踩的坑v3 的z.object默认是 strict 的v4 改成了宽松——多出来的字段不报错、直接被丢弃。如果你的业务需要多一个字段就拒绝比如对外 API 防脏数据要显式声明const StrictOrder Order.strict(); // 或从源头就用 const Order z.strictObject({ orderNo: z.string() });宽松行为方便接收只想要其中几个字段的外部数据但也意味着脏字段会无声消失。选型时想清楚你要的是过滤还是拒绝。对象相关的完整行为可以对照 对象校验测试 看里面覆盖了 pick/omit/extend 等所有组合操作。跨字段校验与错误格式化单字段规则之外业务里最常见的需求是两个字段之间要有关系——密码和确认密码一致、开始时间早于结束时间。这类规则放在对象层面用refine写const RegisterForm z.object({ username: z.string().min(3).max(20), email: z.string().email(), password: z.string().min(8), confirmPassword: z.string(), }) // 跨字段校验两次密码必须一致path 指定后错误会挂到 confirmPassword 字段下 .refine((data) data.password data.confirmPassword, { message: 两次密码输入不一致, path: [confirmPassword], });拿到ZodError之后如果你直接把它吐给前端大概率还要自己写一遍按 path 分组的逻辑。v4 内置了两个格式化函数按字段把 issues 展开成扁平结构和表单控件一一对应const result RegisterForm.safeParse(formValues); if (!result.success) { // 扁平化formErrors 是顶层错误fieldErrors 按字段名分组 const { formErrors, fieldErrors } z.flattenError(result.error); // fieldErrors.confirmPassword [两次密码输入不一致] }进阶用法嵌套结构对象套数组套对象用z.treeifyError()会得到镜像 schema 的树形结构比 flatten 更适合深层数据z.formatError()在 v4 已标记废弃别用。三者选型看 错误格式化文档。编解码与 AOT 编译前两节解决进得来、错得清。还有一件事 Zod 4 做得比较完整schema 不只是校验器还是双向转换器。v4.1 起任何 schema 都支持decode输入 → 输出和encode输出 → 输入典型例子是ISO 日期字符串 ↔ Date 对象const CreatedAt z.codec( z.iso.datetime(), // 输入侧ISO 字符串 z.date(), // 输出侧Date 对象 { decode: (iso) new Date(iso), // 入库前字符串转 Date encode: (date) date.toISOString(), // 返回前端Date 转字符串 } );z.infer拿到的永远是输出侧类型Date而z.input是输入侧类型string两个方向都不会类型错。如果你只关心性能而不关心双向转换v4 还有一个独立能力z.compile(schema)把 schema 提前编译成扁平、无循环的校验函数。仓库基准测试的口径是55 个 schema 的中位提速 2.4 倍20 个字段的大对象和对象数组能到 9 倍左右而裸z.string()几乎没有收益——它优化的是每节点派发和内存分配简单 schema 没有可省的东西。const CompiledOrder z.compile(Order); // 对热路径单独编译 CompiledOrder.parse(input); // 用法与 Order.parse 完全一致注意两个坑一是.refine()、.extend()这类派生方法返回的是未编译的新 schema要先派生完再 compile二是编译依赖new FunctionCSP 严格环境设置了z.config({ jitless: true })下全局编译会自动关闭此时z.compile是显式 opt-in。细节和基准代码在 compile 文档与 基准实现。一个完整场景走通把上面的能力串起来一个用户注册接口前端提交表单数据服务端要完成字段校验、跨字段校验、错误回显三件事并且入库的是强类型对象。需求拆解输入不可信任何字段都可能缺失或类型错乱——所以用safeParse而不是parse不能靠异常流处理业务错误密码强度至少 8 位、含数字和两次密码一致是两条独立的跨字段/单字段规则分别用refine表达错误要能按字段回显到表单用z.flattenError一步到位。import * as z from zod; const RegisterForm z.object({ username: z.string().min(3, 用户名至少 3 个字符).max(20), email: z.string().email(邮箱格式不正确), // 单字段 refine密码强度规则错误直接挂在 password 字段下 password: z.string().min(8).refine((v) /\d/.test(v), { message: 密码需包含至少一个数字, }), confirmPassword: z.string(), }).superRefine((data, ctx) { // superRefine用 ctx.addIssue 精确控制 path比 refine 的 path 选项更灵活 if (data.password ! data.confirmPassword) { ctx.addIssue({ code: custom, message: 两次密码输入不一致, path: [confirmPassword], }); } });服务端处理逻辑整个函数不超过 20 行function handleRegister(body: unknown) { // safeParse 而非 parse前端输入不可信用返回值分支处理不依赖 throw/catch const result RegisterForm.safeParse(body); if (!result.success) { const { fieldErrors } z.flattenError(result.error); // fieldErrors.username / fieldErrors.password ... 与表单控件一一对应 return { ok: false as const, fieldErrors }; } // 走到这里result.data 的类型就是 { username: string; email: string; ... } // 入库函数可以按强类型签名定义编译器会保证调用不出错 return { ok: true as const, user: insertUser(result.data) }; }// 对照合法输入与非法输入的两条路径 handleRegister({ username: 张三, email: zhangsanexample.com, password: Pass1234, confirmPassword: Pass1234, }); // { ok: true, user: {...} } handleRegister({ username: ab, email: bad, password: short, confirmPassword: other }); // { ok: false, fieldErrors: { username: [用户名至少 3 个字符], email: [邮箱格式不正确], ... } }两个关键决策点回顾一下safeParse是因为错误是业务正常分支而不是异常用判别联合比 try/catch 干净superRefinectx.addIssue是因为跨字段错误需要精确指定挂到哪个字段refine的path选项够用但表达力弱一档。如果这条接口在热路径上比如 QPS 很高的注册网关把z.compile(RegisterForm)放在模块加载时执行一次即可用法不变。你会被问到的问题Qparse和safeParse到底选哪个parse在失败时抛ZodErrorsafeParse返回{ success, data | error }判别联合。不可信输入用户、第三方 API用safeParse因为错误是常态分支不是异常内部可信数据用parse可以让不该发生的情况直接炸出来。Qz.infer和z.output有什么区别没有区别z.infer就是z.output的别名。需要区分的是输入侧类型z.input——经过transform、default、codec后输入输出类型会分叉这时候两个都要用。Qz.coerce.number()把空字符串转成了什么NaN。coerce本质是先强转再校验强转数字就是NaN而 Zod 的 number 规则默认拒绝NaN。如果你要接受空串显式.catch(0)或先判断别指望 coerce 帮你兜底。Qv4 为什么我多传了字段不报错v4 的 object 默认丢弃未知字段v3 是严格拒绝。要恢复拒绝行为用z.strictObject或.strict()中间态允许白名单外的字段但报错用.catchall(z.never())之类的组合。这是 v3→v4 迁移时最常撞到的行为差异。Qschema 里有异步 refine为什么parse拿不到值异步规则refine(async ...)必须配parseAsync/safeParseAsync同步版本会直接拒绝。safeParse的返回类型里也不会有异步数据的类型。延伸方向与下一步JSON Schema 互转toJSONSchema/fromJSONSchema内置可以用 Zod schema 反向生成 OpenAPI 文档或者把第三方给的 JSON Schema 直接变成可运行的校验器实现在 JSON Schema 生成器与 转换测试。错误消息国际化内置 50 语言 locale按 locales 目录 引入对应语言包即可错误文案随 locale 切换而不用维护第二套文案。性能与体积热路径上z.compile是正解基准见 compile-matrix如果在意 bundle 体积zod/mini子包提供函数式 API 的紧凑版本树摇测试在 treeshake 包。Zod 的能力边界到这里其实已经够用了schema 声明一处类型、校验、错误格式化、JSON Schema 全部从这一处推导。剩下的就是按数据边界逐个接口铺开。【免费下载链接】zodTypeScript-first schema validation with static type inference项目地址: https://gitcode.com/GitHub_Trending/zo/zod创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考
返回列表