| name | zod |
| description | TypeScript-first schema validation library with static type inference for form validation, API validation, and runtime type checking with compile-time types. |
| user-invocable | false |
| disable-model-invocation | true |
| progressive_disclosure | {"entry_point":["summary","when_to_use","quick_start"],"sections":["primitives","objects_and_arrays","type_inference","validation_methods","schema_composition","transformations","error_handling","async_validation","advanced_types","api_handler_patterns","integrations","best_practices"]} |
| token_estimates | {"entry":65,"primitives":200,"objects_and_arrays":300,"type_inference":150,"validation_methods":250,"schema_composition":300,"transformations":400,"error_handling":250,"async_validation":200,"advanced_types":500,"api_handler_patterns":800,"integrations":1200,"best_practices":300,"full":5800} |
Zod Validation Skill
Summary
TypeScript-first schema validation library with static type inference. Define schemas once, get runtime validation and compile-time types automatically.
When to Use
- Form validation with type-safe data
- API request/response validation
- Environment variable validation
- Runtime type checking with TypeScript inference
- tRPC procedure inputs/outputs
- Database schema validation (Drizzle, Prisma)
Quick Start
import { z } from 'zod';
const UserSchema = z.object({
id: z.string().uuid(),
email: z.string().email(),
age: z.number().min(18),
role: z.enum(['user', 'admin'])
});
type User = z.infer<typeof UserSchema>;
const result = UserSchema.safeParse(data);
if (result.success) {
const user: User = result.data;
}
Primitive Types
Basic Types
import { z } from 'zod';
const nameSchema = z.string()
.min(2, "Too short")
.max(50, "Too long")
.trim();
const emailSchema = z.string().email();
const urlSchema = z.string().url();
const uuidSchema = z.string().uuid();
const regexSchema = z.string().regex(/^[A-Z]{3}$/);
const ageSchema = z.number()
.int("Must be integer")
.positive()
.min(0)
.max(120);
const priceSchema = z.number()
.positive()
.multipleOf(0.01);
const isActiveSchema = z.boolean();
const createdAtSchema = z.date()
.min(new ())
.( ());
dateStringSchema = z.().();
dateOnlySchema = z.().();
Special Types
const roleSchema = z.literal('admin');
const statusSchema = z.literal('pending');
const ColorEnum = z.enum(['red', 'green', 'blue']);
type Color = z.infer<typeof ColorEnum>;
const NativeEnum = z.nativeEnum(MyEnum);
const optionalString = z.string().optional();
const nullableString = z.string().nullable();
const nullishString = z.string().nullish();
const countSchema = z.number().default(0);
const settingsSchema = z.object({
theme: z.string().default('light'),
: z.().()
});
Objects and Arrays
Object Schemas
const UserSchema = z.object({
id: z.string(),
email: z.string().email(),
name: z.string(),
age: z.number().optional()
});
const AddressSchema = z.object({
street: z.string(),
city: z.string(),
country: z.string(),
zipCode: z.string()
});
const PersonSchema = z.object({
name: z.string(),
address: AddressSchema,
contacts: z.object({
email: z.string().email(),
phone: z.string().optional()
})
});
const strictSchema = z.object({ name: z.string() }).strict();
const passthroughSchema = z.object({ : z.() }).();
stripSchema = z.({ : z.() }).();
Array Schemas
const stringArray = z.array(z.string());
const numberArray = z.array(z.number()).min(1).max(10);
const UsersSchema = z.array(UserSchema);
const tagSchema = z.array(z.string()).nonempty("At least one tag required");
const coordinateSchema = z.tuple([z.number(), z.number()]);
type Coordinate = z.infer<typeof coordinateSchema>;
const csvRowSchema = z.tuple([z.string(), z.number()]).rest(z.string());
Records and Maps
const userRolesSchema = z.record(
z.string(),
z.enum(['admin', 'user', 'guest'])
);
type UserRoles = z.infer<typeof userRolesSchema>;
const configMapSchema = z.map(
z.string(),
z.number()
);
const uniqueTagsSchema = z.set(z.string());
Type Inference
import { z } from 'zod';
const UserSchema = z.object({
id: z.string(),
email: z.string().email(),
age: z.number()
});
type User = z.infer<typeof UserSchema>;
const TransformSchema = z.object({
date: z.string().transform(s => new Date(s))
});
type Input = z.input<typeof TransformSchema>;
type Output = z.output<typeof TransformSchema>;
function createUser(data: User): void {
}
(): | {
result = .(data);
result. ? result. : ;
}
Validation Methods
Parse vs SafeParse
try {
const user = UserSchema.parse(data);
} catch (error) {
if (error instanceof z.ZodError) {
console.error(error.issues);
}
}
const result = UserSchema.safeParse(data);
if (result.success) {
const user = result.data;
} else {
const errors = result.error.issues;
errors.forEach(err => {
console.log(`${err.path}: ${err.message}`);
});
}
const asyncResult = await UserSchema.parseAsync(data);
const asyncSafeResult = await UserSchema.safeParseAsync(data);
Partial Validation
const isValid = UserSchema.safeParse(data).success;
function isUser(data: unknown): data is User {
return UserSchema.safeParse(data).success;
}
if (isUser(unknownData)) {
console.log(unknownData.email);
}
Schema Composition
Extending and Merging
const BaseUserSchema = z.object({
id: z.string(),
email: z.string()
});
const AdminUserSchema = BaseUserSchema.extend({
role: z.literal('admin'),
permissions: z.array(z.string())
});
const NameSchema = z.object({ name: z.string() });
const AgeSchema = z.object({ age: z.number() });
const PersonSchema = NameSchema.merge(AgeSchema);
const UserIdEmail = UserSchema.pick({ id: true, email: true });
const UserWithoutId = UserSchema.omit({ id: true });
= .();
= .();
= .();
Union and Intersection
const StringOrNumber = z.union([z.string(), z.number()]);
const StringOrNumberAlt = z.string().or(z.number());
const SuccessResponse = z.object({
status: z.literal('success'),
data: z.any()
});
const ErrorResponse = z.object({
status: z.literal('error'),
message: z.string()
});
const ApiResponse = z.discriminatedUnion('status', [
SuccessResponse,
ErrorResponse
]);
const User = z.object({ name: z.string() });
const Timestamps = z.object({
createdAt: z.date(),
updatedAt: z.date()
});
const UserWithTimestamps = z.(, );
= .();
Transformations and Refinements
Transform
const StringToNumber = z.string().transform(val => parseInt(val, 10));
const DateSchema = z.string().transform(str => new Date(str));
const TrimmedLowercase = z.string()
.transform(s => s.trim())
.transform(s => s.toLowerCase());
const PositiveStringNumber = z.string()
.transform(val => parseInt(val, 10))
.refine(n => n > 0, "Must be positive");
const UserInputSchema = z.object({
name: z.string().transform(s => s.()),
: z.().().( s.()),
: z.().( (s)),
: z.().( s.().( t.()))
});
= z.< >;
= z.< >;
Refine (Custom Validation)
const PasswordSchema = z.string()
.min(8)
.refine(
val => /[A-Z]/.test(val),
"Must contain uppercase letter"
)
.refine(
val => /[0-9]/.test(val),
"Must contain number"
);
const UniqueEmailSchema = z.string().email().refine(
async (email) => {
const exists = await checkEmailExists(email);
return !exists;
},
{ message: "Email already taken" }
);
const PasswordMatchSchema = z.object({
password: z.string(),
confirmPassword: z.string()
}).refine(
data => data.password === data.confirmPassword,
{
message: "Passwords don't match",
path: ["confirmPassword"]
}
);
= z.({
: z.(),
: z.()
}).(
data. > data.,
{
: ,
: []
}
);
SuperRefine (Advanced)
const ComplexSchema = z.object({
type: z.enum(['email', 'phone']),
value: z.string()
}).superRefine((data, ctx) => {
if (data.type === 'email') {
const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
if (!emailRegex.test(data.value)) {
ctx.addIssue({
code: z.ZodIssueCode.custom,
message: "Invalid email format",
path: ["value"]
});
}
} else if (data.type === 'phone') {
const phoneRegex = /^\+?[1-9]\d{1,14}$/;
if (!phoneRegex.test(data.value)) {
ctx.addIssue({
code: z.ZodIssueCode.custom,
message: "Invalid phone format",
path: ["value"]
});
}
}
});
const RegistrationSchema = z.({
: z.(),
: z.(),
: z.()
}).( (data, ctx) => {
( (data.)) {
ctx.({
: z..,
: ,
: []
});
}
( (data.)) {
ctx.({
: z..,
: ,
: []
});
}
(data. < ) {
ctx.({
: z..,
: ,
: []
});
}
});
Error Handling
Custom Error Messages
const UserSchema = z.object({
email: z.string().email({ message: "Invalid email address" }),
age: z.number({
required_error: "Age is required",
invalid_type_error: "Age must be a number"
}).min(18, { message: "Must be 18 or older" })
});
import { z } from 'zod';
const customErrorMap: z.ZodErrorMap = (issue, ctx) => {
if (issue.code === z.ZodIssueCode.invalid_type) {
if (issue.expected === "string") {
return { message: "This field must be text" };
}
}
if (issue.code === z.ZodIssueCode.too_small) {
if (issue.type === "string") {
return { message: `Minimum ${issue.minimum} characters required` };
}
}
return { message: ctx. };
};
z.(customErrorMap);
Processing Errors
const result = UserSchema.safeParse(data);
if (!result.success) {
const flatErrors = result.error.flatten();
console.log(flatErrors.formErrors);
console.log(flatErrors.fieldErrors);
}
function formatZodError(error: z.ZodError) {
return error.issues.map(issue => ({
field: issue.path.join('.'),
message: issue.message
}));
}
const result = UserSchema.safeParse(data);
if (!result.success) {
return res.status(400).json({
errors: formatZodError(result.error)
});
}
Async Validation
import { z } from 'zod';
const UsernameSchema = z.string().refine(
async (username) => {
const available = await checkUsernameAvailable(username);
return available;
},
{ message: "Username already taken" }
);
const result = await UsernameSchema.safeParseAsync("john_doe");
const RegistrationSchema = z.object({
username: z.string().refine(
async (val) => !(await usernameTaken(val)),
"Username taken"
),
email: z.string().email().refine(
async (val) => !(await emailTaken(val)),
"Email already registered"
),
inviteCode: z.string().refine(
async (code) => await validateInviteCode(code),
"Invalid invite code"
)
});
userData = .(input);
result = .(input);
(!result.) {
}
Advanced Types
Recursive Types
type Category = {
name: string;
subcategories: Category[];
};
const CategorySchema: z.ZodType<Category> = z.lazy(() =>
z.object({
name: z.string(),
subcategories: z.array(CategorySchema)
})
);
type TreeNode = {
value: number;
left?: TreeNode;
right?: TreeNode;
};
const TreeNodeSchema: z.ZodType<TreeNode> = z.lazy(() =>
z.object({
value: z.number(),
left: TreeNodeSchema.optional(),
right: TreeNodeSchema.optional()
})
);
Discriminated Unions
const Circle = z.object({
kind: z.literal('circle'),
radius: z.number()
});
const Rectangle = z.object({
kind: z.literal('rectangle'),
width: z.number(),
height: z.number()
});
const Triangle = z.object({
kind: z.literal('triangle'),
base: z.number(),
height: z.number()
});
const Shape = z.discriminatedUnion('kind', [
Circle,
Rectangle,
Triangle
]);
type Shape = z.infer<typeof Shape>;
function calculateArea(shape: Shape): number {
switch (shape.kind) {
case :
. * shape. ** ;
:
shape. * shape.;
:
(shape. * shape.) / ;
}
}
Preprocess
const NumberFromString = z.preprocess(
(val) => (typeof val === 'string' ? parseInt(val, 10) : val),
z.number()
);
const TrimmedString = z.preprocess(
(val) => (typeof val === 'string' ? val.trim() : val),
z.string()
);
const JsonSchema = z.preprocess(
(val) => (typeof val === 'string' ? JSON.parse(val) : val),
z.object({
name: z.string(),
age: z.number()
})
);
const FormDataSchema = z.preprocess(
(data) => {
if (data instanceof FormData) {
return Object.fromEntries(data.entries());
}
data;
},
z.({
: z.(),
: z.().()
})
);
Branded Types
const UserId = z.string().uuid().brand<'UserId'>();
type UserId = z.infer<typeof UserId>;
const Email = z.string().email().brand<'Email'>();
type Email = z.infer<typeof Email>;
function getUserById(id: UserId) { }
function sendEmail(to: Email) { }
const userId = UserId.parse('123e4567-e89b-12d3-a456-426614174000');
const email = Email.parse('user@example.com');
getUserById(userId);
getUserById(email);
API Handler Patterns
Generic Validated Handler
Create type-safe API handlers with automatic validation, error formatting, and authentication.
import { NextRequest, NextResponse } from 'next/server';
import { z } from 'zod';
type ValidatedHandler<T, C = T> = (
state: {
input: T;
request: NextRequest;
ctx: RouteContext<C>;
}
) => Promise<Response> | Response;
interface RouteContext<T = unknown> {
params: T;
}
interface ValidationConfig<T extends z.ZodType, C extends z.ZodType> {
input?: {
schema: T;
source: 'body' | 'query' | 'params';
};
auth?: {
role: 'admin' | 'provider' | 'user';
};
context?: C;
}
export function validatedHandler<
T extends z.ZodType,
C extends z.ZodType = z.ZodType<any>
>(
: <T, C>,
: <z.<T>, z.<C>>
) {
(
: ,
: <z.<C>>
): <> => {
{
: ;
(config.) {
(config..) {
:
rawInput = request.();
;
:
rawInput = .(request..);
;
:
rawInput = ctx.;
;
}
result = config...(rawInput);
(!result.) {
.(
{
: ,
: result...( ({
: err..(),
: err.,
: err.,
})),
},
{ : }
);
}
({
: result.,
request,
ctx
});
}
({
: {} z.<T>,
request,
ctx
});
} (error) {
.(, error);
.(
{ : },
{ : }
);
}
};
}
Usage Example:
const CreateCampSchema = z.object({
name: z.string().min(3),
location: z.string(),
startDate: z.string().datetime(),
endDate: z.string().datetime(),
capacity: z.number().positive(),
});
export const POST = validatedHandler(
{
input: {
schema: CreateCampSchema,
source: 'body',
},
auth: { role: 'admin' },
},
async ({ input, request }) => {
const camp = await createCamp({
name: input.name,
location: input.location,
startDate: new Date(input.startDate),
endDate: new Date(input.endDate),
capacity: input.,
});
.(camp, { : });
}
);
Discriminated Unions for Complex Types
Use discriminated unions to handle multiple input types with type-safe narrowing.
const LocationSelectionSchema = z.discriminatedUnion('type', [
z.object({
type: z.literal('map-bounds'),
north: z.number().min(-90).max(90),
south: z.number().min(-90).max(90),
east: z.number().min(-180).max(180),
west: z.number().min(-180).max(180),
}),
z.object({
type: z.literal('address'),
address: z.string().min(1),
radius: z.number().positive().optional(),
}),
z.object({
type: z.literal('user-location'),
lat: z.number().min(-).(),
: z.().(-).(),
: z.().().(),
}),
]);
= z.< >;
() {
(selection.) {
:
({
: selection.,
: selection.,
: selection.,
: selection.,
});
:
({
: selection.,
: selection.,
});
:
({
: selection.,
: selection.,
: selection.,
});
}
}
Complete Type Mapping with Required
Ensure all keys are mapped with TypeScript's Required utility type.
import { NumberParam, StringParam, DateParam } from 'next-query-params';
interface Filters {
startDate?: Date;
endDate?: Date;
status?: 'active' | 'inactive' | 'pending';
search?: string;
minPrice?: number;
maxPrice?: number;
}
interface QueryParamConfig<T> {
encode: (value: T) => string;
decode: (value: string | undefined) => T | undefined;
}
const filtersQueryParamConfigMap: {
[Key in keyof Required<Filters>]: QueryParamConfig<Filters[Key]>;
} = {
startDate: DateParam,
endDate: DateParam,
status: ,
: ,
: ,
: ,
};
= z.({
: z.().(),
: z.().(),
: z.([, , ]).(),
: z.().(),
: z.().().(),
: z.().().(),
});
= .(
{
(data. && data.) {
data. >= data.;
}
;
},
{
: ,
: [],
}
).(
{
(data. && data.) {
data. >= data.;
}
;
},
{
: ,
: [],
}
);
Structured Error Response Format
Create consistent, type-safe error responses.
const ApiErrorSchema = z.object({
code: z.enum([
'VALIDATION_ERROR',
'AUTHENTICATION_ERROR',
'AUTHORIZATION_ERROR',
'NOT_FOUND',
'INTERNAL_ERROR',
]),
message: z.string(),
details: z.array(
z.object({
path: z.string(),
message: z.string(),
code: z.string().optional(),
})
).optional(),
timestamp: z.string().datetime(),
});
type ApiError = z.infer<typeof ApiErrorSchema>;
function formatZodError(error: z.ZodError): ApiError {
return {
code: 'VALIDATION_ERROR',
message: 'Input validation failed',
details: error.issues.map(issue => ({
path: issue..(),
: issue.,
: issue.,
})),
: ().(),
};
}
(): {
: = {
code,
message,
details,
: ().(),
};
.(error, { status });
}
= (
{
: { : , : },
},
({ input }) => {
{
user = (input);
.(user, { : });
} (error) {
(error z.) {
.(
(error),
{ : }
);
}
(
,
,
);
}
}
);
Query Parameter Transformation
Handle query parameter parsing with validation and transformation.
const SearchParamsSchema = z.object({
page: z
.string()
.transform(Number)
.pipe(z.number().int().positive().default(1)),
pageSize: z
.string()
.transform(Number)
.pipe(z.number().int().min(1).max(100).default(20)),
tags: z
.string()
.optional()
.transform(val => val ? val.split(',').map(t => t.trim()) : []),
startDate: z
.string()
.datetime()
.optional()
.transform( => val ? (val) : ),
: z
.()
.()
.( val === )
.(z.().()),
: z
.([, , ])
.(),
: z
.([, ])
.(),
});
= (
{
: {
: ,
: ,
},
},
({ input }) => {
results = ({
: input.,
: input.,
: input.,
: input.,
: input.,
: input.,
: input.,
});
.(results);
}
);
Schema Composition for Reusable Validation
Build complex schemas from reusable parts.
const TimestampSchema = z.object({
createdAt: z.date(),
updatedAt: z.date(),
});
const PaginationSchema = z.object({
page: z.number().int().positive(),
pageSize: z.number().int().min(1).max(100),
total: z.number().int().nonnegative(),
});
const LocationSchema = z.object({
lat: z.number().min(-90).max(90),
lng: z.number().min(-180).max(180),
address: z.string().optional(),
});
const CampSchema = z.object({
id: z.string().uuid(),
name: z.().(),
: ,
: z.().(),
: z.([, , ]),
}).();
= z.({
: z.(),
: ,
});
= z.< >;
= z.< >;
= (
{
: {
: z.({
: z.().(),
: z.().(),
}),
: ,
},
},
({ input }) => {
camps = (input., input.);
: = {
camps,
: {
: input.,
: input.,
: (),
},
};
.(response);
}
);
Integrations
React Hook Form
import { useForm } from 'react-hook-form';
import { zodResolver } from '@hookform/resolvers/zod';
import { z } from 'zod';
const FormSchema = z.object({
username: z.string().min(3, "Minimum 3 characters"),
email: z.string().email("Invalid email"),
age: z.number().min(18, "Must be 18+")
});
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 =>
{errors.username && {errors.username.message}}
{errors.email && {errors.email.message}}
{errors.age && {errors.age.message}}
Submit
);
}
tRPC
import { z } from 'zod';
import { initTRPC } from '@trpc/server';
const t = initTRPC.create();
const router = t.router;
const publicProcedure = t.procedure;
const appRouter = router({
userById: publicProcedure
.input(z.object({
id: z.string().uuid()
}))
.output(z.object({
id: z.string().uuid(),
name: z.string(),
email: z.string().email()
}))
.query(async ({ input }) => {
const user = await db.user.findUnique({
where: { id: input.id }
});
return user;
}),
createUser: publicProcedure
.input(z.object({
name: z.string().min(2),
: z.().(),
: z.().()
}))
.( ({ input }) => {
db..({ : input });
})
});
= appRouter;
Next.js API Routes
import { NextRequest, NextResponse } from 'next/server';
import { z } from 'zod';
const CreateUserSchema = z.object({
name: z.string().min(2),
email: z.string().email(),
age: z.number().min(18).optional()
});
export async function POST(request: NextRequest) {
try {
const body = await request.json();
const validatedData = CreateUserSchema.parse(body);
const user = await createUser(validatedData);
return NextResponse.json(user, { status: 201 });
} catch (error) {
if (error instanceof z.ZodError) {
.(
{ : error.(). },
{ : }
);
}
.(
{ : },
{ : }
);
}
}
= z.({
: z.().().(z.().()).(),
: z.().().(z.().()).(),
: z.([, ]).()
});
() {
searchParams = .(
request...()
);
params = .(searchParams);
users = (params);
.(users);
}
Express Middleware
import express from 'express';
import { z } from 'zod';
const validate = (schema: z.ZodSchema) => {
return (req: express.Request, res: express.Response, next: express.NextFunction) => {
try {
schema.parse(req.body);
next();
} catch (error) {
if (error instanceof z.ZodError) {
return res.status(400).json({
errors: error.flatten().fieldErrors
});
}
next(error);
}
};
};
const CreateUserSchema = z.object({
name: z.string(),
email: z.string().email(),
age: z.number().min(18)
});
app.post('/users', validate(), (req, res) => {
user = (req.);
res.(user);
});
= () => {
{
{
(schema.) {
req. = schema..(req.);
}
(schema.) {
req. = schema..(req.);
}
(schema.) {
req. = schema..(req.);
}
();
} (error) {
(error z.) {
res.().({ : error. });
}
(error);
}
};
};
app.(
,
({
: z.({ : z.().() }),
: z.({ : z.().() })
}),
(req, res) => {
}
);
Drizzle ORM
import { z } from 'zod';
import { pgTable, serial, text, integer } from 'drizzle-orm/pg-core';
import { createInsertSchema, createSelectSchema } from 'drizzle-zod';
export const users = pgTable('users', {
id: serial('id').primaryKey(),
name: text('name').notNull(),
email: text('email').notNull().unique(),
age: integer('age')
});
export const insertUserSchema = createInsertSchema(users);
export const selectUserSchema = createSelectSchema(users);
export const customInsertUserSchema = createInsertSchema(users, {
email: z.string().email(),
age: z.number().min(18).optional()
});
= z.< insertUserSchema>;
= z.< selectUserSchema>;
() {
validatedData = insertUserSchema.(data);
db.(users).(validatedData);
}
Environment Variables
import { z } from 'zod';
const envSchema = z.object({
NODE_ENV: z.enum(['development', 'production', 'test']),
DATABASE_URL: z.string().url(),
API_KEY: z.string().min(32),
PORT: z.string().transform(Number).pipe(z.number().min(1024)),
REDIS_HOST: z.string().default('localhost'),
REDIS_PORT: z.string().transform(Number).default('6379'),
LOG_LEVEL: z.enum(['debug', 'info', 'warn', 'error']).default('info')
});
export const env = envSchema.parse(process.env);
export type = z.< envSchema>;
.();
Best Practices
Schema Organization
import { z } from 'zod';
export const emailSchema = z.string().email();
export const uuidSchema = z.string().uuid();
export const passwordSchema = z.string()
.min(8)
.regex(/[A-Z]/, "Must contain uppercase")
.regex(/[0-9]/, "Must contain number");
export const baseUserSchema = z.object({
id: uuidSchema,
email: emailSchema,
name: z.string().min(2)
});
export const createUserSchema = baseUserSchema.omit({ id: true }).extend({
password: passwordSchema,
confirmPassword: z.string()
}).refine(
data => data.password === data.confirmPassword,
{ message: , : [] }
);
updateUserSchema = baseUserSchema.().({ : });
= z.< baseUserSchema>;
= z.< createUserSchema>;
= z.< updateUserSchema>;
Performance Optimization
const userSchemaCache = new Map<string, z.ZodSchema>();
function getCachedSchema(key: string, factory: () => z.ZodSchema) {
if (!userSchemaCache.has(key)) {
userSchemaCache.set(key, factory());
}
return userSchemaCache.get(key)!;
}
const lazyUserSchema = z.lazy(() => z.object({
profile: complexProfileSchema,
settings: complexSettingsSchema
}));
async function validateLargeArray(items: unknown[]) {
const errors: z.ZodError[] = [];
for (const item of items) {
const result = ItemSchema.safeParse(item);
if (!result.success) {
errors.push(result.error);
}
}
return errors;
}
Testing Schemas
import { describe, it, expect } from 'vitest';
describe('UserSchema', () => {
it('validates correct user data', () => {
const validUser = {
email: 'user@example.com',
name: 'John Doe',
age: 25
};
expect(() => UserSchema.parse(validUser)).not.toThrow();
});
it('rejects invalid email', () => {
const invalidUser = {
email: 'not-an-email',
name: 'John',
age: 25
};
const result = UserSchema.safeParse(invalidUser);
expect(result.success).toBe(false);
if (!result.success) {
expect(result.error.issues[0].path).toEqual(['email']);
}
});
it('applies transforms correctly', {
input = {
: ,
:
};
result = .(input);
(result.).();
(result.).();
});
});
Common Patterns
const ConditionalSchema = z.object({
type: z.enum(['personal', 'business']),
data: z.any()
}).transform((val) => {
if (val.type === 'personal') {
return {
type: val.type,
data: PersonalDataSchema.parse(val.data)
};
} else {
return {
type: val.type,
data: BusinessDataSchema.parse(val.data)
};
}
});
export const paginationSchema = z.object({
page: z.number().min(1).default(1),
limit: z.number().min(1).max(100).default(20),
sort: z.string().optional(),
: z.([, ]).()
});
filterSchema = z.({
: z.().(),
: z.([, , ]).(),
: z.().().(),
: z.().().()
});
apiResponseSchema = <T z.>
z.({
: z.(),
: dataSchema.(),
: z.().(),
: z.().()
});
userResponseSchema = ();
Migration from Yup/Joi
const yupSchema = yup.object({
email: yup.string().email().required(),
age: yup.number().min(18).required()
});
const zodSchema = z.object({
email: z.string().email(),
age: z.number().min(18)
});
const joiSchema = Joi.object({
email: Joi.string().email().required(),
age: Joi.number().min(18).required()
});
const zodSchema = z.object({
email: z.string().email(),
age: z.number().min(18)
});
Additional Resources