zod

Compare original and translation side by side

🇺🇸

Original

English
🇨🇳

Translation

Chinese

Zod Schema Validation

Zod 模式验证

TypeScript-first schema declaration and validation library with static type inference.
一款优先支持 TypeScript 的模式声明与验证库,具备静态类型推断能力。

Why Zod

为什么选择 Zod

  • Zero dependencies, 2kb gzipped
  • Works in Node.js and browsers
  • Immutable API - methods return new instances
  • Static type inference - no redundant type declarations
  • JSON Schema conversion built-in
  • 零依赖,gzip 压缩后仅 2kb
  • 支持 Node.js 与浏览器环境
  • 不可变 API - 方法返回新实例
  • 静态类型推断 - 无需冗余类型声明
  • 内置 JSON Schema 转换功能

Requirements

要求

  • TypeScript v5.5+
  • Enable
    strict
    mode in tsconfig.json
  • TypeScript v5.5+
  • 在 tsconfig.json 中启用
    strict
    模式

Core Concepts

核心概念

Schema Definition

模式定义

Always define schemas before validation:
typescript
import { z } from "zod";

// Object schema
const UserSchema = z.object({
  id: z.string().uuid(),
  email: z.string().email(),
  age: z.number().int().positive(),
  role: z.enum(["admin", "user", "guest"]),
});

// Infer TypeScript type from schema
type User = z.infer<typeof UserSchema>;
验证前请先定义模式:
typescript
import { z } from "zod";

// 对象模式
const UserSchema = z.object({
  id: z.string().uuid(),
  email: z.string().email(),
  age: z.number().int().positive(),
  role: z.enum(["admin", "user", "guest"]),
});

// 从模式中推断 TypeScript 类型
type User = z.infer<typeof UserSchema>;

Parsing Methods

解析方法

.parse()
- Throws
ZodError
on failure:
typescript
try {
  const user = UserSchema.parse(data);
  // user is typed as User
} catch (e) {
  if (e instanceof z.ZodError) {
    console.error(e.issues);
  }
}
.safeParse()
- Returns discriminated union (preferred):
typescript
const result = UserSchema.safeParse(data);

if (result.success) {
  console.log(result.data); // typed User
} else {
  console.error(result.error.issues);
}
Async variants - Required for async refinements/transforms:
typescript
await UserSchema.parseAsync(data);
await UserSchema.safeParseAsync(data);
.parse()
- 验证失败时抛出
ZodError
typescript
try {
  const user = UserSchema.parse(data);
  // user 类型为 User
} catch (e) {
  if (e instanceof z.ZodError) {
    console.error(e.issues);
  }
}
.safeParse()
- 返回区分联合类型(推荐使用):
typescript
const result = UserSchema.safeParse(data);

if (result.success) {
  console.log(result.data); // 类型为 User
} else {
  console.error(result.error.issues);
}
异步变体 - 异步细化/转换时需要使用:
typescript
await UserSchema.parseAsync(data);
await UserSchema.safeParseAsync(data);

Primitive Types

基本类型

typescript
// Basic primitives
z.string()
z.number()
z.bigint()
z.boolean()
z.date()
z.symbol()
z.undefined()
z.null()
z.void()
z.any()
z.unknown()
z.never()

// Coercion - converts input to target type
z.coerce.string()   // String(input)
z.coerce.number()   // Number(input)
z.coerce.boolean()  // Boolean(input)
z.coerce.bigint()   // BigInt(input)
z.coerce.date()     // new Date(input)
typescript
// 基础类型
z.string()
z.number()
z.bigint()
z.boolean()
z.date()
z.symbol()
z.undefined()
z.null()
z.void()
z.any()
z.unknown()
z.never()

// 强制转换 - 将输入转换为目标类型
z.coerce.string()   // String(input)
z.coerce.number()   // Number(input)
z.coerce.boolean()  // Boolean(input)
z.coerce.bigint()   // BigInt(input)
z.coerce.date()     // new Date(input)

String Validations

字符串验证

typescript
z.string()
  .min(1)              // Minimum length
  .max(255)            // Maximum length
  .length(10)          // Exact length
  .email()             // Email format
  .url()               // URL format
  .uuid()              // UUID format
  .cuid()              // CUID format
  .regex(/pattern/)    // Custom regex
  .startsWith("prefix")
  .endsWith("suffix")
  .includes("substring")
  .trim()              // Transform: trim whitespace
  .toLowerCase()       // Transform: lowercase
  .toUpperCase()       // Transform: uppercase
