环企首页
Zod
概述
Zod 是一个 TypeScript 优先的运行时数据校验库。通过声明式的 Schema 定义,在运行时验证数据是否符合预期,同时自动推导 TypeScript 类型。核心特点:
- 类型推导:Schema 即类型定义,无需重复写 TS 类型
- 链式 API:易读易写
- 零依赖:体积小
- 全能校验:字符串、数字、对象、数组、枚举、递归类型全覆盖
适用场景:
- API 请求参数校验(req.body、query、params)
- 环境变量校验(启动前检查)
- 配置文件校验
- 表单数据校验
vs Joi: Zod 天然支持 TypeScript 类型推导,Joi 需要额外维护类型。
核心函数
创建 Schema
const { z } = require('zod')
// 字符串
const nameSchema = z.string()
const emailSchema = z.string().email() // 邮箱格式
const urlSchema = z.string().url() // URL 格式
const uuidSchema = z.string().uuid() // UUID 格式
const regexSchema = z.string().regex(/^\d{11}$/) // 正则
const minLenSchema = z.string().min(2) // 最短长度
const maxLenSchema = z.string().max(100)
const lenSchema = z.string().length(11) // 固定长度
// 数字
const ageSchema = z.number()
const intSchema = z.number().int() // 整数
const positiveSchema = z.number().positive() // > 0
const nonNegativeSchema = z.number().nonnegative() // >= 0
const rangeSchema = z.number().min(0).max(150) // 范围
// 布尔
const boolSchema = z.boolean()
// 枚举
const statusSchema = z.enum(['pending', 'active', 'done'])
// 对象
const userSchema = z.object({
name: z.string().min(2),
age: z.number().min(0).max(150).optional(), // 可选
email: z.string().email(),
role: z.enum(['admin', 'user']).default('user') // 带默认值
})
// 数组
const idsSchema = z.array(z.number())
const stringsSchema = z.array(z.string()).min(1).max(10)
// 日期
const dateSchema = z.date()
// 联合类型
const statusOrNumSchema = z.union([z.enum(['A', 'B']), z.number()])
// 互斥(满足其一)
const optionalStringSchema = z.string().optional().or(z.null())
// 深度可选(对象所有字段变可选)
const partialUserSchema = userSchema.partial()
// 深度必填(对象所有字段变必填)
const requiredUserSchema = userSchema.required()
// 读取-only(去除某些字段)
const publicUserSchema = userSchema.omit({ passwordHash: true })
const publicUserSchema2 = userSchema.pick({ name: true, email: true })
校验数据 — .parse() / .safeParse()
// parse — 校验失败抛异常(ZodError)
try {
const user = userSchema.parse({
name: '张三',
age: 25,
email: 'zhangsan@example.com',
role: 'admin'
})
// user 是完整类型推断的 User 类型
} catch (err) {
if (err instanceof z.ZodError) {
console.log(err.errors) // [{ path: [...], message: '...' }]
}
}
// safeParse — 返回结果对象,不抛异常(推荐)
const result = userSchema.safeParse({
name: '张',
email: 'not-an-email',
age: 25
})
if (result.success) {
console.log(result.data) // 校验通过的数据
} else {
console.log(result.error.issues)
// [
// { path: ['name'], message: 'String must contain at least 2 character(s)' },
// { path: ['email'], message: 'Invalid email' }
// ]
}
// safeParseAsync — 异步校验(用于异步 transform)
const asyncResult = await userSchema.safeParseAsync(data)
类型推导
// 直接从 Schema 推导 TypeScript 类型(无需重复定义)
type User = z.infer<typeof userSchema>
// 等价于:
// {
// name: string;
// age?: number;
// email: string;
// role: 'admin' | 'user';
// }
// 在 API 路由中使用
app.post('/users', async (req, res) => {
const result = userSchema.safeParse(req.body)
if (!result.success) {
return res.status(400).json({ errors: result.error.errors })
}
// result.data 已经是完全类型安全的 User 类型
const user = await createUser(result.data)
res.status(201).json(user)
})
自定义错误信息
const schema = z.string({
required_error: '必填字段',
invalid_type_error: '类型错误'
})
// 每个方法可单独设置 message
const nameSchema = z.string().min(2, '名字至少2个字符')
const ageSchema = z.number().min(18, '必须年满18周岁').max(150, '年龄不合法')
// 自定义校验函数
const customSchema = z.string().refine(
(val) => val.startsWith('admin_'), // 校验函数
{ message: '必须以 admin_ 开头' } // 失败时的错误信息
)
// 带条件的校验
const passwordSchema = z.string()
.min(8, '密码至少8位')
.refine(
(val) => /[A-Z]/.test(val) && /[0-9]/.test(val),
{ message: '密码必须包含大小写字母和数字' }
)
Transform(转换)
// 数据转换(校验通过后自动转换)
const intSchema = z.string().transform((val) => parseInt(val, 10))
const result = intSchema.safeParse('42')
// result.success === true, result.data === 42
// pipe — 链式转换
const trimAndLower = z.string().trim().toLowerCase()
// 复杂转换(带副作用)
const dateSchema = z.string()
.transform((str) => new Date(str))
.refine((date) => date < new Date(), {
message: '日期不能是未来'
})
预处理(Preprocess)
用于在解析前先处理数据(常用于处理 JSON.parse 字符串)。
// 处理前端传来的 JSON 字符串
const requestSchema = z.object({
data: z.preprocess(
(val) => typeof val === 'string' ? JSON.parse(val) : val,
z.object({ id: z.number(), name: z.string() })
)
})
// 统一处理空字符串为 undefined
const optionalString = z.preprocess(
(val) => val === '' ? undefined : val,
z.string().optional()
)
// 批量预处理(coerce)
const coerceInt = z.coerce.number() // 自动将 '42' 转为 42
const coerceBool = z.coerce.boolean() // ''/null/undefined → false,'true'/'1' → true
嵌套对象与数组
const postSchema = z.object({
id: z.number(),
title: z.string().min(1).max(200),
tags: z.array(z.string()).min(1),
author: z.object({
id: z.number(),
name: z.string(),
profile: z.object({
avatar: z.string().url().optional()
}).optional()
}),
comments: z.array(
z.object({
id: z.number(),
content: z.string().min(1),
createdAt: z.string().datetime()
})
).optional()
})
// 解析
const post = postSchema.parse({ ... })
常见集成模式
// Express 集成
app.post('/users', async (req, res) => {
const result = userSchema.safeParse(req.body)
if (!result.success) {
return res.status(400).json({
error: '参数错误',
details: result.error.errors.map(e => ({
field: e.path.join('.'),
message: e.message
}))
})
}
const { name, email, age } = result.data
// ...
})
// 环境变量校验(启动时)
const envSchema = z.object({
NODE_ENV: z.enum(['development', 'production']).default('development'),
PORT: z.coerce.number().min(1).max(65535).default(3000),
DATABASE_URL: z.string().url(),
JWT_SECRET: z.string().min(32)
})
const env = envSchema.safeParse(process.env)
if (!env.success) {
console.error('环境变量配置错误:')
console.error(env.error.errors)
process.exit(1)
}
// 全局访问已校验的环境变量
const config = env.data
// Fastify 集成
fastify.post('/users', {
schema: {
body: userSchema
}
}, handler)
常见问题
| 问题 | 解决方案 |
|---|---|
| 校验对象时有未知字段 | 默认忽略,用 strict 模式报错 |
| 想要可选字段为空字符串时自动转为 undefined | 用 z.preprocess |
| 递归类型(如树形结构) | 用 z.lazy() 或 z.recursive() |
| 校验通过但类型不对 | 用 z.infer 推导,不用重复定义 |
| 错误信息太笼统 | 每个方法单独设置 message |