Zod 是什么
要点
- 假设你在写一个 AI 聊天接口,前端会把用户的输入 POST 过来,你在后端把它交给 LLM
- TypeScript 的类型只在编译时有效
- Zod 是一个用 TypeScript 写的 Schema 校验库,它让你用一份定义同时完成「运行时校验」和「静态类型推导」
- 光说概念太虚,我们看一段最小可运行的例子
- 在普通后端里,Zod 已经很香
内容
1. 先从一个真实的坑说起
假设你在写一个 AI 聊天接口,前端会把用户的输入 POST 过来,你在后端把它交给 LLM。
最朴素的写法长这样:
// index.ts
app.post('/chat', async (c) => {
const body = await c.req.json()
const reply = await callLLM(body.prompt, body.temperature)
return c.json({ reply })
})只要跑起来没报错,看上去很美好。但你只要上线一段时间,就会陆续撞上这些问题:
- 前端漏传
prompt,body.prompt是undefined,LLM 报错 - 前端把
temperature传成了字符串"0.7",模型调用 SDK 直接抛错 - 某个客户端恶意传了一个 50 万字的
prompt,Token 账单瞬间爆掉 - 另一个前端把字段名打错成
promt,你排查了两小时
如果你是从前端转过来的,你可能会下意识地觉得:
NOTE
「我写了 TypeScript 啊,body.prompt 类型不就是 string 吗?」
这是新手最常见的一个错觉。我们来把这层误会讲清楚。
2. TypeScript 的类型在运行时是不存在的
TypeScript 的类型只在编译时有效。代码一旦被编译成 JavaScript,所有类型信息会被完全抹掉。
也就是说,你写的这段代码:
// index.ts
interface ChatBody {
prompt: string
temperature: number
}
app.post('/chat', async (c) => {
const body = await c.req.json() as ChatBody
// 这里 TypeScript 以为 body.prompt 是 string
})只是在骗编辑器开心。实际运行时,c.req.json() 返回的是一个普通的 JS 对象,里面是什么字段、是什么类型,完全由 HTTP 请求决定。如果前端传了 { foo: 1 },body.prompt 就是 undefined,body.temperature 同理。
这里有一个新手一定要记住的结论:
NOTE
TypeScript 负责的是「你写代码时看到的类型」,运行时请求进来的数据长什么样,TypeScript 一无所知。
所以,任何「从外部世界进来的数据」——HTTP 请求、localStorage、第三方 API、LLM 返回的 JSON——都需要一个运行时的守门员来检查它是不是真的符合你预期的结构。
这个守门员,就是 Zod 要做的事。
3. Zod 是什么
一句话定义:
NOTE
Zod 是一个用 TypeScript 写的 Schema 校验库,它让你用一份定义同时完成「运行时校验」和「静态类型推导」。
这句话有三个关键词,我们一个个拆开。
3.1 Schema 是什么
Schema 就是一份「数据规则说明书」。它描述的不是数据本身,而是「合格数据应该长什么样」。
对应到前面的聊天接口,用 Zod 写 schema 大概是这样:
// index.ts
import { z } from 'zod'
const ChatSchema = z.object({
prompt: z.string().min(1).max(4000),
temperature: z.number().min(0).max(2),
})读起来几乎就是自然语言:
- 请求体应该是一个对象
prompt必须是字符串,长度 1~4000temperature必须是数字,取值 0~2
3.2 运行时校验
有了 schema,你就可以让它在运行时真的去检查数据:
// index.ts
const body = await c.req.json()
const data = ChatSchema.parse(body)
// 到这一行,data 里的字段一定符合 schema 描述
// 否则 parse 会抛错,根本不会执行到这一行这和 TypeScript 不同。TypeScript 只能保证「你写代码时没写错」,Zod 保证「运行时进来的数据没错」。
3.3 静态类型推导
Zod 还做了一件让新手经常惊叹的事:schema 可以直接反推出 TypeScript 类型。
// index.ts
type ChatBody = z.infer<typeof ChatSchema>
// 等价于:
// type ChatBody = {
// prompt: string
// temperature: number
// }你不再需要「手写一份 interface + 手写一份校验规则」这种重复劳动。一份 schema,一边给校验器用,一边给 TypeScript 用。
这是 Zod 最核心的卖点。请你先把它记下来:
NOTE
Schema 是单一真实来源(Single Source of Truth),校验和类型都从它派生。
4. 一个最小可运行的例子
光说概念太虚,我们看一段最小可运行的例子。
// index.ts
import { z } from 'zod'
// 1. 声明 schema
const UserSchema = z.object({
name: z.string().min(1),
age: z.number().int().min(0),
email: z.string().email(),
})
// 2. 推导 TS 类型
type User = z.infer<typeof UserSchema>
// 3. 校验一个合法数据
const ok = UserSchema.parse({
name: 'Alice',
age: 18,
email: '[email protected]',
})
// ok 的类型是 User,且数据一定合法
// 4. 校验一个非法数据
try {
UserSchema.parse({
name: '',
age: -1,
email: 'not-an-email',
})
} catch (err) {
// err 是 ZodError,里面有详细的错误信息
console.log(err)
}你可以把它理解成:
- 没有 Zod 时,你得写一堆
if (!body.name || typeof body.name !== 'string') ... - 有了 Zod,你只要声明「合格数据长什么样」,剩下交给它
5. 为什么 AI 项目特别需要 Zod
在普通后端里,Zod 已经很香。到了 AI 项目,它的价值会再放大一层。因为 AI 项目里「不可信数据」特别多:
- 用户输入:prompt、上下文、工具参数
- LLM 返回:模型输出的 JSON 经常缺字段、多字段、字段名大小写不对
- 工具调用:Agent 给工具传的参数,常常和你声明的 schema 对不上
- 环境变量:API Key、模型名、温度上限,这些运行时值只要一错全盘崩
- RAG 检索元数据:向量库返回的 payload 结构可能在某次迁移后悄悄变了
这些地方都有一个共同特点:你以为是什么结构,和它实际是什么结构,经常不是一回事。
Zod 在这些场景下的作用是:
- 在数据跨边界时统一加一道校验
- 从同一份 schema 生成 TypeScript 类型
- 校验失败能拿到结构化错误,方便记录日志、返回给前端
- 配合
transform还能做类型安全的「脏数据清洗」
后面的章节会围绕这些场景一个个展开。你现在只需要记住:
NOTE
AI 项目里的不可信数据远比传统后端多,Zod 不是锦上添花,而是刚需。
6. 和其他方案的对比
为了给你一个更完整的心智模型,这里简单对比一下常见方案。你可能没听过 io-ts / Joi / Yup,没关系,这里只是让你看到 Zod 在 TS 生态中的坐标,不用深究每一个。
| 手写校验 | Joi / Yup | io-ts | Zod | |
|---|---|---|---|---|
| 运行时校验 | ✅ | ✅ | ✅ | ✅ |
| 自动推导 TS 类型 | ❌ | 有限 | ✅ | ✅ |
| 错误信息可读性 | 取决于自己写 | 一般 | 较弱 | 很好 |
| TypeScript 一等支持 | 无所谓 | 一般 | 强但心智重 | 强且直观 |
| 生态(Hono、RHF、LLM) | 无 | 部分 | 少 | 最广 |
| 学习曲线 | 极低(但越写越痛) | 中 | 陡 | 平缓 |
如果你不是为了维护老项目,新项目直接选 Zod 基本不会错。尤其是它和 Hono、React Hook Form、OpenAI Structured Output 的集成,都是官方或社区一等方案。
7. 安装与版本说明
// index.bash
yarn add zod截至 2026-04,Zod v4 已稳定发布一段时间,但本专栏默认使用 Zod v3(项目依赖锁定在 ^3.24.1)的主流 API。v4 与 v3 大体兼容,主要差异(如 z.string().email() 改为顶层 z.email()、错误格式化 API 的变化等)会在后续专题里单独讲。你现在先把 v3 的心智打牢,之后迁到 v4 成本很小。
安装完可以写一段最小代码验证一下:
// index.ts
import { z } from 'zod'
const schema = z.string().min(3)
console.log(schema.parse('hello')) // 'hello'
console.log(schema.safeParse('hello')) // { success: true, data: 'hello' }
console.log(schema.safeParse('a')) // { success: false, error: ZodError }能跑通就说明安装没问题。
8. 总结
这一篇我们没有急着讲具体语法,而是先把三件事讲清楚:
- 运行时数据不可信:TypeScript 的类型在运行时是不存在的,外部数据必须有一道运行时校验
- Zod 的定位:用一份 schema 同时完成「运行时校验」和「静态类型推导」,避免重复定义
- AI 项目为什么更需要 Zod:用户输入、LLM 输出、工具调用、环境变量、RAG 元数据,这些边界点全都不可信
如果让你用一句话记住 Zod,可以是这样:
NOTE
Zod 让 TypeScript 的类型不再「只停留在编译时」,而是一路延伸到运行时的每一个数据入口。
下一篇我们会专门展开「单一真实来源」这条原则,看看它在真实项目里是怎么把校验、类型、文档、接口契约串成一条链路的。