| 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());
z.().( ());
z.();
z.();
z.();
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({ : z.() });
namedUser = z.({ : z.() });
user = z.(baseUser, namedUser);
user = baseUser.({ : z.() });
z.([, , ]);
z.();
z.(z.());
z.(z.(), z.());
z.(z.(), z.());
z.(z.());
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 (!.(val)) {
ctx.({
: z..,
: ,
});
}
});
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();
schema = userSchema.();
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: });
schema = z.().({ : });
schema = z.().(, );
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 {('')} />
{errors.name && {errors.name.message}}
{errors.email && {errors.email.message}}
{errors.age && {errors.age.message}}
Submit
);
}
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',
,
,
,
,
])).(),
: z.(z.()).(),
}).(
data. > data.,
{ : }
);
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