typescript
z.string()
  .min(1)              // 最小长度
  .max(255)            // 最大长度
  .length(10)          // 精确长度
  .email()             // 邮箱格式
  .url()               // URL 格式
  .uuid()              // UUID 格式
  .cuid()              // CUID 格式
  .regex(/pattern/)    // 自定义正则
  .startsWith("prefix")
  .endsWith("suffix")
  .includes("substring")
  .trim()              // 转换:去除首尾空格
  .toLowerCase()       // 转换:转为小写
  .toUpperCase()       // 转换:转为大写

Number Validations

数字验证

typescript
z.number()
  .int()               // Integer only
  .positive()          // > 0
  .nonnegative()       // >= 0
  .negative()          // < 0
  .nonpositive()       // <= 0
  .gt(5)               // > 5
  .gte(5)              // >= 5 (alias: .min())
  .lt(10)              // < 10
  .lte(10)             // <= 10 (alias: .max())
  .multipleOf(5)       // Divisible by 5
  .finite()            // Excludes Infinity
  .safe()              // Safe integer range
typescript
z.number()
  .int()               // 仅允许整数
  .positive()          // > 0
  .nonnegative()       // >= 0
  .negative()          // < 0
  .nonpositive()       // <= 0
  .gt(5)               // > 5
  .gte(5)              // >= 5(别名:.min())
  .lt(10)              // < 10
  .lte(10)             // <= 10(别名:.max())
  .multipleOf(5)       // 可被 5 整除
  .finite()            // 排除 Infinity
  .safe()              // 安全整数范围

Object Schemas

对象模式

typescript
const PersonSchema = z.object({
  name: z.string(),
  age: z.number(),
});

// Make all properties optional
PersonSchema.partial();

// Make specific properties optional
PersonSchema.partial({ age: true });

// Make all properties required
PersonSchema.required();

// Pick specific properties
PersonSchema.pick({ name: true });

// Omit specific properties
PersonSchema.omit({ age: true });

// Extend with new properties
PersonSchema.extend({
  email: z.string().email(),
});

// Strict mode - reject unknown keys
PersonSchema.strict();

// Passthrough - preserve unknown keys
PersonSchema.passthrough();

// Strip unknown keys (default behavior)
PersonSchema.strip();
typescript
const PersonSchema = z.object({
  name: z.string(),
  age: z.number(),
});

// 将所有属性设为可选
PersonSchema.partial();

// 将指定属性设为可选
PersonSchema.partial({ age: true });

// 将所有属性设为必填
PersonSchema.required();

// 选取指定属性
PersonSchema.pick({ name: true });

// 排除指定属性
PersonSchema.omit({ age: true });

// 扩展新属性
PersonSchema.extend({
  email: z.string().email(),
});

// 严格模式 - 拒绝未知键
PersonSchema.strict();

// 透传模式 - 保留未知键
PersonSchema.passthrough();

// 剥离未知键(默认行为)
PersonSchema.strip();

Arrays and Tuples

数组与元组

typescript
// Array of strings
z.array(z.string())
  .min(1)              // At least 1 element
  .max(10)             // At most 10 elements
  .length(5)           // Exactly 5 elements
  .nonempty();         // At least 1 element (typed)

// Alternative syntax
z.string().array();

// Tuple with fixed positions
z.tuple([
  z.string(),          // First element: string
  z.number(),          // Second element: number
]);

// Tuple with rest elements
z.tuple([z.string(), z.number()]).rest(z.boolean());
typescript
// 字符串数组
z.array(z.string())
  .min(1)              // 至少 1 个元素
  .max(10)             // 最多 10 个元素
  .length(5)           // 恰好 5 个元素
  .nonempty();         // 至少 1 个元素(带类型推断)

// 替代语法
z.string().array();

// 固定位置的元组
z.tuple([
  z.string(),          // 第一个元素:字符串
  z.number(),          // 第二个元素:数字
]);

// 带剩余元素的元组
z.tuple([z.string(), z.number()]).rest(z.boolean());

Unions and Enums

联合类型与枚举

typescript
// Union types
z.union([z.string(), z.number()]);
// Shorthand
z.string().or(z.number());

// Discriminated unions (better error messages)
z.discriminatedUnion("type", [
  z.object({ type: z.literal("email"), email: z.string() }),
  z.object({ type: z.literal("phone"), phone: z.string() }),
]);

// Enum from array
z.enum(["admin", "user", "guest"]);

// Native enum
enum Role { Admin, User }
z.nativeEnum(Role);
typescript
// 联合类型
z.union([z.string(), z.number()]);
// 简写
z.string().or(z.number());

