| name | Zod |
| description | Expert guidance for Zod schema validation including type inference, schema composition, parsing, refinements, transformations, error handling, and TypeScript integration. Use this when building type-safe validation, form validation, or API input validation. |
Zod
Expert assistance with Zod - TypeScript-first schema validation.
Overview
Zod is a TypeScript-first schema declaration and validation library:
- Type Inference: Automatic TypeScript type inference
- Zero Dependencies: No runtime dependencies
- Composable: Build complex schemas from simple ones
- Developer Experience: Excellent autocomplete and error messages
Installation
npm install zod
Basic Usage
import { z } from 'zod';
const userSchema = z.object({
name: z.string(),
age: z.number(),
email: z.string().email(),
});
type User = z.infer<typeof userSchema>;
const user = userSchema.parse({
name: 'John',
age: 30,
email: 'john@example.com',
});
const result = userSchema.safeParse({ name: 'John', age: '30' });
if (result.success) {
console.log(result.data);
} else {
console.error(result.error);
}
Primitive Types
z.string();
z.string().min(5);
z.string().max(100);
z.string().length(10);
z.string().email();
z.string().url();
z.string().uuid();
z.string().regex(/^[a-z]+$/);
z.string().startsWith('https://');
z.string().endsWith('.com');
z.number();
z.number().int();
z.number().positive();
z.number().negative();
z.number().min(0);
z.number().max(100);
z.number().multipleOf(5);
z.boolean();
z.date();
z.date().min(new Date('2024-01-01'));
z.date().max(new Date('2025-01-01'));
z.literal('admin');
z.literal(42);
z.literal(true);
Complex Types
const userSchema = z.object({
name: z.string(),
age: z.number(),
});
z.array(z.string());
z.array(z.number()).min(1).max(10);
z.tuple([z.string(), z.number(), z.boolean()]);
z.union([z.string(), z.number()]);
z.string().or(z.number());
const shapeSchema = z.discriminatedUnion('kind', [
z.object({ kind: z.literal('circle'), radius: z.number() }),
z.object({ kind: z.literal('rectangle'), width: z.number(), height: z.number() }),
]);
const baseUser = z.object({ id: z.string() });
const namedUser = z.object({ name: z.string() });
const user = z.intersection(baseUser, namedUser);
const user = baseUser.extend({ name: z.string() });
z.enum(['admin', 'user', 'guest']);
z.nativeEnum(MyEnum);
z.record(z.string());
z.record(z.string(), z.number());
z.map(z.string(), z.number());
z.set(z.string());
Modifiers
z.string().optional();
z.object({ name: z.string().optional() });
z.string().nullable();
z.string().nullish();
z.string().default('default value');
z.number().default(0);
z.string().catch('fallback');
Refinements
const passwordSchema = z.string().refine(
(val) => val.length >= 8,
{ message: 'Password must be at least 8 characters' }
);
const schema = z.string()
.min(8)
.refine((val) => /[A-Z]/.test(val), {
message: 'Must contain uppercase letter',
})
.refine((val) => /[0-9]/.test(val), {
message: 'Must contain number',
});
const schema = z.string().superRefine((val, ctx) => {
if (val.length < 8) {
ctx.addIssue({
code: z.ZodIssueCode.too_small,
minimum: 8,
type: 'string',
inclusive: true,
message: 'Too short',
});
}
if (!/[A-Z]/.test(val)) {
ctx.addIssue({
code: z.ZodIssueCode.custom,
message: 'Must contain uppercase',
});
}
});
Transformations
const schema = z.string().transform((val) => val.toLowerCase());
const schema = z.string()
.transform((val) => val.trim())
.transform((val) => val.toLowerCase());
const numberSchema = z.string().transform((val) => parseInt(val, 10));
const schema = z.preprocess(
(val) => (typeof val === 'string' ? val.trim() : val),
z.string().min(1)
);
Object Methods
const userSchema = z.object({
id: z.string(),
name: z.string(),
email: z.string(),
age: z.number(),
});
const nameOnly = userSchema.pick({ name: true });
const withoutId = userSchema.omit({ id: true });
const partialUser = userSchema.partial();
const deepPartial = userSchema.deepPartial();
const required = partialUser.required();
const extendedUser = userSchema.extend({
role: z.enum(['admin', 'user']),
});
const merged = userSchema.merge(z.object({ role: z.string() }));
const schema = userSchema.passthrough();
const schema = userSchema.strict();
const schema = userSchema.strip();
Error Handling
const schema = z.object({
name: z.string().min(2),
age: z.number().min(18),
});
const result = schema.safeParse({ name: 'J', age: 15 });
if (!result.success) {
console.log(result.error);
console.log(result.error.format());
console.log(result.error.flatten());
console.log(result.error.issues[0]);
}
const schema = z.string().min(5, { message: 'Too short!' });
const schema = z.string().email({ message: 'Invalid email address' });
const schema = z.string().min(5, 'Custom error');
Async Validation
const schema = z.string().refine(
async (email) => {
const exists = await checkEmailExists(email);
return !exists;
},
{ message: 'Email already exists' }
);
const result = await schema.parseAsync('test@example.com');
const result = await schema.safeParseAsync('test@example.com');
React Hook Form Integration
import { useForm } from 'react-hook-form';
import { zodResolver } from '@hookform/resolvers/zod';
import { z } from 'zod';
const formSchema = z.object({
name: z.string().min(2, 'Name must be at least 2 characters'),
email: z.string().email('Invalid email address'),
age: z.number().min(18, 'Must be 18 or older'),
});
type FormData = z.infer<typeof formSchema>;
function MyForm() {
const { register, handleSubmit, formState: { errors } } = useForm<FormData>({
resolver: zodResolver(formSchema),
});
const onSubmit = (data: FormData) => {
console.log(data);
};
return (
<form onSubmit={handleSubmit(onSubmit)}>
<input {...register('name')} />
{errors.name && <span>{errors.name.message}</span>}
<input {...register('email')} />
{errors.email && <span>{errors.email.message}</span>}
<input {...register('age', { valueAsNumber: true })} type="number" />
{errors.age && <span>{errors.age.message}</span>}
<button type="submit">Submit</button>
</form>
);
}
tRPC Integration
import { z } from 'zod';
import { publicProcedure, router } from './trpc';
const createUserSchema = z.object({
name: z.string().min(2),
email: z.string().email(),
});
export const userRouter = router({
create: publicProcedure
.input(createUserSchema)
.mutation(({ input }) => {
const { name, email } = input;
return createUser({ name, email });
}),
});
Common Patterns
PKI Certificate Validation
const distinguishedNameSchema = z.object({
commonName: z.string().min(1),
organization: z.string().optional(),
organizationalUnit: z.string().optional(),
country: z.string().length(2).optional(),
state: z.string().optional(),
locality: z.string().optional(),
});
const certificateSchema = z.object({
subject: distinguishedNameSchema,
issuer: distinguishedNameSchema,
serialNumber: z.string(),
notBefore: z.date(),
notAfter: z.date(),
keyUsage: z.array(z.enum([
'digitalSignature',
'nonRepudiation',
'keyEncipherment',
'dataEncipherment',
'keyAgreement',
'keyCertSign',
'cRLSign',
])),
extendedKeyUsage: z.array(z.enum([
'serverAuth',
'clientAuth',
'codeSigning',
'emailProtection',
'timeStamping',
'OCSPSigning',
])).optional(),
subjectAlternativeNames: z.array(z.string()).optional(),
}).refine(
(data) => data.notAfter > data.notBefore,
{ message: 'notAfter must be after notBefore' }
);
API Response Validation
const apiResponseSchema = z.object({
success: z.boolean(),
data: z.unknown().optional(),
error: z.object({
code: z.string(),
message: z.string(),
}).optional(),
}).refine(
(data) => data.success ? data.data !== undefined : data.error !== undefined,
{ message: 'Response must have data if success, or error if not' }
);
Best Practices
- Type Inference: Always use
z.infer<typeof schema> for types
- Reusable Schemas: Define common schemas once, reuse everywhere
- Composition: Build complex schemas from simple ones
- Error Messages: Provide clear custom error messages
- safeParse: Use
safeParse when you want to handle errors yourself
- Transformations: Use transforms to normalize data
- Refinements: Use refinements for complex business logic
- Optional vs Nullable: Understand the difference
- Strict Mode: Use
.strict() on objects to catch extra fields
- Documentation: Add JSDoc comments to schemas
Resources