Zod - Quick Reference
When to Use This Skill
- Form validation with React Hook Form
- API data parsing/validation
- Type inference from schemas
Deep Knowledge: Use
mcp__documentation__fetch_docswith technology:zodfor comprehensive documentation.
Essential Patterns
Basic Schema
import { z } from 'zod';
const userSchema = z.object({
name: z.string().min(2),
email: z.string().email(),
age: z.number().min(18).optional(),
role: z.enum(['admin', 'user'])
});
// Type inference
type User = z.infer<typeof userSchema>;
// Parse
const user = userSchema.parse(data); // throws ZodError
const result = userSchema.safeParse(data); // { success, data/error }
Common Validations
// String
z.string().min(1).max(100).email().url().uuid()
// Number
z.number().int().positive().min(0).max(100)
// Custom validation
z.string().refine(val => val.startsWith('@'), "Must start with @")
// Object cross-field
z.object({
password: z.string(),
confirm: z.string()
}).refine(d => d.password === d.confirm, {
message: "Passwords don't match",
path: ['confirm']
});
React Hook Form
import { zodResolver } from '@hookform/resolvers/zod';
const { register, handleSubmit, formState: { errors } } = useForm({
resolver: zodResolver(userSchema)
});
<input {...register('name')} />
{errors.name && <span>{errors.name.message}</span>}
Schema Operations
userSchema.partial() // All fields optional
userSchema.pick({ name: true }) // Pick fields
userSchema.omit({ id: true }) // Omit fields
userSchema.extend({ phone: z.string() }) // Extend
When NOT to Use This Skill
- Yup existing projects - Use
yupskill for Formik integration - NestJS DTOs - Use
class-validatorskill for decorator-based validation - Compile-time only checks - TypeScript types are sufficient
- Simple PropTypes - React PropTypes might be enough
Anti-Patterns to Avoid
- Do not define schemas inside components (performance)
- Do not use
.parse()for user input without try/catch - Do not forget custom error messages
Anti-Patterns
| Anti-Pattern | Why It's Bad | Correct Approach |
|---|---|---|
| Schema inside components | Re-created on every render | Define at module level |
| Using .parse() without try-catch | Throws unhandled errors | Use .safeParse() for user input |
| No custom error messages | Generic "Invalid input" | Add custom messages to all validations |
| Validating twice (FE + BE) | Performance waste | Share schemas between client/server |
| Not using z.infer | Manual type definitions | Always use type inference |
| Ignoring .transform() | Miss data normalization | Use transforms for trimming, etc. |
Quick Troubleshooting
| Issue | Cause | Solution |
|---|---|---|
| "Expected string, received number" | Type mismatch | Use z.coerce.string() for type coercion |
| Performance issues | Schema in component | Move schema to module level |
| Async validation not working | Using .parse() | Use .parseAsync() for async refinements |
| Nested errors not showing | Not using flatten() | Use error.flatten() for nested structures |
| "Invalid type" on optional fields | Using undefined | Use .optional() or .nullable() |
| Form not validating | Resolver not set | Add resolver: zodResolver(schema) |
Production Readiness
Error Handling
import { z, ZodError } from 'zod';
// Safe parsing with error handling
function validateInput<T>(schema: z.ZodSchema<T>, data: unknown): T {
const result = schema.safeParse(data);
if (!result.success) {
const errors = result.error.flatten();
throw new ValidationError(errors);
}
return result.data;
}
// Custom error messages
const userSchema = z.object({
email: z.string({
required_error: 'Email is required',
invalid_type_error: 'Email must be a string',
}).email({ message: 'Invalid email format' }),
password: z.string()
.min(8, { message: 'Password must be at least 8 characters' })
.regex(/[A-Z]/, { message: 'Password must contain uppercase letter' })
.regex(/[0-9]/, { message: 'Password must contain number' }),
});
// Error formatting for API responses
function formatZodError(error: ZodError): Record<string, string[]> {
return error.flatten().fieldErrors as Record<string, string[]>;
}
API Integration
// Express middleware
import { Request, Response, NextFunction } from 'express';
function validate<T extends z.ZodSchema>(schema: T) {
return (req: Request, res: Response, next: NextFunction) => {
const result = schema.safeParse(req.body);
if (!result.success) {
return res.status(400).json({
code: 'VALIDATION_ERROR',
errors: formatZodError(result.error),
});
}
req.body = result.data;
next();
};
}
// Usage
app.post('/users', validate(createUserSchema), createUser);
// tRPC integration
export const appRouter = t.router({
createUser: t.procedure
.input(createUserSchema)
.mutation(({ input }) => {
// input is fully typed and validated
return db.users.create({ data: input });
}),
});
Complex Validations
// Conditional fields
const formSchema = z.discriminatedUnion('type', [
z.object({
type: z.literal('individual'),
name: z.string(),
ssn: z.string().regex(/^\d{3}-\d{2}-\d{4}$/),
}),
z.object({
type: z.literal('business'),
companyName: z.string(),
ein: z.string().regex(/^\d{2}-\d{7}$/),
}),
]);
// Async validation
const uniqueEmailSchema = z.string().email().refine(
async (email) => {
const exists = await db.users.findByEmail(email);
return !exists;
},
{ message: 'Email already registered' }
);
// Transform and validate
const dateSchema = z.string()
.transform((str) => new Date(str))
.refine((date) => !isNaN(date.getTime()), 'Invalid date');
// Preprocess for type coercion
const querySchema = z.object({
page: z.preprocess(
(val) => parseInt(val as string, 10),
z.number().min(1).default(1)
),
limit: z.preprocess(
(val) => parseInt(val as string, 10),
z.number().min(1).max(100).default(20)
),
});
Testing
import { describe, it, expect } from 'vitest';
describe('userSchema', () => {
it('validates correct input', () => {
const input = { email: 'test@example.com', password: 'Password1!' };
const result = userSchema.safeParse(input);
expect(result.success).toBe(true);
if (result.success) {
expect(result.data.email).toBe('test@example.com');
}
});
it('fails on invalid email', () => {
const input = { email: 'invalid', password: 'Password1!' };
const result = userSchema.safeParse(input);
expect(result.success).toBe(false);
if (!result.success) {
expect(result.error.flatten().fieldErrors.email).toContain('Invalid email format');
}
});
it('fails on weak password', () => {
const input = { email: 'test@example.com', password: 'weak' };
const result = userSchema.safeParse(input);
expect(result.success).toBe(false);
});
});
// Property-based testing with fast-check
import * as fc from 'fast-check';
it('accepts all valid emails', () => {
fc.assert(
fc.property(fc.emailAddress(), (email) => {
const result = z.string().email().safeParse(email);
return result.success;
})
);
});
Performance
// Define schemas outside components/functions
const schemas = {
user: z.object({ /* ... */ }),
product: z.object({ /* ... */ }),
} as const;
// Lazy loading for circular references
const categorySchema: z.ZodType<Category> = z.lazy(() =>
z.object({
name: z.string(),
children: z.array(categorySchema).optional(),
})
);
// Coercion for better performance on known types
const numberSchema = z.coerce.number(); // Auto-converts strings
const dateSchema = z.coerce.date();
Monitoring Metrics
| Metric | Target |
|---|---|
| Validation errors | Track & analyze |
| Schema coverage | 100% API inputs |
| Type inference | All schemas |
| Parse time | < 5ms |
Checklist
- Custom error messages
- safeParse for user input
- Error formatting for API
- Schemas defined at module level
- Type inference (z.infer)
- Complex validation (refine)
- Async validation where needed
- Unit tests for schemas
- Integration with React Hook Form
- OpenAPI generation (zod-to-openapi)
Further Reading
For advanced validations:
mcp__documentation__fetch_docs
- Technology:
zod(available in MCP)- Zod Docs