Zod 数据验证实践:从订单字段到接口解析,三个场景完成上手
【免费下载链接】zodTypeScript-first schema validation with static type inference项目地址: https://gitcode.com/GitHub_Trending/zo/zod
周五晚上的一次线上告警:聊天消息的"回复关系"偶发丢失。排查后发现,后端悄悄把replyTo字段从字符串换成了数字,前端一直按字符串处理,问题暴露时脏数据已经流了半天。这类"接口契约与前端假设不一致"的坑,正是 TypeScript 数据验证最典型的用武之地:在数据进入业务的边界处,先用 Zod 按 schema 校验一遍,不合规则直接拦下,而不是让坏值一路渗透到深处。
为什么 Zod 值得进项目
一句话:schema 只写一份,类型由z.infer从 schema 推断出来——校验规则和类型同源,不用人工保持同步;运行时零依赖,打包体积基本可以忽略。
三十秒装好:先分清两种"过一遍"
这一节解决什么问题:数据进来之后,校验失败时到底该抛错还是该返回结果?先装:
npm install zodimport { z } from "zod"; const schema = z.object({ port: z.number().int() }); // 失败直接抛 ZodError,适合"必须对"的内部环节 const ok = schema.parse({ port: 8080 }); // 失败返回结果对象,方便取错误信息做展示 const res = schema.safeParse({ port: "8080" }); if (!res.success) { console.log(res.error.issues[0].message); }区分口径很朴素:自己构造的数据用parse,来源不受控的数据用safeParse,把异常收敛在边界处。
官方文档里用这张图描述数据流向:未知输入穿过parse变成带类型的输出;图中decode/encode双向流是 v4 的 codec 模型,入门阶段知道有这条路径即可。
场景一:电商订单的嵌套结构与默认值
这一节解决什么问题:上游网关推来的订单,结构有嵌套、有可缺项,业务代码不能每处都手写?? []。
const orderSchema = z.object({ orderNo: z.string().min(1), amount: z.number().nonnegative(), // 金额不允许为负 items: z.array(z.object({ sku: z.string(), quantity: z.number().int().min(1), })).min(1), shipTo: z.object({ name: z.string(), phone: z.string(), city: z.string().optional(), // 允许缺,传了必须是字符串 }), notes: z.string().optional(), tags: z.array(z.string()).default([]), // 缺省时兜底空数组 }); type Order = z.infer<typeof orderSchema>; // 用 schema 锁类型为什么这么写:optional只表示"字段可以不出现",传进来之后仍是原始类型;default则由 Zod 主动补值,下游可以默认字段必在。把这两种语义在 schema 里分开表达,比在业务代码里到处补兜底更稳。
场景二:聊天消息的跨字段校验
这一节解决什么问题:单字段规则都过了,但"回复类消息却没有指向原消息"这种跨字段矛盾,schema 得会自己抓出来。
const messageSchema = z.object({ id: z.string(), type: z.enum(["text", "reply", "system"]), content: z.string().max(2000), createdAt: z.number(), // 毫秒时间戳 replyTo: z.string().optional(), // 原消息 id }).refine( (m) => m.type !== "reply" || Boolean(m.replyTo), { message: "reply 类型消息必须指定原消息", path: ["replyTo"] } );注意这里:能写在字段上的规则就写在字段上,refine只留给需要"看一眼别的字段"的场景;path决定错误挂在哪个字段下,前端定位问题时更直观。
场景三:配置解析与强制转换
这一节解决什么问题:.env或配置接口里一切值都是字符串,"8080"不该在业务里再被Number()一遍。
const configSchema = z.object({ PORT: z.coerce.number().int().min(1).max(65535), // 字符串强转为数字 LOG_LEVEL: z.enum(["info", "warn", "error"]).default("info"), EXPIRES: z.coerce.date().optional(), // 日期字符串转 Date }); const cfg = configSchema.parse({ PORT: "8080", EXPIRES: "2026-12-31" }); console.log(typeof cfg.PORT, cfg.EXPIRES instanceof Date);coerce的定位是"值本身没问题,只是类型不对"——配置解析、表单输入正是这种场景的日常,转换和校验在同一步完成。
新手避坑清单
这一节解决什么问题:前三个场景都跑通之后,下面几处仍是最容易翻车的地方。
- parse / safeParse 混用:在错误处理路径上用
parse抛错,又在上层catch后吞掉,错误信息就丢了。来源不受控的数据一律safeParse。 - 忘了
z.infer锁类型:schema 改了一行,手写的类型定义没跟上,两边悄悄分叉。类型永远从 schema 推。 - 把 optional 当 nullable:
optional只表示字段可缺;接口若可能显式传null,需要.nullable()或.nullish(),别指望optional替你接住null。 z.coerce.boolean()的"陷阱":v4 按真值判断,"false"会解析成true,只有空串、0、undefined得false。配置开关建议写z.enum(["true", "false"])再自己转。
下一步可以做什么
这一节给三条最短的继续路径:
- 把场景二里的
type字段改用z.discriminatedUnion重写,体会按判别字段分流各子类型 - 翻一翻 v4 classic 测试目录,每种写法的官方样例都在里面
- 需要与外部工具对接时,查看 schema 的 JSON Schema 转换能力;若维护 v3 风格代码,可对照 v3 中文文档
【免费下载链接】zodTypeScript-first schema validation with static type inference项目地址: https://gitcode.com/GitHub_Trending/zo/zod
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考