zod by bobmatnyc/claude-mpm-skills
npx skills add https://github.com/bobmatnyc/claude-mpm-skills --skill zodTypeScript 优先的 schema 验证库,具备静态类型推断功能。只需定义一次 schema,即可自动获得运行时验证和编译时类型。
import { z } from 'zod';
// 定义 schema
const UserSchema = z.object({
id: z.string().uuid(),
email: z.string().email(),
age: z.number().min(18),
role: z.enum(['user', 'admin'])
});
// 推断 TypeScript 类型
type User = z.infer<typeof UserSchema>;
// 验证数据
const result = UserSchema.safeParse(data);
if (result.success) {
const user: User = result.data;
}
import { z } from 'zod';
// 带验证的字符串
const nameSchema = z.string()
.min(2, "Too short")
.max(50, "Too long")
.trim();
const emailSchema = z.string().email();
const urlSchema = z.string().url();
const uuidSchema = z.string().uuid();
const regexSchema = z.string().regex(/^[A-Z]{3}$/);
// 数字
const ageSchema = z.number()
.int("Must be integer")
.positive()
.min(0)
.max(120);
const priceSchema = z.number()
.positive()
.multipleOf(0.01); // 货币精度
// 布尔值
const isActiveSchema = z.boolean();
// 日期
const createdAtSchema = z.date()
.min(new Date('2020-01-01'))
.max(new Date());
const dateStringSchema = z.string().datetime(); // ISO 8601
const dateOnlySchema = z.string().date(); // YYYY-MM-DD
广告位招租
在这里展示您的产品或服务
触达数万 AI 开发者,精准高效
// 字面量值
const roleSchema = z.literal('admin');
const statusSchema = z.literal('pending');
// 枚举
const ColorEnum = z.enum(['red', 'green', 'blue']);
type Color = z.infer<typeof ColorEnum>; // 'red' | 'green' | 'blue'
const NativeEnum = z.nativeEnum(MyEnum); // 用于 TypeScript 枚举
// 可空和可选
const optionalString = z.string().optional(); // string | undefined
const nullableString = z.string().nullable(); // string | null
const nullishString = z.string().nullish(); // string | null | undefined
// 默认值
const countSchema = z.number().default(0);
const settingsSchema = z.object({
theme: z.string().default('light'),
notifications: z.boolean().default(true)
});
// 基本对象
const UserSchema = z.object({
id: z.string(),
email: z.string().email(),
name: z.string(),
age: z.number().optional()
});
// 嵌套对象
const AddressSchema = z.object({
street: z.string(),
city: z.string(),
country: z.string(),
zipCode: z.string()
});
const PersonSchema = z.object({
name: z.string(),
address: AddressSchema,
contacts: z.object({
email: z.string().email(),
phone: z.string().optional()
})
});
// 严格模式 vs 穿透模式
const strictSchema = z.object({ name: z.string() }).strict();
// 拒绝未知键
const passthroughSchema = z.object({ name: z.string() }).passthrough();
// 允许未知键
const stripSchema = z.object({ name: z.string() }).strip();
// 移除未知键(默认)
// 简单数组
const stringArray = z.array(z.string());
const numberArray = z.array(z.number()).min(1).max(10);
// 对象数组
const UsersSchema = z.array(UserSchema);
// 非空数组
const tagSchema = z.array(z.string()).nonempty("At least one tag required");
// 固定长度数组(元组)
const coordinateSchema = z.tuple([z.number(), z.number()]);
type Coordinate = z.infer<typeof coordinateSchema>; // [number, number]
// 带剩余参数的元组
const csvRowSchema = z.tuple([z.string(), z.number()]).rest(z.string());
// [string, number, ...string[]]
// 记录(具有动态键的对象)
const userRolesSchema = z.record(
z.string(), // 键类型
z.enum(['admin', 'user', 'guest']) // 值类型
);
type UserRoles = z.infer<typeof userRolesSchema>;
// { [key: string]: 'admin' | 'user' | 'guest' }
// 映射
const configMapSchema = z.map(
z.string(), // 键
z.number() // 值
);
// 集合
const uniqueTagsSchema = z.set(z.string());
import { z } from 'zod';
// 推断输出类型
const UserSchema = z.object({
id: z.string(),
email: z.string().email(),
age: z.number()
});
type User = z.infer<typeof UserSchema>;
// { id: string; email: string; age: number }
// 推断输入类型(转换前)
const TransformSchema = z.object({
date: z.string().transform(s => new Date(s))
});
type Input = z.input<typeof TransformSchema>;
// { date: string }
type Output = z.output<typeof TransformSchema>;
// { date: Date }
// 在函数中使用推断类型
function createUser(data: User): void {
// data 是类型安全的
}
function validateAndCreate(data: unknown): User | null {
const result = UserSchema.safeParse(data);
return result.success ? result.data : null;
}
// parse() - 失败时抛出异常
try {
const user = UserSchema.parse(data);
// user 是 User 类型
} catch (error) {
if (error instanceof z.ZodError) {
console.error(error.issues);
}
}
// safeParse() - 返回结果对象
const result = UserSchema.safeParse(data);
if (result.success) {
const user = result.data; // User 类型
} else {
const errors = result.error.issues;
errors.forEach(err => {
console.log(`${err.path}: ${err.message}`);
});
}
// parseAsync() - 用于异步细化
const asyncResult = await UserSchema.parseAsync(data);
// safeParseAsync() - 安全的异步版本
const asyncSafeResult = await UserSchema.safeParseAsync(data);
// 检查数据是否匹配 schema 而不抛出异常
const isValid = UserSchema.safeParse(data).success;
// 自定义类型守卫
function isUser(data: unknown): data is User {
return UserSchema.safeParse(data).success;
}
if (isUser(unknownData)) {
// TypeScript 知道 unknownData 是 User 类型
console.log(unknownData.email);
}
// 扩展(添加字段)
const BaseUserSchema = z.object({
id: z.string(),
email: z.string()
});
const AdminUserSchema = BaseUserSchema.extend({
role: z.literal('admin'),
permissions: z.array(z.string())
});
// 合并(组合 schema)
const NameSchema = z.object({ name: z.string() });
const AgeSchema = z.object({ age: z.number() });
const PersonSchema = NameSchema.merge(AgeSchema);
// { name: string; age: number }
// 选取(选择字段)
const UserIdEmail = UserSchema.pick({ id: true, email: true });
// 省略(排除字段)
const UserWithoutId = UserSchema.omit({ id: true });
// 部分(使所有字段可选)
const PartialUser = UserSchema.partial();
// 深度部分(递归部分)
const DeepPartialUser = UserSchema.deepPartial();
// 必需(使所有字段必需)
const RequiredUser = UserSchema.required();
// 联合(OR)
const StringOrNumber = z.union([z.string(), z.number()]);
// 简写
const StringOrNumberAlt = z.string().or(z.number());
// 可区分联合(标记联合)
const SuccessResponse = z.object({
status: z.literal('success'),
data: z.any()
});
const ErrorResponse = z.object({
status: z.literal('error'),
message: z.string()
});
const ApiResponse = z.discriminatedUnion('status', [
SuccessResponse,
ErrorResponse
]);
// 交叉(AND)
const User = z.object({ name: z.string() });
const Timestamps = z.object({
createdAt: z.date(),
updatedAt: z.date()
});
const UserWithTimestamps = z.intersection(User, Timestamps);
// 简写
const UserWithTimestampsAlt = User.and(Timestamps);
// 验证后转换数据
const StringToNumber = z.string().transform(val => parseInt(val, 10));
const DateSchema = z.string().transform(str => new Date(str));
// 链式转换
const TrimmedLowercase = z.string()
.transform(s => s.trim())
.transform(s => s.toLowerCase());
// 带验证的转换
const PositiveStringNumber = z.string()
.transform(val => parseInt(val, 10))
.refine(n => n > 0, "Must be positive");
// 复杂转换
const UserInputSchema = z.object({
name: z.string().transform(s => s.trim()),
email: z.string().email().transform(s => s.toLowerCase()),
birthDate: z.string().transform(s => new Date(s)),
tags: z.string().transform(s => s.split(',').map(t => t.trim()))
});
type UserInput = z.input<typeof UserInputSchema>;
// { name: string; email: string; birthDate: string; tags: string }
type User = z.output<typeof UserInputSchema>;
// { name: string; email: string; birthDate: Date; tags: string[] }
// 简单细化
const PasswordSchema = z.string()
.min(8)
.refine(
val => /[A-Z]/.test(val),
"Must contain uppercase letter"
)
.refine(
val => /[0-9]/.test(val),
"Must contain number"
);
// 带自定义错误的细化
const UniqueEmailSchema = z.string().email().refine(
async (email) => {
const exists = await checkEmailExists(email);
return !exists;
},
{ message: "Email already taken" }
);
// 对象级细化
const PasswordMatchSchema = z.object({
password: z.string(),
confirmPassword: z.string()
}).refine(
data => data.password === data.confirmPassword,
{
message: "Passwords don't match",
path: ["confirmPassword"] // 错误位置
}
);
// 多字段验证
const DateRangeSchema = z.object({
startDate: z.date(),
endDate: z.date()
}).refine(
data => data.endDate > data.startDate,
{
message: "End date must be after start date",
path: ["endDate"]
}
);
// 访问 Zod 上下文进行复杂验证
const ComplexSchema = z.object({
type: z.enum(['email', 'phone']),
value: z.string()
}).superRefine((data, ctx) => {
if (data.type === 'email') {
const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
if (!emailRegex.test(data.value)) {
ctx.addIssue({
code: z.ZodIssueCode.custom,
message: "Invalid email format",
path: ["value"]
});
}
} else if (data.type === 'phone') {
const phoneRegex = /^\+?[1-9]\d{1,14}$/;
if (!phoneRegex.test(data.value)) {
ctx.addIssue({
code: z.ZodIssueCode.custom,
message: "Invalid phone format",
path: ["value"]
});
}
}
});
// 多个问题
const RegistrationSchema = z.object({
username: z.string(),
email: z.string(),
age: z.number()
}).superRefine(async (data, ctx) => {
// 检查用户名可用性
if (await usernameTaken(data.username)) {
ctx.addIssue({
code: z.ZodIssueCode.custom,
message: "Username taken",
path: ["username"]
});
}
// 检查邮箱可用性
if (await emailTaken(data.email)) {
ctx.addIssue({
code: z.ZodIssueCode.custom,
message: "Email already registered",
path: ["email"]
});
}
// 年龄限制
if (data.age < 18) {
ctx.addIssue({
code: z.ZodIssueCode.custom,
message: "Must be 18 or older",
path: ["age"]
});
}
});
// 字段级消息
const UserSchema = z.object({
email: z.string().email({ message: "Invalid email address" }),
age: z.number({
required_error: "Age is required",
invalid_type_error: "Age must be a number"
}).min(18, { message: "Must be 18 or older" })
});
// 全局错误映射
import { z } from 'zod';
const customErrorMap: z.ZodErrorMap = (issue, ctx) => {
if (issue.code === z.ZodIssueCode.invalid_type) {
if (issue.expected === "string") {
return { message: "This field must be text" };
}
}
if (issue.code === z.ZodIssueCode.too_small) {
if (issue.type === "string") {
return { message: `Minimum ${issue.minimum} characters required` };
}
}
return { message: ctx.defaultError };
};
z.setErrorMap(customErrorMap);
// 扁平化错误以便表单使用
const result = UserSchema.safeParse(data);
if (!result.success) {
const flatErrors = result.error.flatten();
console.log(flatErrors.formErrors); // 顶层错误
console.log(flatErrors.fieldErrors);
// { email: ["Invalid email"], age: ["Must be 18+"] }
}
// 为 API 响应格式化
function formatZodError(error: z.ZodError) {
return error.issues.map(issue => ({
field: issue.path.join('.'),
message: issue.message
}));
}
// 使用示例
const result = UserSchema.safeParse(data);
if (!result.success) {
return res.status(400).json({
errors: formatZodError(result.error)
});
}
import { z } from 'zod';
// 异步细化
const UsernameSchema = z.string().refine(
async (username) => {
const available = await checkUsernameAvailable(username);
return available;
},
{ message: "Username already taken" }
);
// 必须使用 parseAsync 或 safeParseAsync
const result = await UsernameSchema.safeParseAsync("john_doe");
// 复杂异步验证
const RegistrationSchema = z.object({
username: z.string().refine(
async (val) => !(await usernameTaken(val)),
"Username taken"
),
email: z.string().email().refine(
async (val) => !(await emailTaken(val)),
"Email already registered"
),
inviteCode: z.string().refine(
async (code) => await validateInviteCode(code),
"Invalid invite code"
)
});
// 验证
const userData = await RegistrationSchema.parseAsync(input);
// 带错误处理
const result = await RegistrationSchema.safeParseAsync(input);
if (!result.success) {
// 处理验证错误
}
// 自引用 schema
type Category = {
name: string;
subcategories: Category[];
};
const CategorySchema: z.ZodType<Category> = z.lazy(() =>
z.object({
name: z.string(),
subcategories: z.array(CategorySchema)
})
);
// 树结构
type TreeNode = {
value: number;
left?: TreeNode;
right?: TreeNode;
};
const TreeNodeSchema: z.ZodType<TreeNode> = z.lazy(() =>
z.object({
value: z.number(),
left: TreeNodeSchema.optional(),
right: TreeNodeSchema.optional()
})
);
// 基于区分字段的类型安全联合
const Circle = z.object({
kind: z.literal('circle'),
radius: z.number()
});
const Rectangle = z.object({
kind: z.literal('rectangle'),
width: z.number(),
height: z.number()
});
const Triangle = z.object({
kind: z.literal('triangle'),
base: z.number(),
height: z.number()
});
const Shape = z.discriminatedUnion('kind', [
Circle,
Rectangle,
Triangle
]);
type Shape = z.infer<typeof Shape>;
// TypeScript 可以根据区分符进行类型收窄
function calculateArea(shape: Shape): number {
switch (shape.kind) {
case 'circle':
return Math.PI * shape.radius ** 2;
case 'rectangle':
return shape.width * shape.height;
case 'triangle':
return (shape.base * shape.height) / 2;
}
}
// 验证前转换
const NumberFromString = z.preprocess(
(val) => (typeof val === 'string' ? parseInt(val, 10) : val),
z.number()
);
// 验证前清理数据
const TrimmedString = z.preprocess(
(val) => (typeof val === 'string' ? val.trim() : val),
z.string()
);
// 解析 JSON 字符串
const JsonSchema = z.preprocess(
(val) => (typeof val === 'string' ? JSON.parse(val) : val),
z.object({
name: z.string(),
age: z.number()
})
);
// 表单数据预处理
const FormDataSchema = z.preprocess(
(data) => {
// 将 FormData 转换为对象
if (data instanceof FormData) {
return Object.fromEntries(data.entries());
}
return data;
},
z.object({
name: z.string(),
email: z.string().email()
})
);
// 创建名义类型
const UserId = z.string().uuid().brand<'UserId'>();
type UserId = z.infer<typeof UserId>;
const Email = z.string().email().brand<'Email'>();
type Email = z.infer<typeof Email>;
// 防止混合相似类型
function getUserById(id: UserId) { /* ... */ }
function sendEmail(to: Email) { /* ... */ }
const userId = UserId.parse('123e4567-e89b-12d3-a456-426614174000');
const email = Email.parse('user@example.com');
getUserById(userId); // ✓
getUserById(email); // ✗ 类型错误
import { useForm } from 'react-hook-form';
import { zodResolver } from '@hookform/resolvers/zod';
import { z } from 'zod';
const FormSchema = z.object({
username: z.string().min(3, "Minimum 3 characters"),
email: z.string().email("Invalid email"),
age: z.number().min(18, "Must be 18+")
});
type FormData = z.infer<typeof FormSchema>;
function MyForm() {
const {
register,
handleSubmit,
formState: { errors }
} = useForm<FormData>({
resolver: zodResolver(FormSchema)
});
const onSubmit = (data: FormData) => {
// data 已验证且类型安全
console.log(data);
};
return (
<form onSubmit={handleSubmit(onSubmit)}>
<input {...register('username')} />
{errors.username && <span>{errors.username.message}</span>}
<input {...register('email')} />
{errors.email && <span>{errors.email.message}</span>}
<input type="number" {...register('age', { valueAsNumber: true })} />
{errors.age && <span>{errors.age.message}</span>}
<button type="submit">Submit</button>
</form>
);
}
import { z } from 'zod';
import { initTRPC } from '@trpc/server';
const t = initTRPC.create();
const router = t.router;
const publicProcedure = t.procedure;
// 输入/输出验证
const appRouter = router({
userById: publicProcedure
.input(z.object({
id: z.string().uuid()
}))
.output(z.object({
id: z.string().uuid(),
name: z.string(),
email: z.string().email()
}))
.query(async ({ input }) => {
const user = await db.user.findUnique({
where: { id: input.id }
});
return user; // 根据输出 schema 进行类型检查
}),
createUser: publicProcedure
.input(z.object({
name: z.string().min(2),
email: z.string().email(),
age: z.number().min(18)
}))
.mutation(async ({ input }) => {
return await db.user.create({ data: input });
})
});
export type AppRouter = typeof appRouter;
// app/api/users/route.ts
import { NextRequest, NextResponse } from 'next/server';
import { z } from 'zod';
const CreateUserSchema = z.object({
name: z.string().min(2),
email: z.string().email(),
age: z.number().min(18).optional()
});
export async function POST(request: NextRequest) {
try {
const body = await request.json();
const validatedData = CreateUserSchema.parse(body);
// validatedData 已类型化且已验证
const user = await createUser(validatedData);
return NextResponse.json(user, { status: 201 });
} catch (error) {
if (error instanceof z.ZodError) {
return NextResponse.json(
{ errors: error.flatten().fieldErrors },
{ status: 400 }
);
}
return NextResponse.json(
{ error: 'Internal server error' },
{ status: 500 }
);
}
}
// 查询参数验证
const SearchParamsSchema = z.object({
page: z.string().transform(Number).pipe(z.number().min(1)).default('1'),
limit: z.string().transform(Number).pipe(z.number().max(100)).default('10'),
sort: z.enum(['asc', 'desc']).default('asc')
});
export async function GET(request: NextRequest) {
const searchParams = Object.fromEntries(
request.nextUrl.searchParams.entries()
);
const params = SearchParamsSchema.parse(searchParams);
// params 是 { page: number, limit: number, sort: 'asc' | 'desc' }
const users = await getUsers(params);
return NextResponse.json(users);
}
import express from 'express';
import { z } from 'zod';
// 验证中间件
const validate = (schema: z.ZodSchema) => {
return (req: express.Request, res: express.Response, next: express.NextFunction) => {
try {
schema.parse(req.body);
next();
} catch (error) {
if (error instanceof z.ZodError) {
return res.status(400).json({
errors: error.flatten().fieldErrors
});
}
next(error);
}
};
};
const CreateUserSchema = z.object({
name: z.string(),
email: z.string().email(),
age: z.number().min(18)
});
app.post('/users', validate(CreateUserSchema), async (req, res) => {
// req.body 已验证(在 Express 中未类型化)
const user = await createUser(req.body);
res.json(user);
});
// 验证参数、查询、请求体
const validateRequest = (schema: {
params?: z.ZodSchema;
query?: z.ZodSchema;
body?: z.ZodSchema;
}) => {
return (req: express.Request, res: express.Response, next: express.NextFunction) => {
try {
if (schema.params) {
req.params = schema.params.parse(req.params);
}
if (schema.query) {
req.query = schema.query.parse(req.query);
}
if (schema.body) {
req.body = schema.body.parse(req.body);
}
next();
} catch (error) {
if (error instanceof z.ZodError) {
return res.status(400).json({ errors: error.issues });
}
next(error);
}
};
};
app.get(
'/users/:id',
validateRequest({
params: z.object({ id: z.string().uuid() }),
query: z.object({ include: z.string().optional() })
}),
async (req, res) => {
// 已验证的参数和查询
}
);
import { z } from 'zod';
import { pgTable, serial, text, integer } from 'drizzle-orm/pg-core';
import { createInsertSchema, createSelectSchema } from 'drizzle-zod';
// 定义表
export const users = pgTable('users', {
id: serial('id').primaryKey(),
name: text('name').notNull(),
email: text('email').notNull().unique(),
age: integer('age')
});
// 自动生成 schema
export const insertUserSchema = createInsertSchema(users);
export const selectUserSchema = createSelectSchema(users);
// 自定义验证
export const customInsertUserSchema = createInsertSchema(users, {
email: z.string().email(),
age: z.number().min(18).optional()
});
// 在应用中使用
type NewUser = z.infer<typeof insertUserSchema>;
type User = z.infer<typeof selectUserSchema>;
function createUser(data: unknown) {
const validatedData = insertUserSchema.parse(data);
return db.insert(users).values(validatedData);
}
// env.ts
import { z } from 'zod';
const envSchema = z.object({
NODE_ENV: z.enum(['development', 'production', 'test']),
DATABASE_URL: z.string().url(),
API_KEY: z.string().min(32),
PORT: z.string().transform(Number).pipe(z.number().min(1024)),
REDIS_HOST: z.string().default('localhost'),
REDIS_PORT: z.string().transform(Number).default('6379'),
LOG_LEVEL: z.enum(['debug', 'info', 'warn', 'error']).default('info')
});
// 启动时验证
export const env = envSchema.parse(process.env);
// 类型安全的环境变量
export type Env = z.infer<typeof envSchema>;
// 使用
console.log(`Server running on port ${env.PORT}`);
// env.PORT 是数字,不是字符串
// schemas/user.schema.ts
import { z } from 'zod';
// 可重用的原始类型
export const emailSchema = z.string().email();
export const uuidSchema = z.string().uuid();
export const passwordSchema = z.string()
.min(8)
.regex(/[A-Z]/, "Must contain uppercase")
.regex(/[0-9]/, "Must contain number");
// 基础 schema
export const baseUserSchema = z.object({
id: uuidSchema,
email: emailSchema,
name: z.string().min(2)
});
// 扩展 schema
export const createUserSchema = baseUserSchema.omit({ id: true }).extend({
password: passwordSchema,
confirmPassword: z.string()
}).refine(
data => data.password === data.confirmPassword,
{ message: "Passwords must match", path: ["confirmPassword"] }
);
export const updateUserSchema = baseUserSchema.partial().omit({ id: true });
// 导出类型
export type User = z.infer<typeof baseUserSchema>;
export type CreateUser = z.infer<typeof createUserSchema>;
export type UpdateUser = z.infer<typeof updateUserSchema>;
// 缓存解析过的 schema
const userSchemaCache = new Map<string, z.ZodSchema>();
function getCachedSchema(key: string, factory: () => z.ZodSchema) {
if (!userSchemaCache.has(key)) {
userSchemaCache.set(key, factory());
}
return userSchemaCache.get(key)!;
}
// 对大对象进行惰性验证
const lazyUserSchema = z.lazy(() => z.object({
// 仅在访问时验证
profile: complexProfileSchema,
settings: complexSettingsSchema
}));
// 数组的流式验证
async function validateLargeArray(items: unknown[]) {
const errors: z.ZodError[] = [];
for (const item of items) {
const result = ItemSchema.safeParse(item);
if (!result.success) {
errors.push(result.error);
}
}
return errors;
}
import { describe, it, expect } from 'vitest';
describe('UserSchema', () => {
it('validates correct user data', () => {
const validUser = {
email: 'user@example.com',
name: 'John Doe',
age: 25
};
expect(() => UserSchema.parse(validUser)).not.toThrow();
});
it('rejects invalid email', () => {
const invalidUser = {
email: 'not-an-email',
name: 'John',
age: 25
};
const result = UserSchema.safeParse(invalidUser);
expect(result.success).toBe(false);
if (!result.success) {
expect(result.error.issues[0].path).toEqual(['email']);
}
});
it('applies transforms correctly', () => {
const input = {
name: ' JOHN DOE ',
email: 'USER@EXAMPLE.COM'
};
const result = UserSchema.parse(input);
expect(result.name).toBe('john doe');
expect(result.email).toBe('user@example.com');
});
});
// 条件验证
const ConditionalSchema = z.object({
type: z.enum(['personal', 'business']),
data: z.any()
}).transform((val) => {
if (val.type === 'personal') {
return {
type: val.type,
data: PersonalDataSchema.parse(val.data)
};
} else {
return {
type: val.type,
data: BusinessDataSchema.parse(val.data)
};
}
});
// 分页 schema
export const paginationSchema = z.object({
page: z.number().min(1).default(1),
limit: z.number().min(1).max(100).default(20),
sort: z.string().optional(),
order: z.enum(['asc', 'desc']).default('asc')
});
// 过滤 schema
export const filterSchema = z.object({
search: z.string().optional(),
status: z.enum(['active', 'inactive', 'pending']).optional(),
dateFrom: z.string().datetime().optional(),
dateTo: z.string().datetime().optional()
});
// API 响应包装器
export const apiResponseSchema = <T extends z.ZodTypeAny>(dataSchema: T) =>
z.object({
success: z.boolean(),
data: dataSchema.optional(),
error: z.string().optional(),
timestamp: z.string().datetime()
});
const userResponseSchema = apiResponseSchema(UserSchema);
// Yup -> Zod
// Yup
const yupSchema = yup.object({
email: yup.string().email().required(),
age: yup.number().min(18).required()
});
// Zod 等效
const zodSchema = z.object({
email: z.string().email(),
age: z.number().min(18)
});
// Joi -> Zod
// Joi
const joiSchema = Joi.object({
email: Joi.string().email().required(),
age: Joi.number().min(18).required()
});
// Zod 等效(同上)
const zodSchema = z.object({
email: z.string().email(),
age: z.number().min(18)
});
// 主要区别:
// 1. Zod 字段默认是必需的
// 2. Zod 具有一流的 TypeScript 集成
// 3. Zod schema 是不可变的
// 4. Zod 具有更好的 tree-shaking
每周安装次数
455
仓库
GitHub 星标数
18
首次出现
2026年1月23日
安全审计
已安装于
cursor406
opencode154
gemini-cli146
codex140
github-copilot136
claude-code131
TypeScript-first schema validation library with static type inference. Define schemas once, get runtime validation and compile-time types automatically.
import { z } from 'zod';
// Define schema
const UserSchema = z.object({
id: z.string().uuid(),
email: z.string().email(),
age: z.number().min(18),
role: z.enum(['user', 'admin'])
});
// Infer TypeScript type
type User = z.infer<typeof UserSchema>;
// Validate data
const result = UserSchema.safeParse(data);
if (result.success) {
const user: User = result.data;
}
import { z } from 'zod';
// String with validation
const nameSchema = z.string()
.min(2, "Too short")
.max(50, "Too long")
.trim();
const emailSchema = z.string().email();
const urlSchema = z.string().url();
const uuidSchema = z.string().uuid();
const regexSchema = z.string().regex(/^[A-Z]{3}$/);
// Numbers
const ageSchema = z.number()
.int("Must be integer")
.positive()
.min(0)
.max(120);
const priceSchema = z.number()
.positive()
.multipleOf(0.01); // Currency precision
// Boolean
const isActiveSchema = z.boolean();
// Date
const createdAtSchema = z.date()
.min(new Date('2020-01-01'))
.max(new Date());
const dateStringSchema = z.string().datetime(); // ISO 8601
const dateOnlySchema = z.string().date(); // YYYY-MM-DD
// Literal values
const roleSchema = z.literal('admin');
const statusSchema = z.literal('pending');
// Enums
const ColorEnum = z.enum(['red', 'green', 'blue']);
type Color = z.infer<typeof ColorEnum>; // 'red' | 'green' | 'blue'
const NativeEnum = z.nativeEnum(MyEnum); // For TypeScript enums
// Nullable and Optional
const optionalString = z.string().optional(); // string | undefined
const nullableString = z.string().nullable(); // string | null
const nullishString = z.string().nullish(); // string | null | undefined
// Default values
const countSchema = z.number().default(0);
const settingsSchema = z.object({
theme: z.string().default('light'),
notifications: z.boolean().default(true)
});
// Basic object
const UserSchema = z.object({
id: z.string(),
email: z.string().email(),
name: z.string(),
age: z.number().optional()
});
// Nested objects
const AddressSchema = z.object({
street: z.string(),
city: z.string(),
country: z.string(),
zipCode: z.string()
});
const PersonSchema = z.object({
name: z.string(),
address: AddressSchema,
contacts: z.object({
email: z.string().email(),
phone: z.string().optional()
})
});
// Strict vs Passthrough
const strictSchema = z.object({ name: z.string() }).strict();
// Rejects unknown keys
const passthroughSchema = z.object({ name: z.string() }).passthrough();
// Allows unknown keys
const stripSchema = z.object({ name: z.string() }).strip();
// Removes unknown keys (default)
// Simple arrays
const stringArray = z.array(z.string());
const numberArray = z.array(z.number()).min(1).max(10);
// Array of objects
const UsersSchema = z.array(UserSchema);
// Non-empty arrays
const tagSchema = z.array(z.string()).nonempty("At least one tag required");
// Fixed-length arrays (tuples)
const coordinateSchema = z.tuple([z.number(), z.number()]);
type Coordinate = z.infer<typeof coordinateSchema>; // [number, number]
// Tuple with rest
const csvRowSchema = z.tuple([z.string(), z.number()]).rest(z.string());
// [string, number, ...string[]]
// Record (object with dynamic keys)
const userRolesSchema = z.record(
z.string(), // key type
z.enum(['admin', 'user', 'guest']) // value type
);
type UserRoles = z.infer<typeof userRolesSchema>;
// { [key: string]: 'admin' | 'user' | 'guest' }
// Map
const configMapSchema = z.map(
z.string(), // key
z.number() // value
);
// Set
const uniqueTagsSchema = z.set(z.string());
import { z } from 'zod';
// Infer output type
const UserSchema = z.object({
id: z.string(),
email: z.string().email(),
age: z.number()
});
type User = z.infer<typeof UserSchema>;
// { id: string; email: string; age: number }
// Infer input type (before transforms)
const TransformSchema = z.object({
date: z.string().transform(s => new Date(s))
});
type Input = z.input<typeof TransformSchema>;
// { date: string }
type Output = z.output<typeof TransformSchema>;
// { date: Date }
// Using inferred types in functions
function createUser(data: User): void {
// data is type-safe
}
function validateAndCreate(data: unknown): User | null {
const result = UserSchema.safeParse(data);
return result.success ? result.data : null;
}
// parse() - Throws on failure
try {
const user = UserSchema.parse(data);
// user is type User
} catch (error) {
if (error instanceof z.ZodError) {
console.error(error.issues);
}
}
// safeParse() - Returns result object
const result = UserSchema.safeParse(data);
if (result.success) {
const user = result.data; // type User
} else {
const errors = result.error.issues;
errors.forEach(err => {
console.log(`${err.path}: ${err.message}`);
});
}
// parseAsync() - For async refinements
const asyncResult = await UserSchema.parseAsync(data);
// safeParseAsync() - Safe async version
const asyncSafeResult = await UserSchema.safeParseAsync(data);
// Check if data matches schema without throwing
const isValid = UserSchema.safeParse(data).success;
// Custom type guards
function isUser(data: unknown): data is User {
return UserSchema.safeParse(data).success;
}
if (isUser(unknownData)) {
// TypeScript knows unknownData is User
console.log(unknownData.email);
}
// Extend (add fields)
const BaseUserSchema = z.object({
id: z.string(),
email: z.string()
});
const AdminUserSchema = BaseUserSchema.extend({
role: z.literal('admin'),
permissions: z.array(z.string())
});
// Merge (combine schemas)
const NameSchema = z.object({ name: z.string() });
const AgeSchema = z.object({ age: z.number() });
const PersonSchema = NameSchema.merge(AgeSchema);
// { name: string; age: number }
// Pick (select fields)
const UserIdEmail = UserSchema.pick({ id: true, email: true });
// Omit (exclude fields)
const UserWithoutId = UserSchema.omit({ id: true });
// Partial (make all fields optional)
const PartialUser = UserSchema.partial();
// DeepPartial (recursive partial)
const DeepPartialUser = UserSchema.deepPartial();
// Required (make all fields required)
const RequiredUser = UserSchema.required();
// Union (OR)
const StringOrNumber = z.union([z.string(), z.number()]);
// Shorthand
const StringOrNumberAlt = z.string().or(z.number());
// Discriminated Union (tagged union)
const SuccessResponse = z.object({
status: z.literal('success'),
data: z.any()
});
const ErrorResponse = z.object({
status: z.literal('error'),
message: z.string()
});
const ApiResponse = z.discriminatedUnion('status', [
SuccessResponse,
ErrorResponse
]);
// Intersection (AND)
const User = z.object({ name: z.string() });
const Timestamps = z.object({
createdAt: z.date(),
updatedAt: z.date()
});
const UserWithTimestamps = z.intersection(User, Timestamps);
// Shorthand
const UserWithTimestampsAlt = User.and(Timestamps);
// Transform data after validation
const StringToNumber = z.string().transform(val => parseInt(val, 10));
const DateSchema = z.string().transform(str => new Date(str));
// Chaining transforms
const TrimmedLowercase = z.string()
.transform(s => s.trim())
.transform(s => s.toLowerCase());
// Transform with validation
const PositiveStringNumber = z.string()
.transform(val => parseInt(val, 10))
.refine(n => n > 0, "Must be positive");
// Complex transformations
const UserInputSchema = z.object({
name: z.string().transform(s => s.trim()),
email: z.string().email().transform(s => s.toLowerCase()),
birthDate: z.string().transform(s => new Date(s)),
tags: z.string().transform(s => s.split(',').map(t => t.trim()))
});
type UserInput = z.input<typeof UserInputSchema>;
// { name: string; email: string; birthDate: string; tags: string }
type User = z.output<typeof UserInputSchema>;
// { name: string; email: string; birthDate: Date; tags: string[] }
// Simple refinement
const PasswordSchema = z.string()
.min(8)
.refine(
val => /[A-Z]/.test(val),
"Must contain uppercase letter"
)
.refine(
val => /[0-9]/.test(val),
"Must contain number"
);
// Refinement with custom error
const UniqueEmailSchema = z.string().email().refine(
async (email) => {
const exists = await checkEmailExists(email);
return !exists;
},
{ message: "Email already taken" }
);
// Object-level refinement
const PasswordMatchSchema = z.object({
password: z.string(),
confirmPassword: z.string()
}).refine(
data => data.password === data.confirmPassword,
{
message: "Passwords don't match",
path: ["confirmPassword"] // Error location
}
);
// Multiple field validation
const DateRangeSchema = z.object({
startDate: z.date(),
endDate: z.date()
}).refine(
data => data.endDate > data.startDate,
{
message: "End date must be after start date",
path: ["endDate"]
}
);
// Access to Zod context for complex validation
const ComplexSchema = z.object({
type: z.enum(['email', 'phone']),
value: z.string()
}).superRefine((data, ctx) => {
if (data.type === 'email') {
const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
if (!emailRegex.test(data.value)) {
ctx.addIssue({
code: z.ZodIssueCode.custom,
message: "Invalid email format",
path: ["value"]
});
}
} else if (data.type === 'phone') {
const phoneRegex = /^\+?[1-9]\d{1,14}$/;
if (!phoneRegex.test(data.value)) {
ctx.addIssue({
code: z.ZodIssueCode.custom,
message: "Invalid phone format",
path: ["value"]
});
}
}
});
// Multiple issues
const RegistrationSchema = z.object({
username: z.string(),
email: z.string(),
age: z.number()
}).superRefine(async (data, ctx) => {
// Check username availability
if (await usernameTaken(data.username)) {
ctx.addIssue({
code: z.ZodIssueCode.custom,
message: "Username taken",
path: ["username"]
});
}
// Check email availability
if (await emailTaken(data.email)) {
ctx.addIssue({
code: z.ZodIssueCode.custom,
message: "Email already registered",
path: ["email"]
});
}
// Age restriction
if (data.age < 18) {
ctx.addIssue({
code: z.ZodIssueCode.custom,
message: "Must be 18 or older",
path: ["age"]
});
}
});
// Field-level messages
const UserSchema = z.object({
email: z.string().email({ message: "Invalid email address" }),
age: z.number({
required_error: "Age is required",
invalid_type_error: "Age must be a number"
}).min(18, { message: "Must be 18 or older" })
});
// Global error map
import { z } from 'zod';
const customErrorMap: z.ZodErrorMap = (issue, ctx) => {
if (issue.code === z.ZodIssueCode.invalid_type) {
if (issue.expected === "string") {
return { message: "This field must be text" };
}
}
if (issue.code === z.ZodIssueCode.too_small) {
if (issue.type === "string") {
return { message: `Minimum ${issue.minimum} characters required` };
}
}
return { message: ctx.defaultError };
};
z.setErrorMap(customErrorMap);
// Flatten errors for forms
const result = UserSchema.safeParse(data);
if (!result.success) {
const flatErrors = result.error.flatten();
console.log(flatErrors.formErrors); // Top-level errors
console.log(flatErrors.fieldErrors);
// { email: ["Invalid email"], age: ["Must be 18+"] }
}
// Format for API response
function formatZodError(error: z.ZodError) {
return error.issues.map(issue => ({
field: issue.path.join('.'),
message: issue.message
}));
}
// Example usage
const result = UserSchema.safeParse(data);
if (!result.success) {
return res.status(400).json({
errors: formatZodError(result.error)
});
}
import { z } from 'zod';
// Async refinement
const UsernameSchema = z.string().refine(
async (username) => {
const available = await checkUsernameAvailable(username);
return available;
},
{ message: "Username already taken" }
);
// Must use parseAsync or safeParseAsync
const result = await UsernameSchema.safeParseAsync("john_doe");
// Complex async validation
const RegistrationSchema = z.object({
username: z.string().refine(
async (val) => !(await usernameTaken(val)),
"Username taken"
),
email: z.string().email().refine(
async (val) => !(await emailTaken(val)),
"Email already registered"
),
inviteCode: z.string().refine(
async (code) => await validateInviteCode(code),
"Invalid invite code"
)
});
// Validate
const userData = await RegistrationSchema.parseAsync(input);
// With error handling
const result = await RegistrationSchema.safeParseAsync(input);
if (!result.success) {
// Handle validation errors
}
// Self-referential schemas
type Category = {
name: string;
subcategories: Category[];
};
const CategorySchema: z.ZodType<Category> = z.lazy(() =>
z.object({
name: z.string(),
subcategories: z.array(CategorySchema)
})
);
// Tree structure
type TreeNode = {
value: number;
left?: TreeNode;
right?: TreeNode;
};
const TreeNodeSchema: z.ZodType<TreeNode> = z.lazy(() =>
z.object({
value: z.number(),
left: TreeNodeSchema.optional(),
right: TreeNodeSchema.optional()
})
);
// Type-safe union based on discriminator field
const Circle = z.object({
kind: z.literal('circle'),
radius: z.number()
});
const Rectangle = z.object({
kind: z.literal('rectangle'),
width: z.number(),
height: z.number()
});
const Triangle = z.object({
kind: z.literal('triangle'),
base: z.number(),
height: z.number()
});
const Shape = z.discriminatedUnion('kind', [
Circle,
Rectangle,
Triangle
]);
type Shape = z.infer<typeof Shape>;
// TypeScript can narrow based on discriminator
function calculateArea(shape: Shape): number {
switch (shape.kind) {
case 'circle':
return Math.PI * shape.radius ** 2;
case 'rectangle':
return shape.width * shape.height;
case 'triangle':
return (shape.base * shape.height) / 2;
}
}
// Transform before validation
const NumberFromString = z.preprocess(
(val) => (typeof val === 'string' ? parseInt(val, 10) : val),
z.number()
);
// Clean data before validation
const TrimmedString = z.preprocess(
(val) => (typeof val === 'string' ? val.trim() : val),
z.string()
);
// Parse JSON strings
const JsonSchema = z.preprocess(
(val) => (typeof val === 'string' ? JSON.parse(val) : val),
z.object({
name: z.string(),
age: z.number()
})
);
// Form data preprocessing
const FormDataSchema = z.preprocess(
(data) => {
// Convert FormData to object
if (data instanceof FormData) {
return Object.fromEntries(data.entries());
}
return data;
},
z.object({
name: z.string(),
email: z.string().email()
})
);
// Create nominal types
const UserId = z.string().uuid().brand<'UserId'>();
type UserId = z.infer<typeof UserId>;
const Email = z.string().email().brand<'Email'>();
type Email = z.infer<typeof Email>;
// Prevents mixing similar types
function getUserById(id: UserId) { /* ... */ }
function sendEmail(to: Email) { /* ... */ }
const userId = UserId.parse('123e4567-e89b-12d3-a456-426614174000');
const email = Email.parse('user@example.com');
getUserById(userId); // ✓
getUserById(email); // ✗ Type error
import { useForm } from 'react-hook-form';
import { zodResolver } from '@hookform/resolvers/zod';
import { z } from 'zod';
const FormSchema = z.object({
username: z.string().min(3, "Minimum 3 characters"),
email: z.string().email("Invalid email"),
age: z.number().min(18, "Must be 18+")
});
type FormData = z.infer<typeof FormSchema>;
function MyForm() {
const {
register,
handleSubmit,
formState: { errors }
} = useForm<FormData>({
resolver: zodResolver(FormSchema)
});
const onSubmit = (data: FormData) => {
// data is validated and typed
console.log(data);
};
return (
<form onSubmit={handleSubmit(onSubmit)}>
<input {...register('username')} />
{errors.username && <span>{errors.username.message}</span>}
<input {...register('email')} />
{errors.email && <span>{errors.email.message}</span>}
<input type="number" {...register('age', { valueAsNumber: true })} />
{errors.age && <span>{errors.age.message}</span>}
<button type="submit">Submit</button>
</form>
);
}
import { z } from 'zod';
import { initTRPC } from '@trpc/server';
const t = initTRPC.create();
const router = t.router;
const publicProcedure = t.procedure;
// Input/output validation
const appRouter = router({
userById: publicProcedure
.input(z.object({
id: z.string().uuid()
}))
.output(z.object({
id: z.string().uuid(),
name: z.string(),
email: z.string().email()
}))
.query(async ({ input }) => {
const user = await db.user.findUnique({
where: { id: input.id }
});
return user; // Type-checked against output schema
}),
createUser: publicProcedure
.input(z.object({
name: z.string().min(2),
email: z.string().email(),
age: z.number().min(18)
}))
.mutation(async ({ input }) => {
return await db.user.create({ data: input });
})
});
export type AppRouter = typeof appRouter;
// app/api/users/route.ts
import { NextRequest, NextResponse } from 'next/server';
import { z } from 'zod';
const CreateUserSchema = z.object({
name: z.string().min(2),
email: z.string().email(),
age: z.number().min(18).optional()
});
export async function POST(request: NextRequest) {
try {
const body = await request.json();
const validatedData = CreateUserSchema.parse(body);
// validatedData is typed and validated
const user = await createUser(validatedData);
return NextResponse.json(user, { status: 201 });
} catch (error) {
if (error instanceof z.ZodError) {
return NextResponse.json(
{ errors: error.flatten().fieldErrors },
{ status: 400 }
);
}
return NextResponse.json(
{ error: 'Internal server error' },
{ status: 500 }
);
}
}
// Query parameter validation
const SearchParamsSchema = z.object({
page: z.string().transform(Number).pipe(z.number().min(1)).default('1'),
limit: z.string().transform(Number).pipe(z.number().max(100)).default('10'),
sort: z.enum(['asc', 'desc']).default('asc')
});
export async function GET(request: NextRequest) {
const searchParams = Object.fromEntries(
request.nextUrl.searchParams.entries()
);
const params = SearchParamsSchema.parse(searchParams);
// params is { page: number, limit: number, sort: 'asc' | 'desc' }
const users = await getUsers(params);
return NextResponse.json(users);
}
import express from 'express';
import { z } from 'zod';
// Validation middleware
const validate = (schema: z.ZodSchema) => {
return (req: express.Request, res: express.Response, next: express.NextFunction) => {
try {
schema.parse(req.body);
next();
} catch (error) {
if (error instanceof z.ZodError) {
return res.status(400).json({
errors: error.flatten().fieldErrors
});
}
next(error);
}
};
};
const CreateUserSchema = z.object({
name: z.string(),
email: z.string().email(),
age: z.number().min(18)
});
app.post('/users', validate(CreateUserSchema), async (req, res) => {
// req.body is validated (not typed in Express)
const user = await createUser(req.body);
res.json(user);
});
// Validate params, query, body
const validateRequest = (schema: {
params?: z.ZodSchema;
query?: z.ZodSchema;
body?: z.ZodSchema;
}) => {
return (req: express.Request, res: express.Response, next: express.NextFunction) => {
try {
if (schema.params) {
req.params = schema.params.parse(req.params);
}
if (schema.query) {
req.query = schema.query.parse(req.query);
}
if (schema.body) {
req.body = schema.body.parse(req.body);
}
next();
} catch (error) {
if (error instanceof z.ZodError) {
return res.status(400).json({ errors: error.issues });
}
next(error);
}
};
};
app.get(
'/users/:id',
validateRequest({
params: z.object({ id: z.string().uuid() }),
query: z.object({ include: z.string().optional() })
}),
async (req, res) => {
// Validated params and query
}
);
import { z } from 'zod';
import { pgTable, serial, text, integer } from 'drizzle-orm/pg-core';
import { createInsertSchema, createSelectSchema } from 'drizzle-zod';
// Define table
export const users = pgTable('users', {
id: serial('id').primaryKey(),
name: text('name').notNull(),
email: text('email').notNull().unique(),
age: integer('age')
});
// Auto-generate schemas
export const insertUserSchema = createInsertSchema(users);
export const selectUserSchema = createSelectSchema(users);
// Customize validation
export const customInsertUserSchema = createInsertSchema(users, {
email: z.string().email(),
age: z.number().min(18).optional()
});
// Use in application
type NewUser = z.infer<typeof insertUserSchema>;
type User = z.infer<typeof selectUserSchema>;
function createUser(data: unknown) {
const validatedData = insertUserSchema.parse(data);
return db.insert(users).values(validatedData);
}
// env.ts
import { z } from 'zod';
const envSchema = z.object({
NODE_ENV: z.enum(['development', 'production', 'test']),
DATABASE_URL: z.string().url(),
API_KEY: z.string().min(32),
PORT: z.string().transform(Number).pipe(z.number().min(1024)),
REDIS_HOST: z.string().default('localhost'),
REDIS_PORT: z.string().transform(Number).default('6379'),
LOG_LEVEL: z.enum(['debug', 'info', 'warn', 'error']).default('info')
});
// Validate on startup
export const env = envSchema.parse(process.env);
// Type-safe environment variables
export type Env = z.infer<typeof envSchema>;
// Usage
console.log(`Server running on port ${env.PORT}`);
// env.PORT is number, not string
// schemas/user.schema.ts
import { z } from 'zod';
// Reusable primitives
export const emailSchema = z.string().email();
export const uuidSchema = z.string().uuid();
export const passwordSchema = z.string()
.min(8)
.regex(/[A-Z]/, "Must contain uppercase")
.regex(/[0-9]/, "Must contain number");
// Base schemas
export const baseUserSchema = z.object({
id: uuidSchema,
email: emailSchema,
name: z.string().min(2)
});
// Extended schemas
export const createUserSchema = baseUserSchema.omit({ id: true }).extend({
password: passwordSchema,
confirmPassword: z.string()
}).refine(
data => data.password === data.confirmPassword,
{ message: "Passwords must match", path: ["confirmPassword"] }
);
export const updateUserSchema = baseUserSchema.partial().omit({ id: true });
// Export types
export type User = z.infer<typeof baseUserSchema>;
export type CreateUser = z.infer<typeof createUserSchema>;
export type UpdateUser = z.infer<typeof updateUserSchema>;
// Cache parsed schemas
const userSchemaCache = new Map<string, z.ZodSchema>();
function getCachedSchema(key: string, factory: () => z.ZodSchema) {
if (!userSchemaCache.has(key)) {
userSchemaCache.set(key, factory());
}
return userSchemaCache.get(key)!;
}
// Lazy validation for large objects
const lazyUserSchema = z.lazy(() => z.object({
// Only validated when accessed
profile: complexProfileSchema,
settings: complexSettingsSchema
}));
// Streaming validation for arrays
async function validateLargeArray(items: unknown[]) {
const errors: z.ZodError[] = [];
for (const item of items) {
const result = ItemSchema.safeParse(item);
if (!result.success) {
errors.push(result.error);
}
}
return errors;
}
import { describe, it, expect } from 'vitest';
describe('UserSchema', () => {
it('validates correct user data', () => {
const validUser = {
email: 'user@example.com',
name: 'John Doe',
age: 25
};
expect(() => UserSchema.parse(validUser)).not.toThrow();
});
it('rejects invalid email', () => {
const invalidUser = {
email: 'not-an-email',
name: 'John',
age: 25
};
const result = UserSchema.safeParse(invalidUser);
expect(result.success).toBe(false);
if (!result.success) {
expect(result.error.issues[0].path).toEqual(['email']);
}
});
it('applies transforms correctly', () => {
const input = {
name: ' JOHN DOE ',
email: 'USER@EXAMPLE.COM'
};
const result = UserSchema.parse(input);
expect(result.name).toBe('john doe');
expect(result.email).toBe('user@example.com');
});
});
// Conditional validation
const ConditionalSchema = z.object({
type: z.enum(['personal', 'business']),
data: z.any()
}).transform((val) => {
if (val.type === 'personal') {
return {
type: val.type,
data: PersonalDataSchema.parse(val.data)
};
} else {
return {
type: val.type,
data: BusinessDataSchema.parse(val.data)
};
}
});
// Pagination schema
export const paginationSchema = z.object({
page: z.number().min(1).default(1),
limit: z.number().min(1).max(100).default(20),
sort: z.string().optional(),
order: z.enum(['asc', 'desc']).default('asc')
});
// Filter schema
export const filterSchema = z.object({
search: z.string().optional(),
status: z.enum(['active', 'inactive', 'pending']).optional(),
dateFrom: z.string().datetime().optional(),
dateTo: z.string().datetime().optional()
});
// API response wrapper
export const apiResponseSchema = <T extends z.ZodTypeAny>(dataSchema: T) =>
z.object({
success: z.boolean(),
data: dataSchema.optional(),
error: z.string().optional(),
timestamp: z.string().datetime()
});
const userResponseSchema = apiResponseSchema(UserSchema);
// Yup -> Zod
// Yup
const yupSchema = yup.object({
email: yup.string().email().required(),
age: yup.number().min(18).required()
});
// Zod equivalent
const zodSchema = z.object({
email: z.string().email(),
age: z.number().min(18)
});
// Joi -> Zod
// Joi
const joiSchema = Joi.object({
email: Joi.string().email().required(),
age: Joi.number().min(18).required()
});
// Zod equivalent (same as above)
const zodSchema = z.object({
email: z.string().email(),
age: z.number().min(18)
});
// Key differences:
// 1. Zod fields are required by default
// 2. Zod has first-class TypeScript integration
// 3. Zod schemas are immutable
// 4. Zod has better tree-shaking
Weekly Installs
455
Repository
GitHub Stars
18
First Seen
Jan 23, 2026
Security Audits
Gen Agent Trust HubPassSocketPassSnykPass
Installed on
cursor406
opencode154
gemini-cli146
codex140
github-copilot136
claude-code131
agent-browser 浏览器自动化工具 - Vercel Labs 命令行网页操作与测试
140,500 周安装