TypeScript 的类型系统在编译时帮你挡住错误,但 API 响应是在运行时到达的。Zod 补上了这个缺口:它在运行时验证数据的形状和类型,同时从 schema 推导出 TypeScript 类型,一份定义两用。手写复杂 JSON 结构的 Zod schema 很费时间,JSON 转 Zod 工具能在几秒内完成这件事。
Zod 是什么?
Zod 是一个以 TypeScript 为先的 schema 声明与验证库。与 JSON.parse() 返回 any 不同,Zod:
- 验证数据是否符合预期结构
- 从 schema 推导 TypeScript 类型(无需重复写类型定义)
- 生成结构化的详细错误信息
- 同时支持 Node.js 和浏览器
import { z } from 'zod'
const UserSchema = z.object({
id: z.number(),
name: z.string(),
email: z.string().email(),
role: z.enum(['admin', 'user', 'guest'])
})
type User = z.infer<typeof UserSchema>
// 等价于: { id: number; name: string; email: string; role: 'admin' | 'user' | 'guest' }
const result = UserSchema.safeParse(apiResponse)
if (result.success) {
console.log(result.data.name) // 类型为 string
} else {
console.error(result.error.issues)
}
JSON 转 Zod 的转换逻辑
给定 JSON 样本:
{
"id": 42,
"name": "张三",
"email": "[email protected]",
"age": 28,
"active": true,
"tags": ["admin", "beta"],
"address": {
"province": "广东",
"city": "深圳",
"district": "南山区"
},
"metadata": null
}
工具生成:
import { z } from 'zod'
const Schema = z.object({
id: z.number(),
name: z.string(),
email: z.string(),
age: z.number(),
active: z.boolean(),
tags: z.array(z.string()),
address: z.object({
province: z.string(),
city: z.string(),
district: z.string()
}),
metadata: z.null()
})
export type Schema = z.infer<typeof Schema>
生成的 schema 是起点,后续按业务规则细化(见下文)。
在哪里用 Zod 验证
API 响应验证
// 不用 Zod — any 类型,运行时没有保障
const user = (await fetch('/api/user').then(r => r.json())) as User
// 用 Zod — 验证 + 类型推导
const UserSchema = z.object({ id: z.number(), name: z.string() })
async function fetchUser(id: number) {
const raw = await fetch(`/api/users/${id}`).then(r => r.json())
return UserSchema.parse(raw) // 结构不匹配时抛出 ZodError
}
对接微信开放平台、阿里云 API、第三方支付接口时,响应结构复杂且文档不总是准确,Zod 验证能在对齐阶段提前暴露问题。
tRPC / Next.js API 路由
import { z } from 'zod'
import { publicProcedure } from '../trpc'
const createUserInput = z.object({
name: z.string().min(1).max(100),
email: z.string().email(),
role: z.enum(['admin', 'user']).default('user')
})
export const createUser = publicProcedure
.input(createUserInput)
.mutation(async ({ input }) => {
// input 类型为 { name: string; email: string; role: 'admin' | 'user' }
return db.users.create(input)
})
表单验证(React Hook Form)
import { useForm } from 'react-hook-form'
import { zodResolver } from '@hookform/resolvers/zod'
import { z } from 'zod'
const formSchema = z.object({
phone: z.string().regex(/^1[3-9]\d{9}$/, '请输入有效的手机号'),
password: z.string().min(8, '至少 8 位'),
confirmPassword: z.string()
}).refine(data => data.password === data.confirmPassword, {
message: '两次密码不一致',
path: ['confirmPassword']
})
type FormData = z.infer<typeof formSchema>
function RegisterForm() {
const { register, handleSubmit, formState: { errors } } = useForm<FormData>({
resolver: zodResolver(formSchema)
})
// ...
}
环境变量校验
import { z } from 'zod'
const envSchema = z.object({
DATABASE_URL: z.string().url(),
PORT: z.coerce.number().int().positive().default(3000),
NODE_ENV: z.enum(['development', 'production', 'test']),
API_SECRET: z.string().min(32)
})
export const env = envSchema.parse(process.env)
// 启动时若环境变量缺失或格式错误,直接崩溃并输出清晰报错
细化生成的 Schema
工具从 JSON 值推断类型,但 JSON 无法表达所有 Zod 约束,这些需要手动补充:
字符串格式
// 工具生成
email: z.string()
// 细化后
email: z.string().email()
url: z.string().url()
uuid: z.string().uuid()
isoDate: z.string().datetime()
phone: z.string().regex(/^1[3-9]\d{9}$/)
数字约束
// 工具生成
age: z.number()
// 细化后
age: z.number().int().min(0).max(150)
price: z.number().positive()
quantity: z.number().int().nonnegative()
可选字段
JSON 样本只反映有值的字段,实际可能缺失:
// 样本中存在该字段,工具生成
nickname: z.string()
// 细化后
nickname: z.string().optional() // 允许 undefined
nickname: z.string().nullable() // 允许 null
nickname: z.string().nullish() // 允许 null 或 undefined
枚举和联合类型
// 样本中 status 是 "active",工具生成
status: z.string()
// 掌握全部枚举值后细化
status: z.enum(['active', 'inactive', 'pending', 'archived'])
// 不同形状的联合
result: z.union([
z.object({ success: z.literal(true), data: DataSchema }),
z.object({ success: z.literal(false), error: z.string() })
])
null 值的处理
JSON 字段为 null 时,工具生成 z.null()。实际含义通常是该字段可为 null,而非永远为 null:
// 工具生成
metadata: z.null()
// 更实用的写法
metadata: z.string().nullable() // string | null
metadata: z.record(z.unknown()).nullable() // object | null
parse 与 safeParse
| 方法 | 失败时行为 | 返回值 |
|---|---|---|
.parse(data) | 抛出 ZodError | 验证通过的数据 |
.safeParse(data) | 返回 { success: false, error } | { success, data | error } |
启动阶段用 .parse()(环境变量、配置文件),失败应该直接崩溃。处理 API 响应和用户输入时用 .safeParse(),优雅处理错误。
// parse — 失败即崩溃
const config = ConfigSchema.parse(process.env)
// safeParse — 优雅处理
const result = UserSchema.safeParse(apiResponse)
if (!result.success) {
return { error: result.error.format() }
}
// result.data 完整类型推导
return result.data
使用工具
粘贴 API 响应样本、Postman 示例、Swagger 定义中的 JSON,立即获得可运行的 Zod schema。支持嵌套对象、数组、null 值和混合类型数组,输出包含 z.infer 类型导出,无需额外写 TypeScript interface。