AI 电子伴侣
创建时间: 2026-04-18最后更新: 2026-04-18

1. 写你的第一个 Zod Schema

前两篇都在讲道理,这一篇开始动手。

Zod 里最小的那个例子是这样的:

index.ts
1
import { z } from 'zod'
2
3
const schema = z.string()

这一行代码做的事情,用大白话翻译就是:

NOTE

「我声明一个规则,这个规则要求数据必须是字符串。」

它就是一个 schema。还没真正校验任何东西,只是一份规则说明书

要让这份说明书真的去工作,你需要调用它的两个核心方法之一:parse 或者 safeParse

这两个方法是 Zod 里最高频的 API,几乎所有校验场景都离不开它们。我们一个个来看。

2. parse:严格模式,失败就抛错

parse 的行为非常直白:

  • 数据合法 → 返回处理后的数据
  • 数据不合法 → 抛出 ZodError
index.ts
01
import { z } from 'zod'
02
03
const schema = z.string()
04
05
// 合法:返回 'hello'
06
const a = schema.parse('hello')
07
console.log(a) // 'hello'
08
09
// 不合法:抛出 ZodError
10
const b = schema.parse(123)
11
// 💥 这一行会抛错,后面的代码根本执行不到

你可以把 parse 想成机场安检的「严格通道」:数据能过,就原样放行;过不了,直接报警,后面的流程都停掉。

2.1 parse 之后的数据是「可信的」

这里有一个新手特别容易忽视的细节:parse 返回的数据,TypeScript 会给它一个准确的类型。

index.ts
1
const schema = z.string()
2
3
const a = schema.parse(someUnknownValue)
4
// a 的类型是 string,哪怕 someUnknownValue 是 unknown

也就是说,parse 不仅做运行时校验,还顺手帮你把 unknown 类型「窄化」成了具体类型。

这是为什么前面说 Zod 把 TypeScript 的类型一路延伸到运行时入口:外部世界进来时是 unknown,经过 parse 之后就是一个你可以放心使用的具体类型。

2.2 什么时候适合用 parse

parse 适合一个典型场景:你非常确信数据应该是合法的,不合法就是异常情况。

比如:

  • 解析你自己写进数据库、刚刚读出来的配置
  • 解析一个你自己定义的、从受信环境来的 JSON
  • Hono / Express 里,通常让框架帮你抓 parse 抛出的错误,然后统一返回 400

换句话说:你希望「不合法」走异常分支,而不是走业务分支时,用 parse

3. safeParse:安全模式,永远不抛错

safeParse 的设计刚好相反。它永远不抛错,而是返回一个「结果对象」:

index.ts
01
const schema = z.string()
02
03
const result = schema.safeParse('hello')
04
05
if (result.success) {
06
// 成功分支
07
console.log(result.data) // 'hello',类型是 string
08
} else {
09
// 失败分支
10
console.log(result.error) // ZodError 实例
11
}

safeParse 的返回值是一个可辨识联合(discriminated union),靠 success 字段区分:

success字段类型
trueresult.data你期望的类型
falseresult.errorZodError

TypeScript 会根据你是否判断了 result.success,自动给你正确的类型。这就是为什么上面代码里 result.data 被识别成 string

3.1 什么时候适合用 safeParse

safeParse 适合所有希望把失败当作一个正常业务分支来处理的场景,比如:

  • 表单校验:字段错了要显示错误提示,而不是抛异常
  • LLM 返回的 JSON 校验:模型返回脏数据是家常便饭,不应该当成崩溃事件
  • 用户输入解析:一定有错,要给用户友好提示
  • 定时任务里批量处理数据:一条脏数据不应该让整批任务挂掉

用一句话概括:

NOTE

「失败是预期之内的事」 → 用 safeParse;「失败是异常情况」 → 用 parse

4. parse vs safeParse:决策地图

新手最常见的一个误用是:无脑用 parse 再包一层 try/catch

index.ts
01
// ❌ 别这么写
02
try {
03
const data = schema.parse(input)
04
// 处理 data
05
} catch (err) {
06
// 处理错误
07
}
08
09
// ✅ 这时候应该用 safeParse
10
const result = schema.safeParse(input)
11
if (!result.success) {
12
// 处理错误
13
return
14
}
15
// 处理 result.data

两种写法运行时表现差不多,但代码风格完全不同

  • try/catch 适合「异常控制流」——少见、意外的情况
  • if (!result.success) 适合「普通控制流」——预期内的分支

一个简单的判断标准:

场景选择
数据理论上一定合法,不合法就是 bugparse
数据经常会不合法,要给出反馈safeParse
框架(如 Hono)会自动捕获异常并返回 400parse(交给框架处理)
要把错误信息展示给用户safeParse(方便拿到 error)

5. 看懂 ZodError

无论用哪个方法,失败的时候你都会拿到一个 ZodError。读懂它是 Zod 的基本功。

我们用一个稍微复杂的 schema 触发一个错误:

index.ts
01
const UserSchema = z.object({
02
name: z.string().min(1),
03
age: z.number().int().min(0),
04
email: z.string().email(),
05
})
06
07
const result = UserSchema.safeParse({
08
name: '',
09
age: -1,
10
email: 'not-an-email',
11
})
12
13
if (!result.success) {
14
console.log(result.error.issues)
15
}

