读懂验证错误
解析失败会返回一个错误对象。如果直接把它打印到控制台,看起来就是一大堆文字,让人无从下手。
其实不然。把提示信息挂到正确的表单字段上所需要的一切,都藏在这个错误对象里,而且它的结构本来就是设计给代码读取的。
从一个两个值都会出错的 schema 开始:
import * as z from 'zod'
const teacherSchema = z.object({
name: z.string(),
age: z.number().min(18),
})
const result = teacherSchema.safeParse({ name: 12345, age: 13 })issues 数组
错误对象里真正有用的内容都在 .issues 上,它是一个数组,因为一次解析可能同时发现好几个问题:
console.log(result.error.issues.length)
// 2两个值都出错了,所以有两条 issue。每一条都是一个描述单个问题的对象:
console.log(result.error.issues[0])
// {
// expected: 'string',
// code: 'invalid_type',
// path: [ 'name' ],
// message: 'Invalid input: expected string, received number'
// }值得记住的四个字段:
| 字段 | 它告诉你什么 |
|---|---|
code | 失败的种类,比如 invalid_type 或 too_small |
path | 哪个字段出了问题,以数组形式表示 |
message | 描述问题的一句话 |
expected | 在类型失败时,schema 原本期望的类型 |
用扫描仪来打比方依然成立。控制台里的提示信息,是机器打在显示屏上的摘要。issues 才是底下那份详细报告,真正拿来干活的是它。
其实数组一直都在那里。用 .issues 取出来,你就能拿到结构化的版本,每一条问题占一个条目。
哪个字段出了问题
path 是一个数组而不是字符串,因为字段的层级可能是嵌套的:
const orderSchema = z.object({
user: z.object({
profile: z.object({
email: z.email(),
}),
}),
})
const bad = orderSchema.safeParse({ user: { profile: { email: 'nope' } } })
console.log(bad.error.issues[0].path)
// [ 'user', 'profile', 'email' ]对于扁平的表单来说,第一个元素就是字段名,这已经足够拼出表单需要的东西:
const errors = result.error.issues.map((issue) => ({
field: issue.path[0],
message: issue.message,
}))
console.log(errors)
// [
// { field: 'name', message: 'Invalid input: expected string, received number' },
// { field: 'age', message: 'Too small: expected number to be >=18' }
// ]一个字段名配一句提示,每个问题各一条。表单需要的正是这种结构,好把每条提示放在对应输入框旁边。
['user', 'profile', 'email'] 就像一组路线指示:先进入 user,再进入 profile,最后是 email。普通字符串没法表达这种层级,除非你再手动把它拆开。
编写自己的提示信息
默认的提示信息描述的是类型系统本身,而不是你的表单。“Invalid input: expected string, received number”这句话很准确,但对任何一个真实用户来说都毫无意义。
几乎所有 Zod 方法都接受一个提示信息作为参数:
const teacherSchema = z.object({
name: z.string('请输入你的姓名'),
age: z.number().min(18, '教师年龄必须年满十八岁'),
})
const result = teacherSchema.safeParse({ name: 12345, age: 13 })
console.log(result.error.issues.map((issue) => issue.message))
// [ '请输入你的姓名', '教师年龄必须年满十八岁' ]这条提示信息只替换该项检查的默认信息。每个约束都带着自己的提示,所以同一个字段在“没填”和“填得太短”这两种情况下可以显示不同的话。
看到这条提示的人不是在调试你的 schema,他们只是想注册一个账号而已。
动手试一试
给定下面这个 schema,以及一个同时违反两条规则的角色数据:
const characterSchema = z.object({
name: z.string('每个角色都需要一个名字'),
episode: z.number().min(1, '集数从第1集开始'),
})
const character = { name: 42, episode: 0 }六处空白,用 ___ 标记。success、result、error、issues、path 和 message 各自恰好对应其中一处:
const ___ = characterSchema.safeParse(character)
if (result.___) {
console.log('All good')
} else {
console.log(
result.___.___.map((issue) => ({
field: issue.___[0],
message: issue.___,
})),
)
}核对你的答案
const result = characterSchema.safeParse(character)
if (result.success) {
console.log('All good')
} else {
console.log(
result.error.issues.map((issue) => ({
field: issue.path[0],
message: issue.message,
})),
)
}按顺序梳理一遍:
- 第一处空白是给
safeParse的返回值命名的,下面全都写作result,所以这里必须是result。 result.success是决定走哪个分支的布尔值。result.error只在失败的分支上才存在,这就是为什么它出现在else里面。.issues是那个数组,所以.map需要用它。issue.path[0]是字段名,因为path是数组,所以要用下标取值。issue.message就是那句提示信息。
这一串串起来其实就是一句话:这次结果的错误里,每条 issue 都有一个 path 和一条 message。
两条规则都被违反了,所以会打印出两条记录:
// [
// { field: 'name', message: '每个角色都需要一个名字' },
// { field: 'episode', message: '集数从第1集开始' }
// ]接下来去哪儿
这三章的 Zod 内容,全都是你手动拿虚构对象跑出来的。schema 本身确实在做实实在在的工作,但目前还没有跟真正的应用程序连接起来。
验证该放在哪里 会把它接入应用程序,把这层检查放在传入的请求和处理请求的代码之间。

