typescript
Zod Schema Patterns
Common Zod validation patterns for forms, API responses, and environment variables.
#validation
#zod
#forms
Practical Zod schemas for everyday validation needs.
Form Validation
import { z } from 'zod';
const signupSchema = z.object({
email: z.string().email('Invalid email address'),
password: z
.string()
.min(8, 'Password must be at least 8 characters')
.regex(/[A-Z]/, 'Password must contain uppercase letter')
.regex(/[0-9]/, 'Password must contain a number'),
confirmPassword: z.string(),
age: z.coerce.number().min(18, 'Must be 18 or older'),
terms: z.literal(true, {
errorMap: () => ({ message: 'You must accept the terms' }),
}),
}).refine((data) => data.password === data.confirmPassword, {
message: "Passwords don't match",
path: ['confirmPassword'],
});
type SignupForm = z.infer<typeof signupSchema>;
API Response Validation
const userSchema = z.object({
id: z.string().uuid(),
name: z.string(),
email: z.string().email(),
role: z.enum(['admin', 'user', 'guest']),
createdAt: z.string().datetime(),
profile: z.object({
avatar: z.string().url().nullable(),
bio: z.string().max(500).optional(),
}).optional(),
});
const apiResponseSchema = z.object({
data: z.array(userSchema),
meta: z.object({
total: z.number(),
page: z.number(),
limit: z.number(),
}),
});
// Usage with fetch
async function fetchUsers() {
const response = await fetch('/api/users');
const json = await response.json();
return apiResponseSchema.parse(json); // Throws if invalid
}
Environment Variables
// env.ts
const envSchema = z.object({
NODE_ENV: z.enum(['development', 'production', 'test']),
DATABASE_URL: z.string().url(),
API_KEY: z.string().min(1),
PORT: z.coerce.number().default(3000),
DEBUG: z.coerce.boolean().default(false),
});
export const env = envSchema.parse(process.env);
Transform & Preprocess
const dateSchema = z.string().transform((str) => new Date(str));
const trimmedString = z.string().transform((s) => s.trim());
const slugSchema = z.string().transform((s) =>
s.toLowerCase().replace(/\s+/g, '-').replace(/[^a-z0-9-]/g, '')
);
// Preprocess for coercion with fallback
const numberWithDefault = z.preprocess(
(val) => (val === '' ? undefined : val),
z.coerce.number().optional()
);
Discriminated Unions
const notificationSchema = z.discriminatedUnion('type', [
z.object({ type: z.literal('email'), email: z.string().email() }),
z.object({ type: z.literal('sms'), phone: z.string() }),
z.object({ type: z.literal('push'), deviceId: z.string() }),
]);
// TypeScript knows the shape based on `type`
const result = notificationSchema.parse(data);
if (result.type === 'email') {
console.log(result.email); // Typed correctly
}