打印出来大概长这样(精简过):

index.json
01
[
02
{
03
"code": "too_small",
04
"minimum": 1,
05
"type": "string",
06
"path": ["name"],
07
"message": "String must contain at least 1 character(s)"
08
},
09
{
10
"code": "too_small",
11
"minimum": 0,
12
"type": "number",
13
"path": ["age"],
14
"message": "Number must be greater than or equal to 0"
15
},
16
{
17
"code": "invalid_string",
18
"validation": "email",
19
"path": ["email"],
20
"message": "Invalid email"
21
}
22
]

这个 issues 数组就是 ZodError 的核心。每一条都带这几个最常用的字段:

字段含义
path出错字段的路径,例如 ['user', 'address', 'zip']
code错误类型,例如 too_small / invalid_type / invalid_string
message人类可读的错误信息(默认英文,后面章节会讲本地化)

5.1 给前端返回结构化错误

在 API 场景里,一个常见模式是把 issues 直接返回给前端,让前端能逐字段显示:

index.ts
01
app.post('/register', async (c) => {
02
const result = RegisterSchema.safeParse(await c.req.json())
03
if (!result.success) {
04
return c.json({
05
ok: false,
06
errors: result.error.issues.map(issue => ({
07
field: issue.path.join('.'),
08
message: issue.message,
09
})),
10
}, 400)
11
}
12
// 合法数据
13
return c.json({ ok: true })
14
})

前端拿到的结构是这样的,可以直接喂给表单组件:

index.json
1
{
2
"ok": false,
3
"errors": [
4
{ "field": "name", "message": "String must contain at least 1 character(s)" },
5
{ "field": "email", "message": "Invalid email" }
6
]
7
}

5.2 快速格式化:flatten 和 format

如果你不想手动遍历 issues,Zod 提供了两个常用的格式化方法:

index.ts
01
if (!result.success) {
02
// flatten:按字段聚合,适合表单
03
console.log(result.error.flatten())
04
// {
05
// formErrors: [],
06
// fieldErrors: {
07
// name: ['String must contain at least 1 character(s)'],
08
// email: ['Invalid email'],
09
// }
10
// }
11
12
// format:保留嵌套结构,适合嵌套对象
13
console.log(result.error.format())
14
}

新手选型建议:

  • 扁平结构(普通表单)→ flatten()
  • 嵌套结构(对象里套对象)→ format()
  • 要最大灵活性 → 直接用 issues

6. 异步版本:parseAsync 与 safeParseAsync

有些校验规则是异步的(比如「用户名是否已被占用」要查数据库)。这种场景下必须用异步版本:

index.ts
01
const UsernameSchema = z.string().refine(
02
async (name) => {
03
const exists = await db.user.findFirst({ where: { name } })
04
return !exists
05
},
06
{ message: '用户名已被占用' }
07
)
08
09
// 同步版本会直接抛错:含异步校验,必须用 async 版本
10
const ok = await UsernameSchema.parseAsync('alice')
11
const result = await UsernameSchema.safeParseAsync('alice')

规则也很简单:

NOTE

schema 里有任何异步逻辑(refine 返回 Promise、transform 是 async),就必须用 parseAsync / safeParseAsync

同步的 parse / safeParse 遇到异步校验会直接报错提醒你。

7. 动手练一下

看完这一篇,建议你花 5 分钟在本地跑一下下面三段代码,观察输出:

index.ts
01
import { z } from 'zod'
02
03
// 练习 1:用 parse 校验一个数字
04
const NumSchema = z.number().int().min(0)
05
console.log(NumSchema.parse(42))
06
// console.log(NumSchema.parse('42')) // 放开这行看会发生什么
07
08
// 练习 2:用 safeParse 校验一个邮箱
09
const EmailSchema = z.string().email()
10
const r = EmailSchema.safeParse('not-an-email')
11
console.log(r.success ? r.data : r.error.issues)
12
13
// 练习 3:校验一个对象,拿 flatten 输出
14
const PersonSchema = z.object({
15
name: z.string().min(1),
16
age: z.number().int().min(0),
17
})
18
const r2 = PersonSchema.safeParse({ name: '', age: -1 })
19
if (!r2.success) console.log(r2.error.flatten())

当你能读懂每一段的输出,parse / safeParse 的心智就已经建立起来了。

8. 总结

这一篇我们把 Zod 最核心的两个 API 讲完了:

  1. parse — 严格模式,合法返回数据,不合法抛 ZodError。适合「数据应该合法,不合法是异常」的场景
  2. safeParse — 安全模式,永远返回 { success, data } | { success, error }。适合「失败是预期之内」的场景
  3. ZodError.issues — 错误信息的核心,包含 path / code / message,配合 flatten / format 快速拿到可用结构
  4. 异步校验 — 用 parseAsync / safeParseAsync,规则是:schema 里有异步逻辑就必须用异步版本

记住这句话,你之后 90% 的 Zod 场景都能稳定选对工具:

NOTE

「失败是异常」用 parse,「失败是业务分支」用 safeParse

下一篇开始,我们系统过一遍 Zod 的基础类型:stringnumberbooleandate,每一个都有一些新手很容易踩的小坑。