zod
Validates data with Zod schemas including type inference, transformations, error handling, and form integration. Use when validating API inputs, form data, environment variables, or any runtime data validation.
When & Why to Use This Skill
This Claude skill provides a comprehensive implementation of Zod for TypeScript-first schema validation. It ensures runtime data integrity by offering static type inference, complex data transformations, and sophisticated error handling. By validating API inputs, form data, and environment variables, it eliminates type-related bugs and streamlines the development of secure, type-safe applications.
Use Cases
- API Input Validation: Automatically validate and parse incoming request bodies, query parameters, and headers to ensure they meet specific structural requirements before reaching business logic.
- Type-Safe Form Processing: Integrate with frontend frameworks like React Hook Form to validate user input in real-time, providing clear error messages and automatic type coercion for numeric or boolean fields.
- Environment Variable Verification: Define and validate the structure of process.env at application startup to prevent runtime failures caused by missing or malformed configuration.
- Complex Data Transformation: Use pipeline and transform functions to sanitize, format, and convert raw data into validated, typed objects ready for database insertion or UI rendering.
- Cross-Field Validation: Implement complex logic to validate dependencies between different data fields, such as ensuring a 'confirm password' field matches the 'password' field.
| name | zod |
|---|---|
| description | Validates data with Zod schemas including type inference, transformations, error handling, and form integration. Use when validating API inputs, form data, environment variables, or any runtime data validation. |
Zod
TypeScript-first schema validation with static type inference.
Quick Start
Install:
npm install zod
Basic usage:
import { z } from 'zod';
// Define schema
const UserSchema = z.object({
name: z.string(),
email: z.string().email(),
age: z.number().min(18),
});
// Infer TypeScript type
type User = z.infer<typeof UserSchema>;
// Validate data
const result = UserSchema.safeParse(data);
if (result.success) {
console.log(result.data); // Typed as User
} else {
console.log(result.error.issues);
}
Primitive Types
// Strings
const name = z.string();
const email = z.string().email();
const url = z.string().url();
const uuid = z.string().uuid();
const cuid = z.string().cuid();
const regex = z.string().regex(/^[a-z]+$/);
const minMax = z.string().min(1).max(100);
const length = z.string().length(10);
const nonempty = z.string().min(1, 'Required');
// Numbers
const age = z.number();
const positive = z.number().positive();
const negative = z.number().negative();
const integer = z.number().int();
const range = z.number().min(0).max(100);
const finite = z.number().finite();
// BigInt
const bigint = z.bigint();
// Boolean
const active = z.boolean();
// Date
const date = z.date();
const minDate = z.date().min(new Date('2024-01-01'));
const maxDate = z.date().max(new Date());
// Undefined/Null
const undef = z.undefined();
const nul = z.null();
const nullable = z.string().nullable(); // string | null
const optional = z.string().optional(); // string | undefined
const nullish = z.string().nullish(); // string | null | undefined
Objects
Basic Objects
const UserSchema = z.object({
id: z.string().uuid(),
name: z.string().min(1),
email: z.string().email(),
age: z.number().optional(),
});
type User = z.infer<typeof UserSchema>;
// { id: string; name: string; email: string; age?: number }
Modifiers
const schema = z.object({
name: z.string(),
email: z.string(),
age: z.number(),
});
// All properties optional
const PartialSchema = schema.partial();
// { name?: string; email?: string; age?: number }
// All properties required
const RequiredSchema = schema.required();
// Pick specific properties
const NameEmail = schema.pick({ name: true, email: true });
// { name: string; email: string }
// Omit specific properties
const NoAge = schema.omit({ age: true });
// { name: string; email: string }
// Extend with new properties
const ExtendedSchema = schema.extend({
role: z.enum(['admin', 'user']),
});
// Merge schemas
const merged = schema.merge(z.object({ role: z.string() }));
// Make specific properties optional
const PartialAge = schema.partial({ age: true });
// { name: string; email: string; age?: number }
Strict and Passthrough
// Strict: fail if unknown keys present
const StrictSchema = z.object({ name: z.string() }).strict();
// Passthrough: allow and preserve unknown keys
const PassthroughSchema = z.object({ name: z.string() }).passthrough();
// Strip: remove unknown keys (default)
const StripSchema = z.object({ name: z.string() }).strip();
Arrays and Tuples
// Arrays
const strings = z.array(z.string());
const numbers = z.number().array(); // Alternative syntax
const nonEmpty = z.array(z.string()).nonempty();
const lengthRange = z.array(z.string()).min(1).max(10);
const exactLength = z.array(z.string()).length(5);
// Tuples (fixed length, specific types)
const tuple = z.tuple([z.string(), z.number(), z.boolean()]);
// [string, number, boolean]
// Tuple with rest
const tupleWithRest = z.tuple([z.string()]).rest(z.number());
// [string, ...number[]]
Unions and Enums
Unions
// Union types
const StringOrNumber = z.union([z.string(), z.number()]);
// string | number
// Shorthand
const StringOrNumber2 = z.string().or(z.number());
// Discriminated unions (better performance, error messages)
const Result = z.discriminatedUnion('status', [
z.object({ status: z.literal('success'), data: z.string() }),
z.object({ status: z.literal('error'), error: z.string() }),
]);
Enums
// Zod enum
const RoleSchema = z.enum(['admin', 'user', 'guest']);
type Role = z.infer<typeof RoleSchema>; // 'admin' | 'user' | 'guest'
// Get enum values
RoleSchema.options; // ['admin', 'user', 'guest']
// Native enum
enum Status {
Active = 'active',
Inactive = 'inactive',
}
const StatusSchema = z.nativeEnum(Status);
Literals
const admin = z.literal('admin');
const fortyTwo = z.literal(42);
const trueLiteral = z.literal(true);
Transformations
Transform
// Transform string to number
const StringToNumber = z.string().transform((val) => parseInt(val, 10));
// Parse and transform
const DateString = z.string().transform((val) => new Date(val));
// With validation after transform
const PositiveNumber = z.string()
.transform((val) => parseInt(val, 10))
.pipe(z.number().positive());
Coercion
// Automatic coercion
const coercedString = z.coerce.string(); // Any -> string
const coercedNumber = z.coerce.number(); // Any -> number
const coercedBoolean = z.coerce.boolean(); // Any -> boolean
const coercedDate = z.coerce.date(); // Any -> Date
// Common use: form data
const FormSchema = z.object({
age: z.coerce.number().min(0).max(120),
active: z.coerce.boolean(),
date: z.coerce.date(),
});
Preprocess
// Run function before validation
const TrimmedString = z.preprocess(
(val) => (typeof val === 'string' ? val.trim() : val),
z.string()
);
// Handle null/undefined
const NullableToDefault = z.preprocess(
(val) => val ?? 'default',
z.string()
);
Default Values
const WithDefault = z.string().default('default value');
const WithDefaultFn = z.string().default(() => generateId());
// Catch: use default on parse failure
const SafeNumber = z.number().catch(0);
const SafeString = z.string().catch('');
Refinements
Custom Validation
// Simple refinement
const PasswordSchema = z.string()
.min(8, 'Password must be at least 8 characters')
.refine((val) => /[A-Z]/.test(val), {
message: 'Password must contain uppercase letter',
})
.refine((val) => /[0-9]/.test(val), {
message: 'Password must contain number',
});
// Superrefine (multiple issues)
const PasswordSchema2 = z.string().superRefine((val, ctx) => {
if (val.length < 8) {
ctx.addIssue({
code: z.ZodIssueCode.too_small,
minimum: 8,
type: 'string',
inclusive: true,
message: 'Password must be at least 8 characters',
});
}
if (!/[A-Z]/.test(val)) {
ctx.addIssue({
code: z.ZodIssueCode.custom,
message: 'Password must contain uppercase letter',
});
}
});
Cross-Field Validation
const SignupSchema = z.object({
password: z.string().min(8),
confirmPassword: z.string(),
}).refine((data) => data.password === data.confirmPassword, {
message: "Passwords don't match",
path: ['confirmPassword'], // Error on specific field
});
Error Handling
Safe Parse
const result = UserSchema.safeParse(data);
if (result.success) {
// result.data is typed
console.log(result.data);
} else {
// result.error is ZodError
console.log(result.error.issues);
}
Parse (throws)
try {
const user = UserSchema.parse(data);
// user is typed
} catch (error) {
if (error instanceof z.ZodError) {
console.log(error.issues);
}
}
Format Errors
const result = UserSchema.safeParse(data);
if (!result.success) {
// Flat format
const flat = result.error.flatten();
// { formErrors: string[], fieldErrors: { [key]: string[] } }
// Formatted
const formatted = result.error.format();
// { _errors: string[], field: { _errors: string[] } }
// Custom format
const issues = result.error.issues.map((issue) => ({
path: issue.path.join('.'),
message: issue.message,
}));
}
Custom Error Messages
const UserSchema = z.object({
name: z.string({
required_error: 'Name is required',
invalid_type_error: 'Name must be a string',
}).min(1, 'Name cannot be empty'),
email: z.string()
.email({ message: 'Invalid email format' }),
age: z.number()
.min(18, { message: 'Must be 18 or older' })
.max(120, { message: 'Invalid age' }),
});
Common Patterns
API Request Validation
const CreatePostSchema = z.object({
title: z.string().min(1).max(100),
content: z.string().min(1),
tags: z.array(z.string()).default([]),
published: z.boolean().default(false),
});
// In API handler
export async function POST(request: Request) {
const body = await request.json();
const result = CreatePostSchema.safeParse(body);
if (!result.success) {
return Response.json(
{ error: result.error.flatten().fieldErrors },
{ status: 400 }
);
}
const post = await createPost(result.data);
return Response.json(post);
}
Environment Variables
const EnvSchema = z.object({
DATABASE_URL: z.string().url(),
API_KEY: z.string().min(1),
PORT: z.coerce.number().default(3000),
NODE_ENV: z.enum(['development', 'production', 'test']).default('development'),
});
export const env = EnvSchema.parse(process.env);
Form Data
const ContactFormSchema = z.object({
name: z.string().min(1, 'Name is required'),
email: z.string().email('Invalid email'),
message: z.string().min(10, 'Message too short').max(1000),
subscribe: z.coerce.boolean().default(false),
});
// Parse FormData
function parseFormData(formData: FormData) {
return ContactFormSchema.safeParse({
name: formData.get('name'),
email: formData.get('email'),
message: formData.get('message'),
subscribe: formData.get('subscribe'),
});
}
React Hook Form Integration
import { useForm } from 'react-hook-form';
import { zodResolver } from '@hookform/resolvers/zod';
const FormSchema = z.object({
email: z.string().email(),
password: z.string().min(8),
});
type FormData = z.infer<typeof FormSchema>;
function LoginForm() {
const {
register,
handleSubmit,
formState: { errors },
} = useForm<FormData>({
resolver: zodResolver(FormSchema),
});
const onSubmit = (data: FormData) => {
console.log(data);
};
return (
<form onSubmit={handleSubmit(onSubmit)}>
<input {...register('email')} />
{errors.email && <span>{errors.email.message}</span>}
<input type="password" {...register('password')} />
{errors.password && <span>{errors.password.message}</span>}
<button type="submit">Login</button>
</form>
);
}
Server Actions (Next.js)
'use server';
import { z } from 'zod';
const CreatePostSchema = z.object({
title: z.string().min(1).max(100),
content: z.string().min(1),
});
export async function createPost(formData: FormData) {
const result = CreatePostSchema.safeParse({
title: formData.get('title'),
content: formData.get('content'),
});
if (!result.success) {
return { error: result.error.flatten().fieldErrors };
}
const post = await db.posts.create({ data: result.data });
return { data: post };
}
Type Inference
const UserSchema = z.object({
id: z.string(),
name: z.string(),
email: z.string().email(),
});
// Infer type from schema
type User = z.infer<typeof UserSchema>;
// Input type (before transforms)
type UserInput = z.input<typeof UserSchema>;
// Output type (after transforms)
type UserOutput = z.output<typeof UserSchema>;
Best Practices
- Define schemas once - Reuse across validation points
- Use safeParse - Handle errors gracefully
- Infer types - Don't duplicate type definitions
- Use coercion for forms - Handle string inputs
- Custom error messages - User-friendly validation
Common Mistakes
| Mistake | Fix |
|---|---|
| Using parse without try/catch | Use safeParse instead |
| Not handling errors | Check result.success |
| Duplicating types | Use z.infer |
| Ignoring transforms | Remember input vs output types |
| Over-complex schemas | Break into smaller schemas |
Reference Files
- references/advanced.md - Advanced patterns
- references/integration.md - Framework integrations