// 可区分联合类型(更清晰的错误提示)
z.discriminatedUnion("type", [
  z.object({ type: z.literal("email"), email: z.string() }),
  z.object({ type: z.literal("phone"), phone: z.string() }),
]);

// 从数组创建枚举
z.enum(["admin", "user", "guest"]);

// 原生枚举
enum Role { Admin, User }
z.nativeEnum(Role);

Optional and Nullable

可选与可空类型

typescript
// Optional - allows undefined
z.string().optional();  // string | undefined

// Nullable - allows null
z.string().nullable();  // string | null

// Both
z.string().nullish();   // string | null | undefined

// Default values
z.string().default("anonymous");
z.string().optional().default("anonymous");

// Catch - use default on parse failure
z.string().catch("fallback");
typescript
// 可选 - 允许 undefined
z.string().optional();  // string | undefined

// 可空 - 允许 null
z.string().nullable();  // string | null

// 同时允许两者
z.string().nullish();   // string | null | undefined

// 默认值
z.string().default("anonymous");
z.string().optional().default("anonymous");

// 捕获失败 - 解析失败时使用默认值
z.string().catch("fallback");

Transforms

转换

typescript
// Transform output type
const StringToNumber = z.string().transform((val) => parseInt(val, 10));
type Output = z.output<typeof StringToNumber>; // number

// Chain transforms
z.string()
  .trim()
  .toLowerCase()
  .transform((val) => val.split(","));

// Preprocess input before validation
z.preprocess(
  (val) => String(val),
  z.string().min(1)
);
typescript
// 转换输出类型
const StringToNumber = z.string().transform((val) => parseInt(val, 10));
type Output = z.output<typeof StringToNumber>; // number

// 链式转换
z.string()
  .trim()
  .toLowerCase()
  .transform((val) => val.split(","));

// 验证前预处理输入
z.preprocess(
  (val) => String(val),
  z.string().min(1)
);

Refinements

细化验证

typescript
// Custom validation
z.string().refine(
  (val) => val.length <= 255,
  { message: "String must be 255 chars or less" }
);

// Async refinement
z.string().refine(
  async (val) => await checkUnique(val),
  { message: "Value must be unique" }
);

// Super refine for complex validations
z.object({
  password: z.string(),
  confirm: z.string(),
}).superRefine((data, ctx) => {
  if (data.password !== data.confirm) {
    ctx.addIssue({
      code: z.ZodIssueCode.custom,
      message: "Passwords don't match",
      path: ["confirm"],
    });
  }
});
typescript
// 自定义验证
z.string().refine(
  (val) => val.length <= 255,
  { message: "String must be 255 chars or less" }
);

// 异步细化验证
z.string().refine(
  async (val) => await checkUnique(val),
  { message: "Value must be unique" }
);

// Super refine 用于复杂验证
z.object({
  password: z.string(),
  confirm: z.string(),
}).superRefine((data, ctx) => {
  if (data.password !== data.confirm) {
    ctx.addIssue({
      code: z.ZodIssueCode.custom,
      message: "Passwords don't match",
      path: ["confirm"],
    });
  }
});

Error Handling

错误处理

typescript
const result = schema.safeParse(data);

if (!result.success) {
  // Access all issues
  result.error.issues.forEach((issue) => {
    console.log(issue.path);    // Field path
    console.log(issue.message); // Error message
    console.log(issue.code);    // Error code
  });

  // Flatten for form errors
  const flat = result.error.flatten();
  // { formErrors: string[], fieldErrors: { [key]: string[] } }

  // Format for display
  const formatted = result.error.format();
  // { _errors: string[], field: { _errors: string[] } }
}
typescript
const result = schema.safeParse(data);

if (!result.success) {
  // 访问所有错误信息
  result.error.issues.forEach((issue) => {
    console.log(issue.path);    // 字段路径
    console.log(issue.message); // 错误信息
    console.log(issue.code);    // 错误代码
  });

  // 扁平化处理表单错误
  const flat = result.error.flatten();
  // { formErrors: string[], fieldErrors: { [key]: string[] } }

  // 格式化用于展示
  const formatted = result.error.format();
  // { _errors: string[], field: { _errors: string[] } }
}

Common Patterns

常见模式

API Request Validation

API 请求验证

typescript
const CreateUserRequest = z.object({
  email: z.string().email(),
  password: z.string().min(8),
  name: z.string().min(1).max(100),
});

// In Express/Fastify handler
const body = CreateUserRequest.parse(req.body);
typescript
const CreateUserRequest = z.object({
  email: z.string().email(),
  password: z.string().min(8),
  name: z.string().min(1).max(100),
});

