安装
pnpm add zod # Zod v4 已是默认版本(~2025 起)
import { z } from "zod";
原始类型
z.string(); // string z.number(); // number z.bigint(); // bigint z.boolean(); // boolean z.date(); // Date 实例(不是日期字符串!) z.symbol(); // symbol z.undefined(); z.null(); z.void(); // 通常用于函数返回 z.any(); z.unknown(); z.never();
parse:抛异常式
const result = z.string().parse("hello"); // → "hello"(原样返回,TS 类型 string) z.string().parse(123); // 抛 ZodError: // Expected string, received number
适合"数据就应该对,对不了就让它崩"的场景——启动时 env 校验、Server Action 输入等。
safeParse:Result 式
const res = z.string().safeParse(123); if (res.success) { console.log(res.data); // string } else { console.error(res.error); // ZodError 实例 }
适合表单校验、API 入口——错了不 crash,返回错误给用户。
选择原则
知道数据"应该对"(如启动 env、代码里自己造的对象)——
知道数据"应该对"(如启动 env、代码里自己造的对象)——
parse;数据来自外部用户/网络—— safeParse。不要用 try/catch(parse) 当控制流。
z.object
const User = z.object({ id: z.string().uuid(), name: z.string().min(1).max(50), age: z.number().int().min(0).max(150), email: z.string().email(), }); const u = User.parse({ id: "550e8400-e29b-41d4-a716-446655440000", name: "Alice", age: 30, email: "a@b.com", });
z.infer:类型推断
type User = z.infer<typeof User>; // 等价于: // type User = { // id: string; // name: string; // age: number; // email: string; // }
一份 schema 同时是值和类型——不用再手写 interface。改 schema 就自动改类型,绝不漏。
z.array
z.array(z.string()); // string[] z.string().array(); // 语法糖,同上 z.array(z.number()).min(1).max(10); // number[] 且长度 1-10 z.array(User); // User[]
嵌套对象
const Post = z.object({ id: z.string(), title: z.string(), author: z.object({ id: z.string(), name: z.string(), }), tags: z.array(z.string()), comments: z.array( z.object({ user: z.string(), text: z.string(), createdAt: z.date(), }) ), });
可选字段
const User = z.object({ name: z.string(), bio: z.string().optional(), // string | undefined avatar: z.string().nullable(), // string | null signupDate: z.date().nullish(), // Date | null | undefined });
默认值
const Settings = z.object({ theme: z.enum(["light", "dark"]).default("light"), fontSize: z.number().default(14), }); Settings.parse({}); // { theme: "light", fontSize: 14 } ← 填入默认
default 的值参与解析结果——输出类型依然含这些字段,而不是 undefined。
字符串修饰
z.string() .min(3) .max(20) .length(10) // 精确长度 .email() .url() .uuid() .cuid() .regex(/^[a-z]+$/) .startsWith("https://") .endsWith(".com") .includes("foo") .trim() // 先 trim 再校验 .toLowerCase() .nonempty(); // 非空串,等价于 .min(1)
数字修饰
z.number() .int() .positive() .nonnegative() .negative() .multipleOf(5) .min(0) .max(100) .gt(0).gte(0) // 同 min 但排他 .lt(100).lte(100) .finite() .safe(); // Number.MAX_SAFE_INTEGER 内
z.enum
const Role = z.enum(["admin", "editor", "viewer"]); type Role = z.infer<typeof Role>; // "admin" | "editor" | "viewer" Role.options; // ["admin", "editor", "viewer"] ← 运行时也能拿到 Role.enum; // { admin: "admin", editor: "editor", viewer: "viewer" }
比 z.union([z.literal("admin"), ...]) 清爽,还能枚举 options。
native enum 集成
enum Role { Admin = "admin", User = "user" } const schema = z.nativeEnum(Role); // 接受 "admin" / "user"
z.literal
z.literal("foo"); // "foo" 字面量 z.literal(42); z.literal(true);
coerce:强制类型转换
z.coerce.string(); // 先 String(input),再当字符串校验 z.coerce.number().parse("42"); // → 42(number) z.coerce.boolean().parse("true"); // → true,但 "false" 也是 true!(Boolean("false") === true) z.coerce.date().parse("2026-05-01"); // → Date 实例
coerce.boolean 的陷阱
任何非空字符串都被
任何非空字符串都被
Boolean(...) 转成 true——包括 "false"、"0"。如果要"字符串 'true' → true",用 z.enum(["true", "false"]).transform(s => s === "true")。
strict / passthrough / strip
const User = z.object({ name: z.string() }); // strip(默认):未知字段会被丢弃 User.parse({ name: "A", extra: 1 }); // → { name: "A" } // strict:未知字段报错 User.strict().parse({ name: "A", extra: 1 }); // → ZodError: Unrecognized key: "extra" // passthrough:未知字段保留 User.passthrough().parse({ name: "A", extra: 1 }); // → { name: "A", extra: 1 } // catchall:未知字段都得符合某 schema User.catchall(z.number()).parse({ name: "A", extra: 1 }); // 额外字段必须是 number
shape 和 keyof
User.shape.name; // 拿到 name 字段的子 schema User.keyof(); // z.enum(["id", "name", "age", "email"]) // 常用于生成"字段选择器"的下拉
手动 issue(提前 return)
const schema = z.string().refine( (s) => s.startsWith("http"), { message: "必须以 http 开头" } ); const res = schema.safeParse("ftp://..."); if (!res.success) { console.log(res.error.issues); // [{ code: "custom", message: "必须以 http 开头", path: [] }] }
parseAsync(异步校验)
const schema = z.string().refine( async (s) => !await isBlocked(s), // 查 DB "用户名已被占用" ); const u = await schema.parseAsync("alice"); // 含异步 refine 的 schema 要用 parseAsync / safeParseAsync
describe:文档化
z.string() .email() .describe("用户登录邮箱,必须能收通知"); // 给 zod-to-openapi 生成文档时用
一个完整例子
const ProductSchema = z.object({ id: z.string().uuid(), name: z.string().min(1).max(200), price: z.number().nonnegative().multipleOf(0.01), currency: z.enum(["CNY", "USD", "EUR"]).default("CNY"), stock: z.number().int().nonnegative(), tags: z.array(z.string()).max(10).default([]), description: z.string().optional(), publishedAt: z.coerce.date().nullable(), }); type Product = z.infer<typeof ProductSchema>; // { // id: string; // name: string; // price: number; // currency: "CNY" | "USD" | "EUR"; // stock: number; // tags: string[]; // description?: string; // publishedAt: Date | null; // }
本章小结
z.string/number/boolean/date/array/object五六个基本块拼出大部分 schemaparse抛异常(内部数据),safeParse返回 Result(外部输入)z.infer<typeof schema>一行得类型,不再手写 interfaceoptional/nullable/default处理可选字段,coerce强制转换strict / passthrough / strip控制未知字段的处理策略