Zod 模式验证
Zod TypeScript优先的模式验证示例,包括原语、对象、数组、转换、自定义验证器和错误处理
💻 Zod 基础和原语类型 typescript
🟢 simple
⭐⭐
基础Zod模式验证,包括原始类型、基本验证和简单转换
⏱️ 25 min
🏷️ zod, validation, typescript, runtime
Prerequisites:
TypeScript basics, JavaScript
// Zod Basics and Primitive Types Example
import { z } from 'zod';
// 1. Primitive Types
const stringSchema = z.string();
const numberSchema = z.number();
const booleanSchema = z.boolean();
const dateSchema = z.date();
const undefinedSchema = z.undefined();
const nullSchema = z.null();
// Test primitive validation
console.log(stringSchema.parse('hello')); // ✅ 'hello'
console.log(numberSchema.parse(42)); // ✅ 42
console.log(booleanSchema.parse(true)); // ✅ true
console.log(dateSchema.parse(new Date())); // ✅ Date object
console.log(undefinedSchema.parse(undefined)); // ✅ undefined
console.log(nullSchema.parse(null)); // ✅ null
// 2. Type Constraints
const minStringLength = z.string().min(5);
const maxStringLength = z.string().max(10);
const emailRegex = z.string().email();
const urlRegex = z.string().url();
console.log(minStringLength.parse('hello world')); // ✅
console.log(maxStringLength.parse('short')); // ✅
console.log(emailRegex.parse('[email protected]')); // ✅
console.log(urlRegex.parse('https://example.com')); // ✅
// 3. Number Constraints
const positiveNumber = z.number().positive();
const nonNegativeNumber = z.number().nonnegative();
const maxNumber = z.number().max(100);
const minNumber = z.number().min(18);
const intNumber = z.number().int();
console.log(positiveNumber.parse(10)); // ✅
console.log(nonNegativeNumber.parse(0)); // ✅
console.log(maxNumber.parse(50)); // ✅
console.log(minNumber.parse(25)); // ✅
console.log(intNumber.parse(42)); // ✅
// 4. Optional and Nullable Types
const optionalString = z.string().optional();
const nullableString = z.string().nullable();
const optionalOrNullableString = z.string().optional().nullable();
console.log(optionalString.parse('hello')); // ✅ 'hello'
console.log(optionalString.parse(undefined)); // ✅ undefined
console.log(nullableString.parse(null)); // ✅ null
console.log(nullableString.parse('hello')); // ✅ 'hello'
// 5. Default Values
const stringWithDefault = z.string().default('default value');
const optionalStringWithDefault = z.string().optional().default('optional default');
console.log(stringWithDefault.parse(undefined)); // ✅ 'default value'
console.log(optionalStringWithDefault.parse(undefined)); // ✅ 'optional default'
// 6. Enum-like Values
const directionSchema = z.enum(['north', 'south', 'east', 'west']);
const brandSchema = z.enum(['Nike', 'Adidas', 'Puma', 'Reebok']);
console.log(directionSchema.parse('north')); // ✅ 'north'
console.log(brandSchema.parse('Nike')); // ✅ 'Nike'
// 7. Literal Types
const trueSchema = z.literal(true);
const numberLiteral = z.literal(42);
const stringLiteral = z.literal('specific-value');
console.log(trueSchema.parse(true)); // ✅ true
console.log(numberLiteral.parse(42)); // ✅ 42
console.log(stringLiteral.parse('specific-value')); // ✅ 'specific-value'
// 8. Union Types
const stringOrNumber = z.union([z.string(), z.number()]);
const colorSchema = z.union([z.literal('red'), z.literal('green'), z.literal('blue')]);
console.log(stringOrNumber.parse('hello')); // ✅ 'hello'
console.log(stringOrNumber.parse(42)); // ✅ 42
console.log(colorSchema.parse('red')); // ✅ 'red'
// 9. Intersection Types
const personName = z.object({
firstName: z.string(),
lastName: z.string(),
});
const personAge = z.object({
age: z.number().positive(),
});
const personWithDetails = z.intersection(personName, personAge);
console.log(personWithDetails.parse({
firstName: 'John',
lastName: 'Doe',
age: 30
})); // ✅
// 10. Safe Parsing
const result = stringSchema.safeParse(123);
if (result.success) {
console.log('Valid:', result.data);
} else {
console.log('Invalid:', result.error);
}
// 11. Custom Validation
const customEmailSchema = z.string().email().refine((email) => {
return email.endsWith('@company.com');
}, {
message: 'Email must be from @company.com domain',
path: ['domain']
});
try {
customEmailSchema.parse('[email protected]'); // ✅
customEmailSchema.parse('[email protected]'); // ❌
} catch (error) {
console.error('Custom validation error:', error);
}
// 12. Transformations
const stringToNumber = z.string().transform((str) => parseInt(str, 10));
const uppercaseString = z.string().transform((str) => str.toUpperCase());
const trimmedString = z.string().transform((str) => str.trim());
const transformedNumber = stringToNumber.parse('123'); // ✅ 123 (number)
const transformedUpper = uppercaseString.parse('hello'); // ✅ 'HELLO'
const transformedTrim = trimmedString.parse(' hello '); // ✅ 'hello'
// 13. Preprocessing and Postprocessing
const preprocessSchema = z.string().transform((str) => str.trim().toLowerCase());
const postprocessSchema = z.string().transform((str) => {
return {
original: str,
length: str.length,
uppercase: str.toUpperCase()
};
});
const preprocessResult = preprocessSchema.parse(' HELLO '); // ✅ 'hello'
const postprocessResult = postprocessSchema.parse('hello'); // ✅ { original: 'hello', length: 5, uppercase: 'HELLO' }
// 14. Pipeline Transformations
const dateToTimestamp = z.date().transform((date) => date.getTime());
const timestampToDate = z.number().transform((timestamp) => new Date(timestamp));
const timestamp = dateToTimestamp.parse(new Date()); // ✅ timestamp (number)
const date = timestampToDate.parse(Date.now()); // ✅ Date object
// 15. Error Customization
const customErrorSchema = z.string({
errorMap: (issue, ctx) => {
if (issue.code === z.ZodIssueCode.invalid_string) {
return { message: 'Custom error message for invalid string' };
}
if (issue.code === z.ZodIssueCode.too_small) {
return { message: 'String is too short!' };
}
return { message: ctx.defaultError };
},
}).min(5);
try {
customErrorSchema.parse('abc'); // ❌ Custom error
} catch (error) {
console.log('Custom error message');
}
// 16. Advanced Primitive Examples
// Password validation with multiple requirements
const passwordSchema = z.string()
.min(8, 'Password must be at least 8 characters')
.max(100, 'Password must be less than 100 characters')
.regex(/[A-Z]/, 'Password must contain at least one uppercase letter')
.regex(/[a-z]/, 'Password must contain at least one lowercase letter')
.regex(/[0-9]/, 'Password must contain at least one number')
.regex(/[^A-Za-z0-9]/, 'Password must contain at least one special character');
// Username validation
const usernameSchema = z.string()
.min(3, 'Username must be at least 3 characters')
.max(20, 'Username must be less than 20 characters')
.regex(/^[a-zA-Z0-9_]+$/, 'Username can only contain letters, numbers, and underscores')
.refine((username) => !username.startsWith('_'), {
message: 'Username cannot start with an underscore'
})
.refine((username) => !username.endsWith('_'), {
message: 'Username cannot end with an underscore'
});
// Phone number validation
const phoneNumberSchema = z.string()
.regex(/^+?[1-9]d{1,14}$/, 'Invalid phone number format')
.transform((phone) => {
// Normalize phone number format
return phone.replace(/[^0-9+]/g, '');
});
// URL validation with protocols
const urlSchema = z.string().url().refine((url) => {
const protocols = ['http:', 'https:', 'ftp:'];
try {
const parsedUrl = new URL(url);
return protocols.includes(parsedUrl.protocol);
} catch {
return false;
}
}, {
message: 'URL must use http, https, or ftp protocol'
});
// Color validation
const colorHexSchema = z.string()
.regex(/^#([A-Fa-f0-9]{6}|[A-Fa-f0-9]{3})$/, 'Invalid hex color format')
.transform((hex) => hex.toLowerCase());
const colorRgbSchema = z.string()
.regex(/^rgb(s*d+s*,s*d+s*,s*d+s*)$/i, 'Invalid RGB format')
.transform((rgb) => {
const matches = rgb.match(/d+/g);
if (!matches || matches.length !== 3) return null;
return {
r: parseInt(matches[0]),
g: parseInt(matches[1]),
b: parseInt(matches[2])
};
});
// Test advanced validation
try {
passwordSchema.parse('MySecureP@ssw0rd!'); // ✅
usernameSchema.parse('john_doe123'); // ✅
phoneNumberSchema.parse('+1-555-123-4567'); // ✅
urlSchema.parse('https://example.com'); // ✅
colorHexSchema.parse('#FF5733'); // ✅
colorRgbSchema.parse('rgb(255, 87, 51)'); // ✅
} catch (error) {
console.error('Advanced validation error:', error);
}
// 17. Usage Examples
// Form validation function
function validateUserInput(userData: unknown) {
const userSchema = z.object({
name: z.string().min(2).max(50),
email: z.string().email(),
age: z.number().min(18).max(120),
website: z.string().url().optional(),
bio: z.string().max(500).optional()
});
const result = userSchema.safeParse(userData);
if (result.success) {
return { success: true, data: result.data };
} else {
return {
success: false,
errors: result.error.issues.map(issue => ({
field: issue.path.join('.'),
message: issue.message
}))
};
}
}
// API response validation
function validateApiResponse(response: unknown) {
const responseSchema = z.object({
success: z.boolean(),
data: z.unknown().optional(),
error: z.string().optional(),
timestamp: z.string()
});
return responseSchema.parse(response);
}
// Environment variable validation
function validateEnvVars(envVars: Record<string, unknown>) {
const envSchema = z.object({
NODE_ENV: z.enum(['development', 'production', 'test']),
PORT: z.string().transform(Number).pipe(z.number().positive()),
DATABASE_URL: z.string().url(),
JWT_SECRET: z.string().min(32),
API_KEY: z.string().min(16),
ENABLE_LOGGING: z.string().transform(Boolean).pipe(z.boolean()).default('true'),
MAX_CONNECTIONS: z.string().transform(Number).pipe(z.number().positive()).default('10')
});
return envSchema.parse(envVars);
}
// Example usage
console.log('\n--- Usage Examples ---');
const userInput = {
name: 'John Doe',
email: '[email protected]',
age: 25,
website: 'https://johndoe.com',
bio: 'Software developer'
};
const userValidation = validateUserInput(userInput);
console.log('User validation:', userValidation);
const apiResponse = {
success: true,
data: { message: 'Hello World' },
timestamp: new Date().toISOString()
};
const validatedResponse = validateApiResponse(apiResponse);
console.log('API response validation:', validatedResponse);
const envVars = {
NODE_ENV: 'development',
PORT: '3000',
DATABASE_URL: 'https://localhost:5432/mydb',
JWT_SECRET: 'my-super-secret-jwt-key-32-chars-long',
API_KEY: 'my-api-key-16-chars'
};
const validatedEnv = validateEnvVars(envVars);
console.log('Environment validation:', validatedEnv);
export {
stringSchema,
numberSchema,
booleanSchema,
dateSchema,
userSchema: z.object({
name: z.string().min(2).max(50),
email: z.string().email(),
age: z.number().min(18).max(120),
website: z.string().url().optional(),
bio: z.string().max(500).optional()
}),
passwordSchema,
usernameSchema,
validateUserInput,
validateApiResponse,
validateEnvVars
};
💻 Zod 对象和数组 typescript
🟡 intermediate
⭐⭐⭐
复杂的Zod模式,用于对象、数组、嵌套结构和高级验证模式
⏱️ 40 min
🏷️ zod, validation, typescript, schemas, objects
Prerequisites:
Zod basics, TypeScript, Advanced data structures
// Zod Objects and Arrays Example
import { z } from 'zod';
// 1. Basic Object Schema
const userSchema = z.object({
id: z.string().uuid(),
name: z.string().min(2).max(50),
email: z.string().email(),
age: z.number().min(13).max(120),
isActive: z.boolean().default(true),
createdAt: z.date().default(new Date()),
});
type User = z.infer<typeof userSchema>;
const validUser = {
id: '550e8400-e29b-41d4-a716-446655440000',
name: 'John Doe',
email: '[email protected]',
age: 30,
isActive: true,
createdAt: new Date()
};
console.log(userSchema.parse(validUser)); // ✅
// 2. Partial Object (for updates)
const updateUserSchema = userSchema.partial();
const updateData = {
name: 'Jane Doe',
age: 28
};
console.log(updateUserSchema.parse(updateData)); // ✅
// 3. Required Object (no optional/extra properties)
const strictUserSchema = z.object({
name: z.string(),
email: z.string().email()
}).strict();
console.log(strictUserSchema.parse({ name: 'John', email: '[email protected]' })); // ✅
// strictUserSchema.parse({ name: 'John', email: '[email protected]', extra: 'value' }); // ❌
// 4. Object with Passthrough (allow extra properties)
const passthroughUserSchema = z.object({
name: z.string(),
email: z.string().email()
}).passthrough();
console.log(passthroughUserSchema.parse({
name: 'John',
email: '[email protected]',
role: 'admin',
department: 'IT'
})); // ✅, preserves extra properties
// 5. Object with Catchall (custom handling for extra properties)
const catchallUserSchema = z.object({
name: z.string(),
email: z.string().email()
}).catchall(z.string());
console.log(catchallUserSchema.parse({
name: 'John',
email: '[email protected]',
role: 'admin', // ✅, string value
score: 100 // ❌, not a string
}));
// 6. Array Schemas
const stringArraySchema = z.array(z.string());
const numberArraySchema = z.array(z.number());
const userArraySchema = z.array(userSchema);
console.log(stringArraySchema.parse(['hello', 'world'])); // ✅
console.log(numberArraySchema.parse([1, 2, 3])); // ✅
console.log(userArraySchema.parse([validUser, validUser])); // ✅
// 7. Array Constraints
const minLengthArray = z.array(z.string()).min(2);
const maxLengthArray = z.array(z.number()).max(5);
const exactLengthArray = z.array(z.string()).length(3);
console.log(minLengthArray.parse(['a', 'b', 'c'])); // ✅
console.log(maxLengthArray.parse([1, 2, 3])); // ✅
console.log(exactLengthArray.parse(['x', 'y', 'z'])); // ✅
// 8. Non-empty Array
const nonEmptyArray = z.array(z.string()).nonempty();
console.log(nonEmptyArray.parse(['hello'])); // ✅
// nonEmptyArray.parse([]); // ❌
// 9. Set-like Array (unique values)
const uniqueArray = z.array(z.string()).refine(
(arr) => new Set(arr).size === arr.length,
{ message: 'Array must contain unique values' }
);
console.log(uniqueArray.parse(['a', 'b', 'c'])); // ✅
// uniqueArray.parse(['a', 'b', 'a']); // ❌
// 10. Nested Objects
const addressSchema = z.object({
street: z.string(),
city: z.string(),
state: z.string().length(2),
zipCode: z.string().regex(/^d{5}(-d{4})?$/),
country: z.string().default('USA'),
coordinates: z.object({
lat: z.number().min(-90).max(90),
lng: z.number().min(-180).max(180)
}).optional()
});
const userWithAddressSchema = z.object({
id: z.string().uuid(),
name: z.string(),
email: z.string().email(),
address: addressSchema,
phoneNumbers: z.array(z.string().regex(/^+?[1-9]d{1,14}$/)).optional(),
metadata: z.record(z.unknown()).optional()
});
const userWithAddress = {
id: '550e8400-e29b-41d4-a716-446655440000',
name: 'John Doe',
email: '[email protected]',
address: {
street: '123 Main St',
city: 'New York',
state: 'NY',
zipCode: '10001',
coordinates: { lat: 40.7128, lng: -74.0060 }
},
phoneNumbers: ['+1-555-123-4567'],
metadata: {
lastLogin: new Date().toISOString(),
preferences: { theme: 'dark' }
}
};
console.log(userWithAddressSchema.parse(userWithAddress)); // ✅
// 11. Array of Objects
const postSchema = z.object({
id: z.string().uuid(),
title: z.string().min(1).max(200),
content: z.string().max(10000),
authorId: z.string().uuid(),
tags: z.array(z.string().min(1).max(20)).max(5),
published: z.boolean().default(false),
createdAt: z.date(),
updatedAt: z.date().optional()
});
const postsSchema = z.array(postSchema);
const posts = [
{
id: '550e8400-e29b-41d4-a716-446655440001',
title: 'Hello World',
content: 'This is my first post',
authorId: '550e8400-e29b-41d4-a716-446655440000',
tags: ['intro', 'hello'],
published: true,
createdAt: new Date()
},
{
id: '550e8400-e29b-41d4-a716-446655440002',
title: 'Second Post',
content: 'Another interesting post',
authorId: '550e8400-e29b-41d4-a716-446655440000',
tags: ['updates', 'announcement'],
published: false,
createdAt: new Date()
}
];
console.log(postsSchema.parse(posts)); // ✅
// 12. Discriminated Unions
const baseEventSchema = z.object({
id: z.string().uuid(),
timestamp: z.date(),
userId: z.string().uuid()
});
const userLoginEventSchema = baseEventSchema.extend({
type: z.literal('user.login'),
ipAddress: z.string().ip(),
userAgent: z.string()
});
const userLogoutEventSchema = baseEventSchema.extend({
type: z.literal('user.logout'),
sessionDuration: z.number().positive()
});
const postCreatedEventSchema = baseEventSchema.extend({
type: z.literal('post.created'),
postId: z.string().uuid(),
postTitle: z.string()
});
const eventSchema = z.discriminatedUnion('type', [
userLoginEventSchema,
userLogoutEventSchema,
postCreatedEventSchema
]);
type Event = z.infer<typeof eventSchema>;
// Valid events
const loginEvent = {
id: '550e8400-e29b-41d4-a716-446655440003',
timestamp: new Date(),
userId: '550e8400-e29b-41d4-a716-446655440000',
type: 'user.login' as const,
ipAddress: '192.168.1.1',
userAgent: 'Mozilla/5.0...'
};
const logoutEvent = {
id: '550e8400-e29b-41d4-a716-446655440004',
timestamp: new Date(),
userId: '550e8400-e29b-41d4-a716-446655440000',
type: 'user.logout' as const,
sessionDuration: 3600
};
const postEvent = {
id: '550e8400-e29b-41d4-a716-446655440005',
timestamp: new Date(),
userId: '550e8400-e29b-41d4-a716-446655440000',
type: 'post.created' as const,
postId: '550e8400-e29b-41d4-a716-446655440001',
postTitle: 'Hello World'
};
console.log(eventSchema.parse(loginEvent)); // ✅
console.log(eventSchema.parse(logoutEvent)); // ✅
console.log(eventSchema.parse(postEvent)); // ✅
// 13. Record Schema (key-value object)
const configSchema = z.record(z.string(), z.unknown());
const config = {
apiUrl: 'https://api.example.com',
timeout: 5000,
retries: 3,
features: {
darkMode: true,
notifications: false
}
};
console.log(configSchema.parse(config)); // ✅
// More specific record schema
const themeConfigSchema = z.record(z.enum(['primary', 'secondary', 'success', 'danger', 'warning', 'info']), z.string());
const themeConfig = {
header: 'primary',
sidebar: 'secondary',
buttons: 'success',
alerts: 'danger',
notifications: 'warning',
links: 'info'
};
console.log(themeConfigSchema.parse(themeConfig)); // ✅
// 14. Tuple Schema
const coordinateSchema = z.tuple([z.number(), z.number()]);
const rgbColorSchema = z.tuple([z.number().min(0).max(255), z.number().min(0).max(255), z.number().min(0).max(255)]);
console.log(coordinateSchema.parse([10, 20])); // ✅
console.log(rgbColorSchema.parse([255, 128, 0])); // ✅
// 15. Optional Array Elements
const profileSchema = z.object({
username: z.string(),
email: z.string().email(),
socialLinks: z.array(z.object({
platform: z.string(),
url: z.string().url()
})).optional(),
skills: z.array(z.string()).optional()
});
const profile = {
username: 'johndoe',
email: '[email protected]',
socialLinks: [
{ platform: 'twitter', url: 'https://twitter.com/johndoe' },
{ platform: 'github', url: 'https://github.com/johndoe' }
],
skills: ['TypeScript', 'React', 'Node.js']
};
console.log(profileSchema.parse(profile)); // ✅
// 16. Recursive Schema (self-referencing)
const categorySchema = z.object({
id: z.string(),
name: z.string(),
description: z.string().optional(),
parentCategory: z.string().optional()
});
const categoryWithChildren = categorySchema.extend({
subcategories: z.lazy(() => z.array(categoryWithChildren)).optional()
});
// Valid category with nested children
const categoryTree = {
id: 'cat1',
name: 'Technology',
description: 'All things tech',
subcategories: [
{
id: 'cat2',
name: 'Programming',
subcategories: [
{
id: 'cat3',
name: 'JavaScript',
parentCategory: 'cat2'
},
{
id: 'cat4',
name: 'Python',
parentCategory: 'cat2'
}
]
},
{
id: 'cat5',
name: 'Hardware',
parentCategory: 'cat1'
}
]
};
console.log(categoryWithChildren.parse(categoryTree)); // ✅
// 17. Complex Validation Examples
// Product schema with inventory management
const productSchema = z.object({
id: z.string().uuid(),
name: z.string().min(1).max(100),
description: z.string().max(1000),
price: z.number().positive(),
categories: z.array(z.string().uuid()).min(1),
variants: z.array(z.object({
id: z.string().uuid(),
name: z.string(),
sku: z.string(),
price: z.number().positive().optional(),
inventory: z.object({
quantity: z.number().min(0),
lowStockThreshold: z.number().min(0),
trackInventory: z.boolean()
}),
attributes: z.record(z.string(), z.union([z.string(), z.number(), z.boolean()])),
images: z.array(z.object({
url: z.string().url(),
alt: z.string().optional(),
isPrimary: z.boolean().default(false)
}))
})).min(1),
tags: z.array(z.string()).max(10).optional(),
metadata: z.object({
weight: z.number().positive().optional(),
dimensions: z.object({
length: z.number().positive(),
width: z.number().positive(),
height: z.number().positive(),
unit: z.enum(['cm', 'in', 'mm']).default('cm')
}).optional(),
seo: z.object({
title: z.string().max(60).optional(),
description: z.string().max(160).optional(),
keywords: z.array(z.string()).max(10).optional()
}).optional()
}).optional()
});
// Order schema with line items
const orderItemSchema = z.object({
productId: z.string().uuid(),
variantId: z.string().uuid(),
quantity: z.number().positive().max(100),
price: z.number().positive(),
discounts: z.array(z.object({
type: z.enum(['percentage', 'fixed']),
value: z.number().positive(),
reason: z.string().optional()
})).optional()
});
const orderSchema = z.object({
id: z.string().uuid(),
customerId: z.string().uuid(),
items: z.array(orderItemSchema).min(1),
status: z.enum(['pending', 'confirmed', 'processing', 'shipped', 'delivered', 'cancelled']),
shippingAddress: addressSchema,
billingAddress: addressSchema,
paymentMethod: z.object({
type: z.enum(['credit_card', 'debit_card', 'paypal', 'stripe']),
lastFour: z.string().length(4).regex(/^d+$/),
brand: z.string().optional()
}),
subtotal: z.number().positive(),
tax: z.number().nonnegative(),
shipping: z.number().nonnegative(),
total: z.number().positive(),
createdAt: z.date(),
shippedAt: z.date().optional(),
deliveredAt: z.date().optional()
});
// Validation functions for complex scenarios
function validateProductInventory(product: unknown, requestedQuantity: number) {
const parsedProduct = productSchema.parse(product);
for (const variant of parsedProduct.variants) {
if (variant.inventory.quantity < requestedQuantity) {
throw new Error(`Insufficient inventory for variant ${variant.name}. Available: ${variant.inventory.quantity}, Requested: ${requestedQuantity}`);
}
if (variant.inventory.trackInventory && variant.inventory.quantity <= variant.inventory.lowStockThreshold) {
console.warn(`Low stock warning for variant ${variant.name}. Current stock: ${variant.inventory.quantity}`);
}
}
return parsedProduct;
}
function validateOrderTotal(order: unknown) {
const parsedOrder = orderSchema.parse(order);
const calculatedSubtotal = parsedOrder.items.reduce((sum, item) => {
const itemTotal = item.price * item.quantity;
const discountTotal = (item.discounts || []).reduce((discountSum, discount) => {
if (discount.type === 'percentage') {
return discountSum + (itemTotal * discount.value / 100);
} else {
return discountSum + discount.value;
}
}, 0);
return sum + (itemTotal - discountTotal);
}, 0);
if (Math.abs(calculatedSubtotal - parsedOrder.subtotal) > 0.01) {
throw new Error(`Subtotal mismatch. Calculated: ${calculatedSubtotal}, Provided: ${parsedOrder.subtotal}`);
}
const calculatedTotal = calculatedSubtotal + parsedOrder.tax + parsedOrder.shipping;
if (Math.abs(calculatedTotal - parsedOrder.total) > 0.01) {
throw new Error(`Total mismatch. Calculated: ${calculatedTotal}, Provided: ${parsedOrder.total}`);
}
return parsedOrder;
}
// Test complex validation
try {
const testProduct = {
id: '550e8400-e29b-41d4-a716-446655440006',
name: 'Wireless Headphones',
description: 'High-quality wireless headphones with noise cancellation',
price: 99.99,
categories: ['cat1', 'cat2'],
variants: [
{
id: 'var1',
name: 'Black',
sku: 'WH-BLK-001',
inventory: {
quantity: 50,
lowStockThreshold: 10,
trackInventory: true
},
attributes: { color: 'black', wireless: true },
images: [
{ url: 'https://example.com/headphones-black.jpg', alt: 'Black wireless headphones', isPrimary: true }
]
}
]
};
validateProductInventory(testProduct, 5); // ✅
const testOrder = {
id: 'order1',
customerId: 'customer1',
items: [
{
productId: 'product1',
variantId: 'var1',
quantity: 2,
price: 99.99
}
],
status: 'pending' as const,
shippingAddress: {
street: '123 Main St',
city: 'New York',
state: 'NY',
zipCode: '10001'
},
billingAddress: {
street: '123 Main St',
city: 'New York',
state: 'NY',
zipCode: '10001'
},
paymentMethod: {
type: 'credit_card' as const,
lastFour: '1234',
brand: 'Visa'
},
subtotal: 199.98,
tax: 16.00,
shipping: 5.00,
total: 220.98,
createdAt: new Date()
};
validateOrderTotal(testOrder); // ✅
} catch (error) {
console.error('Complex validation error:', error);
}
export {
userSchema,
addressSchema,
userWithAddressSchema,
postSchema,
postsSchema,
eventSchema,
productSchema,
orderItemSchema,
orderSchema,
validateProductInventory,
validateOrderTotal,
type User,
type Event
};
💻 Zod 高级模式 typescript
🔴 complex
⭐⭐⭐⭐⭐
高级Zod模式,包括自定义验证器、错误处理、模式组合、中间件和实际用例
⏱️ 60 min
🏷️ zod, validation, patterns, typescript, advanced
Prerequisites:
Advanced Zod, TypeScript, Design patterns, Async/await
// Zod Advanced Patterns and Real-World Examples
import { z, ZodIssue, ZodSchema } from 'zod';
// 1. Custom Validation with Detailed Error Messages
// Enhanced string validation with multiple custom rules
const enhancedStringSchema = z.string()
.min(3, 'String must be at least 3 characters long')
.max(100, 'String must be at most 100 characters long')
.regex(/^[a-zA-Z0-9\s-_]+$/, 'String can only contain letters, numbers, spaces, hyphens, and underscores')
.refine((val) => val.trim() === val, {
message: 'String cannot have leading or trailing whitespace'
})
.refine((val) => {
const wordCount = val.trim().split(/\s+/).length;
return wordCount >= 2;
}, {
message: 'String must contain at least 2 words'
})
.transform((val) => val.trim());
// Custom ID validator with format checking
const idSchema = z.string()
.regex(/^[a-zA-Z0-9_-]+$/, 'ID can only contain letters, numbers, hyphens, and underscores')
.min(3, 'ID must be at least 3 characters long')
.max(50, 'ID must be at most 50 characters long')
.refine((val) => /^[a-zA-Z]/.test(val), {
message: 'ID must start with a letter'
})
.refine((val) => !/[-_]{2,}/.test(val), {
message: 'ID cannot contain consecutive hyphens or underscores'
});
// 2. Schema Composition and Reuse
// Base field schemas for reuse
const baseFieldSchemas = {
id: idSchema,
name: z.string().min(1).max(100),
description: z.string().max(500).optional(),
createdAt: z.date(),
updatedAt: z.date().optional(),
isActive: z.boolean().default(true)
};
// Timestamp schema with validation
const timestampSchema = z.date()
.refine((date) => date <= new Date(), {
message: 'Date cannot be in the future'
})
.refine((date) => date >= new Date('2000-01-01'), {
message: 'Date must be after year 2000'
});
// 3. Advanced Error Handling
// Custom error formatting
const formatZodError = (error: z.ZodError) => {
return {
success: false,
error: {
code: error.issues[0]?.code || 'VALIDATION_ERROR',
message: error.issues[0]?.message || 'Validation failed',
field: error.issues[0]?.path?.join('.') || 'unknown',
details: error.issues.map(issue => ({
field: issue.path?.join('.') || 'unknown',
code: issue.code,
message: issue.message,
received: issue.received,
expected: issue.expected
}))
}
};
};
// Custom error class for validation
class ValidationError extends Error {
constructor(
message: string,
public readonly code: string,
public readonly field?: string,
public readonly details?: ZodIssue[]
) {
super(message);
this.name = 'ValidationError';
}
}
// Enhanced safe parse with custom error handling
function safeParseWithCustomError<T>(schema: ZodSchema<T>, data: unknown): T {
const result = schema.safeParse(data);
if (!result.success) {
const firstError = result.error.issues[0];
throw new ValidationError(
firstError.message,
firstError.code,
firstError.path?.join('.'),
result.error.issues
);
}
return result.data;
}
// 4. Middleware Pattern for Validation
// Validation middleware type
type ValidationMiddleware<T> = (data: unknown) => T;
// Create validation middleware
function createValidationMiddleware<T>(schema: ZodSchema<T>): ValidationMiddleware<T> {
return (data: unknown): T => {
return safeParseWithCustomError(schema, data);
};
}
// Higher-order function for validation
function withValidation<T>(
schema: ZodSchema<T>,
fn: (validatedData: T) => unknown
) {
const validate = createValidationMiddleware(schema);
return (data: unknown) => {
const validatedData = validate(data);
return fn(validatedData);
};
}
// 5. Async Validation Patterns
// Async validator for checking uniqueness
async function createUniqueEmailValidator(existingEmails: Set<string>) {
return async (email: string) => {
// Simulate database check
await new Promise(resolve => setTimeout(resolve, 100));
if (existingEmails.has(email.toLowerCase())) {
throw new Error('Email already exists');
}
return true;
};
}
// Async schema with custom validation
async function createUserSchema(existingEmails: Set<string> = new Set()) {
const uniqueEmailValidator = await createUniqueEmailValidator(existingEmails);
return z.object({
email: z.string().email().refine(uniqueEmailValidator, {
message: 'Email must be unique'
}),
username: z.string().min(3).max(20).regex(/^[a-zA-Z0-9_]+$/),
password: z.string()
.min(8)
.regex(/[A-Z]/, 'Password must contain uppercase')
.regex(/[a-z]/, 'Password must contain lowercase')
.regex(/[0-9]/, 'Password must contain number')
.regex(/[^A-Za-z0-9]/, 'Password must contain special character')
});
}
// 6. Conditional Validation
// Conditional object schema based on user type
function createUserProfileSchema(isAdmin: boolean) {
const baseProfile = z.object({
firstName: z.string().min(1),
lastName: z.string().min(1),
email: z.string().email(),
bio: z.string().max(500).optional()
});
if (isAdmin) {
return baseProfile.extend({
adminLevel: z.enum(['junior', 'senior', 'lead']),
permissions: z.array(z.string()).min(1),
department: z.string().min(1)
});
}
return baseProfile.extend({
preferences: z.object({
theme: z.enum(['light', 'dark']),
notifications: z.boolean(),
language: z.string().length(2)
})
});
}
// 7. Schema Refactoring with Composition
// Address schema
const addressSchema = z.object({
street: z.string().min(1),
city: z.string().min(1),
state: z.string().length(2),
zipCode: z.string().regex(/^\d{5}(-\d{4})?$/),
country: z.string().default('USA'),
addressType: z.enum(['shipping', 'billing', 'both'])
});
// Contact schema
const contactSchema = z.object({
phone: z.string().regex(/^\+?[1-9]\d{1,14}$/).optional(),
email: z.string().email(),
website: z.string().url().optional(),
socialMedia: z.record(z.string().url()).optional()
});
// User schema composed from smaller schemas
const createUserSchema = () => z.object({
...baseFieldSchemas,
contact: contactSchema,
addresses: z.array(addressSchema).min(1),
role: z.enum(['user', 'admin', 'moderator']).default('user'),
profile: z.object({
avatar: z.string().url().optional(),
timezone: z.string(),
locale: z.string().default('en-US')
})
});
// 8. Real-World API Request/Response Validation
// API Request schemas
const PaginationSchema = z.object({
page: z.coerce.number().positive().default(1),
limit: z.coerce.number().positive().max(100).default(20),
sortBy: z.enum(['createdAt', 'updatedAt', 'name']).default('createdAt'),
sortOrder: z.enum(['asc', 'desc']).default('desc')
});
const FilterSchema = z.object({
status: z.enum(['active', 'inactive', 'all']).optional(),
category: z.string().optional(),
dateFrom: z.string().datetime().optional(),
dateTo: z.string().datetime().optional(),
search: z.string().optional()
});
// API Request wrapper
const ApiRequestSchema = z.object({
pagination: PaginationSchema,
filters: FilterSchema,
include: z.array(z.string()).optional()
});
// API Response schemas
const ApiSuccessSchema = <T>(dataSchema: ZodSchema<T>) => z.object({
success: z.literal(true),
data: dataSchema,
pagination: z.object({
currentPage: z.number(),
totalPages: z.number(),
totalItems: z.number(),
hasNext: z.boolean(),
hasPrev: z.boolean()
}).optional(),
meta: z.record(z.unknown()).optional()
});
const ApiErrorSchema = z.object({
success: z.literal(false),
error: z.object({
code: z.string(),
message: z.string(),
details: z.array(z.object({
field: z.string(),
code: z.string(),
message: z.string()
})).optional()
}),
meta: z.record(z.unknown()).optional()
});
// Generic API response schema
const ApiResponseSchema = <T>(dataSchema: ZodSchema<T>) =>
z.union([ApiSuccessSchema(dataSchema), ApiErrorSchema]);
// 9. Database Model Validation
// UUID validator with fallback
const uuidSchema = z.string().uuid().or(z.string().length(36)).transform((val) => {
// Handle both UUID and string representation
return val.length === 36 ? val : val;
});
// Entity status validator
const EntityStatusSchema = z.enum(['active', 'inactive', 'archived', 'pending']).default('active');
// Soft delete mixin
const withSoftDelete = <T extends z.ZodRawShape>(
schema: T
) => z.object(schema).extend({
deletedAt: z.date().nullable().default(null),
deletedBy: uuidSchema.nullable().default(null)
});
// Audit fields mixin
const withAuditFields = <T extends z.ZodRawShape>(
schema: T
) => z.object(schema).extend({
createdAt: z.date().default(() => new Date()),
updatedAt: z.date().default(() => new Date()),
createdBy: uuidSchema,
updatedBy: uuidSchema.optional()
});
// 10. Environment Configuration Validation
// Environment type discriminator
const EnvironmentSchema = z.enum(['development', 'staging', 'production', 'test']);
// Database configuration
const DatabaseConfigSchema = z.object({
url: z.string().url(),
ssl: z.boolean().default(true),
maxConnections: z.number().positive().default(10),
connectionTimeout: z.number().positive().default(30000),
idleTimeout: z.number().positive().default(300000)
});
// Server configuration
const ServerConfigSchema = z.object({
port: z.number().positive().default(3000),
host: z.string().default('localhost'),
cors: z.object({
origin: z.union([z.string(), z.array(z.string())]).default('*'),
credentials: z.boolean().default(false),
methods: z.array(z.enum(['GET', 'POST', 'PUT', 'DELETE', 'OPTIONS'])).default(['GET', 'POST'])
}).default({}),
rateLimit: z.object({
windowMs: z.number().positive().default(900000), // 15 minutes
maxRequests: z.number().positive().default(100)
}).optional()
});
// Complete environment configuration
const EnvironmentConfigSchema = z.object({
nodeEnv: EnvironmentSchema,
server: ServerConfigSchema,
database: DatabaseConfigSchema,
auth: z.object({
jwtSecret: z.string().min(32),
jwtExpiresIn: z.string().default('24h'),
bcryptRounds: z.number().positive().min(10).max(12).default(12)
}),
redis: z.object({
url: z.string().url().optional(),
host: z.string().default('localhost'),
port: z.number().positive().default(6379),
password: z.string().optional(),
db: z.number().default(0)
}).optional(),
features: z.object({
enableMetrics: z.boolean().default(false),
enableLogging: z.boolean().default(true),
enableCache: z.boolean().default(true),
enableRateLimit: z.boolean().default(true)
}).default({})
});
// 11. Form Validation Patterns
// Multi-step form schema
const step1Schema = z.object({
firstName: z.string().min(1),
lastName: z.string().min(1),
email: z.string().email(),
phone: z.string().regex(/^\+?[1-9]\d{1,14}$/)
});
const step2Schema = z.object({
address: addressSchema,
preferences: z.object({
newsletter: z.boolean().default(false),
marketing: z.boolean().default(false)
})
});
const step3Schema = z.object({
paymentMethod: z.enum(['credit_card', 'paypal', 'bank_transfer']),
terms: z.boolean().refine(val => val === true, {
message: 'You must accept the terms and conditions'
})
});
// Multi-step form validator
function validateMultiStepForm(step: number, data: unknown) {
switch (step) {
case 1:
return step1Schema.parse(data);
case 2:
return step2Schema.parse(data);
case 3:
return step3Schema.parse(data);
default:
throw new Error('Invalid step number');
}
}
// 12. Real-World Usage Examples
// User registration service
class UserService {
private existingEmails = new Set<string>();
private existingUsernames = new Set<string>();
async registerUser(userData: unknown) {
const userSchema = await this.createUserRegistrationSchema();
const validatedUser = safeParseWithCustomError(userSchema, userData);
// Additional business logic
await this.checkDuplicateEmail(validatedUser.email);
await this.checkDuplicateUsername(validatedUser.username);
const user = await this.createUserInDatabase(validatedUser);
// Update tracking sets
this.existingEmails.add(user.email.toLowerCase());
this.existingUsernames.add(user.username.toLowerCase());
return user;
}
private async createUserRegistrationSchema() {
const uniqueEmailValidator = await this.createUniqueEmailValidator();
return z.object({
username: z.string()
.min(3)
.max(20)
.regex(/^[a-zA-Z0-9_]+$/)
.refine(async (username) => {
await new Promise(resolve => setTimeout(resolve, 50)); // Simulate async check
return !this.existingUsernames.has(username.toLowerCase());
}, {
message: 'Username already exists'
}),
email: z.string().email(),
password: z.string().min(8),
confirmPassword: z.string()
}).refine((data) => data.password === data.confirmPassword, {
message: 'Passwords do not match',
path: ['confirmPassword']
});
}
private async createUniqueEmailValidator() {
return async (email: string) => {
await new Promise(resolve => setTimeout(resolve, 50));
return !this.existingEmails.has(email.toLowerCase());
};
}
private async checkDuplicateEmail(email: string) {
if (this.existingEmails.has(email.toLowerCase())) {
throw new ValidationError('Email already exists', 'DUPLICATE_EMAIL', 'email');
}
}
private async checkDuplicateUsername(username: string) {
if (this.existingUsernames.has(username.toLowerCase())) {
throw new ValidationError('Username already exists', 'DUPLICATE_USERNAME', 'username');
}
}
private async createUserInDatabase(userData: any) {
// Simulate database insertion
await new Promise(resolve => setTimeout(resolve, 100));
return {
id: Math.random().toString(36).substr(2, 9),
...userData,
createdAt: new Date(),
updatedAt: new Date()
};
}
}
// API request/response handler
class ApiController {
handleRequest(request: unknown, responseSchema: ZodSchema<any>) {
try {
const validatedRequest = safeParseWithCustomError(
ApiRequestSchema,
request
);
// Process request...
const data = await this.processRequest(validatedRequest);
const validatedResponse = ApiSuccessSchema(responseSchema).parse({
success: true,
data
});
return validatedResponse;
} catch (error) {
if (error instanceof ValidationError) {
return ApiErrorSchema.parse({
success: false,
error: {
code: error.code,
message: error.message,
details: error.details?.map(issue => ({
field: issue.field,
code: issue.code,
message: issue.message
}))
}
});
}
// Handle other unexpected errors
return ApiErrorSchema.parse({
success: false,
error: {
code: 'INTERNAL_ERROR',
message: 'An unexpected error occurred'
}
});
}
}
private async processRequest(request: any) {
// Simulate async processing
await new Promise(resolve => setTimeout(resolve, 50));
return { message: 'Request processed successfully' };
}
}
// 13. Testing with Zod
// Test utilities
function expectValidationSuccess<T>(schema: ZodSchema<T>, data: unknown): T {
const result = schema.safeParse(data);
if (!result.success) {
throw new Error(`Expected validation to succeed, but it failed: ${result.error.message}`);
}
return result.data;
}
function expectValidationFailure<T>(schema: ZodSchema<T>, data: unknown, expectedError?: string): void {
const result = schema.safeParse(data);
if (result.success) {
throw new Error('Expected validation to fail, but it succeeded');
}
if (expectedError && !result.error.message.includes(expectedError)) {
throw new Error(`Expected error message to contain "${expectedError}", but got: ${result.error.message}`);
}
}
// Test examples
function runTests() {
console.log('Running Zod validation tests...');
// Test success cases
const validUser = expectValidationSuccess(userSchema, {
id: '550e8400-e29b-41d4-a716-446655440000',
name: 'John Doe',
email: '[email protected]',
age: 30
});
console.log('✅ Valid user test passed');
// Test failure cases
expectValidationFailure(
userSchema,
{ id: 'invalid-uuid', name: '', email: 'invalid-email', age: -5 },
'Expected string'
);
console.log('✅ Invalid user test passed');
console.log('All tests completed successfully!');
}
// Execute tests
runTests();
// 14. Schema Documentation Generator
function generateSchemaDocumentation(schema: ZodSchema<any>, name: string): string {
const description = schema._def.description || name;
let docs = `# ${name}\n\n${description}\n\n## Schema Definition\n\n`;
if (schema._def.typeName === 'ZodObject') {
docs += '| Property | Type | Required | Description |\n';
docs += '|---------|------|----------|-------------|\n';
for (const [key, def] of Object.entries(schema._def.shape())) {
const type = def._def.typeName;
const required = !def._def.defaultValue;
const description = def._def.description || '';
docs += `| ${key} | ${type} | ${required ? 'Yes' : 'No'} | ${description} |\n`;
}
}
return docs;
}
export {
enhancedStringSchema,
idSchema,
formatZodError,
ValidationError,
safeParseWithCustomError,
createValidationMiddleware,
withValidation,
ApiRequestSchema,
ApiResponseSchema,
EnvironmentConfigSchema,
validateMultiStepForm,
UserService,
ApiController,
expectValidationSuccess,
expectValidationFailure,
generateSchemaDocumentation
};