// 在 Express/Fastify 处理器中
const body = CreateUserRequest.parse(req.body);

Environment Variables

环境变量

typescript
const EnvSchema = z.object({
  NODE_ENV: z.enum(["development", "production", "test"]),
  PORT: z.coerce.number().default(3000),
  DATABASE_URL: z.string().url(),
  API_KEY: z.string().min(1),
});

export const env = EnvSchema.parse(process.env);
typescript
const EnvSchema = z.object({
  NODE_ENV: z.enum(["development", "production", "test"]),
  PORT: z.coerce.number().default(3000),
  DATABASE_URL: z.string().url(),
  API_KEY: z.string().min(1),
});

export const env = EnvSchema.parse(process.env);

Form Data

表单数据

typescript
const ContactForm = z.object({
  name: z.string().min(1, "Name is required"),
  email: z.string().email("Invalid email address"),
  message: z.string().min(10, "Message must be at least 10 characters"),
});

// With React Hook Form
const { register, handleSubmit } = useForm({
  resolver: zodResolver(ContactForm),
});
typescript
const ContactForm = z.object({
  name: z.string().min(1, "Name is required"),
  email: z.string().email("Invalid email address"),
  message: z.string().min(10, "Message must be at least 10 characters"),
});

// 结合 React Hook Form 使用
const { register, handleSubmit } = useForm({
  resolver: zodResolver(ContactForm),
});

API Response

API 响应

typescript
const ApiResponse = z.object({
  data: z.array(UserSchema),
  pagination: z.object({
    page: z.number(),
    total: z.number(),
  }),
});

const response = await fetch("/api/users");
const json = await response.json();
const validated = ApiResponse.parse(json);
typescript
const ApiResponse = z.object({
  data: z.array(UserSchema),
  pagination: z.object({
    page: z.number(),
    total: z.number(),
  }),
});

const response = await fetch("/api/users");
const json = await response.json();
const validated = ApiResponse.parse(json);

JSON Schema Conversion

JSON Schema 转换

typescript
import { z } from "zod";

// Zod to JSON Schema
const jsonSchema = z.toJSONSchema(UserSchema);

// JSON Schema to Zod
const zodSchema = z.fromJSONSchema(jsonSchema);
typescript
import { z } from "zod";

// Zod 转 JSON Schema
const jsonSchema = z.toJSONSchema(UserSchema);

// JSON Schema 转 Zod
const zodSchema = z.fromJSONSchema(jsonSchema);

Anti-Patterns

反模式

Avoid redundant type declarations

避免冗余类型声明

typescript
// Bad - duplicates schema
interface User {
  name: string;
  age: number;
}
const UserSchema = z.object({
  name: z.string(),
  age: z.number(),
});

// Good - infer from schema
const UserSchema = z.object({
  name: z.string(),
  age: z.number(),
});
type User = z.infer<typeof UserSchema>;
typescript
// 错误写法 - 重复定义模式
interface User {
  name: string;
  age: number;
}
const UserSchema = z.object({
  name: z.string(),
  age: z.number(),
});

// 正确写法 - 从模式推断类型
const UserSchema = z.object({
  name: z.string(),
  age: z.number(),
});
type User = z.infer<typeof UserSchema>;

Use safeParse for user input

对用户输入使用 safeParse

typescript
// Bad - throws on invalid input
const user = UserSchema.parse(userInput);

// Good - handle errors gracefully
const result = UserSchema.safeParse(userInput);
if (!result.success) {
  return { errors: result.error.flatten().fieldErrors };
}
typescript
// 错误写法 - 输入无效时抛出异常
const user = UserSchema.parse(userInput);

// 正确写法 - 优雅处理错误
const result = UserSchema.safeParse(userInput);
if (!result.success) {
  return { errors: result.error.flatten().fieldErrors };
}

Prefer discriminated unions

优先使用可区分联合类型

typescript
// Bad - ambiguous errors
z.union([
  z.object({ email: z.string() }),
  z.object({ phone: z.string() }),
]);

// Good - clear error messages
z.discriminatedUnion("contactType", [
  z.object({ contactType: z.literal("email"), email: z.string() }),
  z.object({ contactType: z.literal("phone"), phone: z.string() }),
]);
typescript
// 错误写法 - 错误提示模糊
z.union([
  z.object({ email: z.string() }),
  z.object({ phone: z.string() }),
]);

// 正确写法 - 清晰的错误提示
z.discriminatedUnion("contactType", [
  z.object({ contactType: z.literal("email"), email: z.string() }),
  z.object({ contactType: z.literal("phone"), phone: z.string() }),
]);

References

参考资料