Zod schema validation patterns. Use when validating API inputs and data.
This skill inherits all available tools. When active, it can use any tool Claude has access to.
This skill covers Zod schema validation for type-safe data validation.
Use this skill when:
VALIDATE AT BOUNDARIES - Validate all external input. Trust internal data. Use Zod for type inference.
npm install zod
import { z } from 'zod';
// Primitives
const stringSchema = z.string();
const numberSchema = z.number();
const booleanSchema = z.boolean();
const dateSchema = z.date();
// String validations
const emailSchema = z.string().email();
const urlSchema = z.string().url();
const uuidSchema = z.string().uuid();
const minLengthSchema = z.string().min(1).max(100);
const regexSchema = z.string().regex(/^[a-z]+$/);
// Number validations
const positiveSchema = z.number().positive();
const intSchema = z.number().int();
const rangeSchema = z.number().min(0).max(100);
// Optional and nullable
const optionalSchema = z.string().optional(); // string | undefined
const nullableSchema = z.string().nullable(); // string | null
const nullishSchema = z.string().nullish(); // string | null | undefined
const UserSchema = z.object({
id: z.string().uuid(),
email: z.string().email(),
name: z.string().min(1).max(100),
age: z.number().int().positive().optional(),
role: z.enum(['USER', 'ADMIN', 'MODERATOR']),
createdAt: z.date(),
});
// Type inference
type User = z.infer<typeof UserSchema>;
// Partial (all fields optional)
const PartialUserSchema = UserSchema.partial();
// Pick specific fields
const UserEmailSchema = UserSchema.pick({ email: true, name: true });
// Omit fields
const UserWithoutIdSchema = UserSchema.omit({ id: true, createdAt: true });
// Extend schema
const UserWithProfileSchema = UserSchema.extend({
profile: z.object({
bio: z.string().optional(),
avatar: z.string().url().optional(),
}),
});
// Arrays
const StringArraySchema = z.array(z.string());
const NumberArraySchema = z.array(z.number()).min(1).max(10);
// Tuples
const CoordinatesSchema = z.tuple([z.number(), z.number()]);
// Unions
const StringOrNumberSchema = z.union([z.string(), z.number()]);
const ResultSchema = z.discriminatedUnion('status', [
z.object({ status: z.literal('success'), data: z.unknown() }),
z.object({ status: z.literal('error'), error: z.string() }),
]);
// Enums
const RoleSchema = z.enum(['USER', 'ADMIN', 'MODERATOR']);
type Role = z.infer<typeof RoleSchema>; // 'USER' | 'ADMIN' | 'MODERATOR'
// Create user request
const CreateUserSchema = 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 number'),
name: z.string().min(1, 'Name is required').max(100),
});
// Update user request (all fields optional)
const UpdateUserSchema = CreateUserSchema.partial();
// Query parameters
const PaginationSchema = z.object({
page: z.coerce.number().int().positive().default(1),
perPage: z.coerce.number().int().min(1).max(100).default(20),
sort: z.enum(['asc', 'desc']).default('desc'),
});
// Path parameters
const IdParamSchema = z.object({
id: z.string().uuid('Invalid ID format'),
});
// Transform during parse
const TrimmedStringSchema = z.string().trim();
const LowercaseEmailSchema = z.string().email().toLowerCase();
// Transform to different type
const DateStringSchema = z.string().transform((str) => new Date(str));
// Coerce types
const CoercedNumberSchema = z.coerce.number(); // "42" -> 42
const CoercedDateSchema = z.coerce.date(); // "2024-01-01" -> Date
// Complex transformation
const UserInputSchema = z.object({
email: z.string().email().toLowerCase().trim(),
name: z.string().trim(),
tags: z.string().transform((str) => str.split(',').map((t) => t.trim())),
});
// Custom refinement
const PasswordSchema = z.string()
.min(8)
.refine(
(password) => /[A-Z]/.test(password),
{ message: 'Password must contain uppercase letter' }
)
.refine(
(password) => /[0-9]/.test(password),
{ message: 'Password must contain number' }
);
// Cross-field validation
const PasswordConfirmSchema = z.object({
password: z.string().min(8),
confirmPassword: z.string(),
}).refine(
(data) => data.password === data.confirmPassword,
{
message: 'Passwords do not match',
path: ['confirmPassword'],
}
);
// Async validation
const UniqueEmailSchema = z.string().email().refine(
async (email) => {
const exists = await checkEmailExists(email);
return !exists;
},
{ message: 'Email already registered' }
);
// src/routes/users.ts
import { FastifyPluginAsync } from 'fastify';
import { z } from 'zod';
import { zodToJsonSchema } from 'zod-to-json-schema';
const CreateUserSchema = z.object({
email: z.string().email(),
name: z.string().min(1),
password: z.string().min(8),
});
const UserResponseSchema = z.object({
id: z.string(),
email: z.string(),
name: z.string(),
createdAt: z.string(),
});
type CreateUserInput = z.infer<typeof CreateUserSchema>;
type UserResponse = z.infer<typeof UserResponseSchema>;
const usersRoutes: FastifyPluginAsync = async (fastify) => {
// Manual validation
fastify.post<{ Body: CreateUserInput }>('/', async (request, reply) => {
const result = CreateUserSchema.safeParse(request.body);
if (!result.success) {
return reply.status(400).send({
error: 'Validation failed',
details: result.error.flatten(),
});
}
const user = await createUser(result.data);
return reply.status(201).send(user);
});
// With JSON Schema (Fastify validates)
fastify.post<{ Body: CreateUserInput; Reply: UserResponse }>('/v2', {
schema: {
body: zodToJsonSchema(CreateUserSchema),
response: {
201: zodToJsonSchema(UserResponseSchema),
},
},
}, async (request, reply) => {
const user = await createUser(request.body);
return reply.status(201).send(user);
});
};
// src/config/env.ts
import { z } from 'zod';
const EnvSchema = z.object({
NODE_ENV: z.enum(['development', 'production', 'test']).default('development'),
PORT: z.coerce.number().default(3000),
DATABASE_URL: z.string().url(),
JWT_SECRET: z.string().min(32),
CORS_ORIGIN: z.string().url().optional(),
LOG_LEVEL: z.enum(['debug', 'info', 'warn', 'error']).default('info'),
});
export type Env = z.infer<typeof EnvSchema>;
function validateEnv(): Env {
const result = EnvSchema.safeParse(process.env);
if (!result.success) {
console.error('Invalid environment variables:');
console.error(result.error.flatten().fieldErrors);
process.exit(1);
}
return result.data;
}
export const env = validateEnv();
import { z, ZodError } from 'zod';
function parseOrThrow<T>(schema: z.ZodSchema<T>, data: unknown): T {
return schema.parse(data);
}
function parseOrNull<T>(schema: z.ZodSchema<T>, data: unknown): T | null {
const result = schema.safeParse(data);
return result.success ? result.data : null;
}
// Flatten errors for API response
function formatZodError(error: ZodError): Record<string, string[]> {
return error.flatten().fieldErrors as Record<string, string[]>;
}
// Usage
try {
const user = parseOrThrow(UserSchema, requestBody);
} catch (error) {
if (error instanceof ZodError) {
return { errors: formatZodError(error) };
}
throw error;
}
// Base schemas
const AddressSchema = z.object({
street: z.string(),
city: z.string(),
country: z.string(),
postalCode: z.string(),
});
const ContactSchema = z.object({
email: z.string().email(),
phone: z.string().optional(),
});
// Composed schema
const CustomerSchema = z.object({
id: z.string().uuid(),
name: z.string(),
contact: ContactSchema,
billingAddress: AddressSchema,
shippingAddress: AddressSchema.optional(),
});
z.infer<typeof Schema>safeParse for graceful errorsrefine for async validationszodToJsonSchema for OpenAPI/Swagger