zod

TypeScript-first schema validation with automatic static type inference from runtime schemas. Define schemas once and automatically infer TypeScript types for compile-time safety; supports primitives, objects, arrays, unions, and recursive types Validate data with parse() (throws) or safeParse() (returns result object); includes async validation for database lookups and external checks Transform and refine data during validation using transform() , refine() , and superRefine() for custom logic and cross-field validation Integrates seamlessly with React Hook Form, tRPC, Next.js, Express, Drizzle ORM, and environment variable validation Compose schemas by extending, merging, picking, omitting, and creating discriminated unions; customize error messages globally or per field

INSTALLATION
npx skills add https://github.com/bobmatnyc/claude-mpm-skills --skill zod
Run in your project or agent environment. Adjust flags if your CLI version differs.

SKILL.md

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';

// Define schema

const UserSchema = z.object({

id: z.string().uuid(),

email: z.string().email(),

age: z.number().min(18),

role: z.enum(['user', 'admin'])

});

// Infer TypeScript type

type User = z.infer;

// Validate data

const result = UserSchema.safeParse(data);

if (result.success) {

const user: User = result.data;

}

<!-- SECTION: primitives -->

## Primitive Types

### Basic Types

import { z } from 'zod';

// String with validation

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}$/);

// Numbers

const ageSchema = z.number()

.int("Must be integer")

.positive()

.min(0)

.max(120);

const priceSchema = z.number()

.positive()

.multipleOf(0.01); // Currency precision

// Boolean

const isActiveSchema = z.boolean();

// Date

const createdAtSchema = z.date()

.min(new Date('2020-01-01'))

.max(new Date());

const dateStringSchema = z.string().datetime(); // ISO 8601

const dateOnlySchema = z.string().date(); // YYYY-MM-DD


### Special Types

// Literal values

const roleSchema = z.literal('admin');

const statusSchema = z.literal('pending');

// Enums

const ColorEnum = z.enum(['red', 'green', 'blue']);

type Color = z.infer<typeof ColorEnum>; // 'red' | 'green' | 'blue'

const NativeEnum = z.nativeEnum(MyEnum); // For TypeScript enums

// Nullable and Optional

const optionalString = z.string().optional(); // string | undefined

const nullableString = z.string().nullable(); // string | null

const nullishString = z.string().nullish(); // string | null | undefined

// Default values

const countSchema = z.number().default(0);

const settingsSchema = z.object({

theme: z.string().default('light'),

notifications: z.boolean().default(true)

});


## Objects and Arrays

### Object Schemas

// Basic object

const UserSchema = z.object({

id: z.string(),

email: z.string().email(),

name: z.string(),

age: z.number().optional()

});

// Nested objects

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()

})

});

// Strict vs Passthrough

const strictSchema = z.object({ name: z.string() }).strict();

// Rejects unknown keys

const passthroughSchema = z.object({ name: z.string() }).passthrough();

// Allows unknown keys

const stripSchema = z.object({ name: z.string() }).strip();

// Removes unknown keys (default)


### Array Schemas

// Simple arrays

const stringArray = z.array(z.string());

const numberArray = z.array(z.number()).min(1).max(10);

// Array of objects

const UsersSchema = z.array(UserSchema);

// Non-empty arrays

const tagSchema = z.array(z.string()).nonempty("At least one tag required");

// Fixed-length arrays (tuples)

const coordinateSchema = z.tuple([z.number(), z.number()]);

type Coordinate = z.infer<typeof coordinateSchema>; // [number, number]

// Tuple with rest

const csvRowSchema = z.tuple([z.string(), z.number()]).rest(z.string());

// [string, number, ...string[]]


### Records and Maps

// Record (object with dynamic keys)

const userRolesSchema = z.record(

z.string(), // key type

z.enum(['admin', 'user', 'guest']) // value type

);

type UserRoles = z.infer<typeof userRolesSchema>;

// { [key: string]: 'admin' | 'user' | 'guest' }

// Map

const configMapSchema = z.map(

z.string(), // key

z.number() // value

);

// Set

const uniqueTagsSchema = z.set(z.string());


## Type Inference

import { z } from 'zod';

// Infer output type

const UserSchema = z.object({

id: z.string(),

email: z.string().email(),

age: z.number()

});

type User = z.infer<typeof UserSchema>;

// { id: string; email: string; age: number }

// Infer input type (before transforms)

const TransformSchema = z.object({

date: z.string().transform(s => new Date(s))

});

type Input = z.input<typeof TransformSchema>;

// { date: string }

type Output = z.output<typeof TransformSchema>;

// { date: Date }

// Using inferred types in functions

function createUser(data: User): void {

// data is type-safe

}

function validateAndCreate(data: unknown): User | null {

const result = UserSchema.safeParse(data);

return result.success ? result.data : null;

}


## Validation Methods

### Parse vs SafeParse

// parse() - Throws on failure

try {

const user = UserSchema.parse(data);

// user is type User

} catch (error) {

if (error instanceof z.ZodError) {

console.error(error.issues);

}

}

// safeParse() - Returns result object

const result = UserSchema.safeParse(data);

if (result.success) {

const user = result.data; // type User

} else {

const errors = result.error.issues;

errors.forEach(err => {

console.log(${err.path}: ${err.message});

});

}

// parseAsync() - For async refinements

const asyncResult = await UserSchema.parseAsync(data);

// safeParseAsync() - Safe async version

const asyncSafeResult = await UserSchema.safeParseAsync(data);


### Partial Validation

// Check if data matches schema without throwing

const isValid = UserSchema.safeParse(data).success;

// Custom type guards

function isUser(data: unknown): data is User {

return UserSchema.safeParse(data).success;

}

if (isUser(unknownData)) {

// TypeScript knows unknownData is User

console.log(unknownData.email);

}


## Schema Composition

### Extending and Merging

// Extend (add fields)

const BaseUserSchema = z.object({

id: z.string(),

email: z.string()

});

const AdminUserSchema = BaseUserSchema.extend({

role: z.literal('admin'),

permissions: z.array(z.string())

});

// Merge (combine schemas)

const NameSchema = z.object({ name: z.string() });

const AgeSchema = z.object({ age: z.number() });

const PersonSchema = NameSchema.merge(AgeSchema);

// { name: string; age: number }

// Pick (select fields)

const UserIdEmail = UserSchema.pick({ id: true, email: true });

// Omit (exclude fields)

const UserWithoutId = UserSchema.omit({ id: true });

// Partial (make all fields optional)

const PartialUser = UserSchema.partial();

// DeepPartial (recursive partial)

const DeepPartialUser = UserSchema.deepPartial();

// Required (make all fields required)

const RequiredUser = UserSchema.required();


### Union and Intersection

// Union (OR)

const StringOrNumber = z.union([z.string(), z.number()]);

// Shorthand

const StringOrNumberAlt = z.string().or(z.number());

// Discriminated Union (tagged union)

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

]);

// Intersection (AND)

const User = z.object({ name: z.string() });

const Timestamps = z.object({

createdAt: z.date(),

updatedAt: z.date()

});

const UserWithTimestamps = z.intersection(User, Timestamps);

// Shorthand

const UserWithTimestampsAlt = User.and(Timestamps);


## Transformations and Refinements

### Transform

// Transform data after validation

const StringToNumber = z.string().transform(val => parseInt(val, 10));

const DateSchema = z.string().transform(str => new Date(str));

// Chaining transforms

const TrimmedLowercase = z.string()

.transform(s => s.trim())

.transform(s => s.toLowerCase());

// Transform with validation

const PositiveStringNumber = z.string()

.transform(val => parseInt(val, 10))

.refine(n => n > 0, "Must be positive");

// Complex transformations

const UserInputSchema = z.object({

name: z.string().transform(s => s.trim()),

email: z.string().email().transform(s => s.toLowerCase()),

birthDate: z.string().transform(s => new Date(s)),

tags: z.string().transform(s => s.split(',').map(t => t.trim()))

});

type UserInput = z.input<typeof UserInputSchema>;

// { name: string; email: string; birthDate: string; tags: string }

type User = z.output<typeof UserInputSchema>;

// { name: string; email: string; birthDate: Date; tags: string[] }


### Refine (Custom Validation)

// Simple refinement

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"

);

// Refinement with custom error

const UniqueEmailSchema = z.string().email().refine(

async (email) => {

const exists = await checkEmailExists(email);

return !exists;

},

{ message: "Email already taken" }

);

// Object-level refinement

const PasswordMatchSchema = z.object({

password: z.string(),

confirmPassword: z.string()

}).refine(

data => data.password === data.confirmPassword,

{

message: "Passwords don't match",

path: ["confirmPassword"] // Error location

}

);

// Multiple field validation

const DateRangeSchema = z.object({

startDate: z.date(),

endDate: z.date()

}).refine(

data => data.endDate > data.startDate,

{

message: "End date must be after start date",

path: ["endDate"]

}

);


### SuperRefine (Advanced)

// Access to Zod context for complex validation

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"]

});

}

}

});

// Multiple issues

const RegistrationSchema = z.object({

username: z.string(),

email: z.string(),

age: z.number()

}).superRefine(async (data, ctx) => {

// Check username availability

if (await usernameTaken(data.username)) {

ctx.addIssue({

code: z.ZodIssueCode.custom,

message: "Username taken",

path: ["username"]

});

}

// Check email availability

if (await emailTaken(data.email)) {

ctx.addIssue({

code: z.ZodIssueCode.custom,

message: "Email already registered",

path: ["email"]

});

}

// Age restriction

if (data.age < 18) {

ctx.addIssue({

code: z.ZodIssueCode.custom,

message: "Must be 18 or older",

path: ["age"]

});

}

});


## Error Handling

### Custom Error Messages

// Field-level 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" })

});

// Global error map

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.defaultError };

};

z.setErrorMap(customErrorMap);


### Processing Errors

// Flatten errors for forms

const result = UserSchema.safeParse(data);

if (!result.success) {

const flatErrors = result.error.flatten();

console.log(flatErrors.formErrors); // Top-level errors

console.log(flatErrors.fieldErrors);

// { email: ["Invalid email"], age: ["Must be 18+"] }

}

// Format for API response

function formatZodError(error: z.ZodError) {

return error.issues.map(issue => ({

field: issue.path.join('.'),

message: issue.message

}));

}

// Example usage

const result = UserSchema.safeParse(data);

if (!result.success) {

return res.status(400).json({

errors: formatZodError(result.error)

});

}


## Async Validation

import { z } from 'zod';

// Async refinement

const UsernameSchema = z.string().refine(

async (username) => {

const available = await checkUsernameAvailable(username);

return available;

},

{ message: "Username already taken" }

);

// Must use parseAsync or safeParseAsync

const result = await UsernameSchema.safeParseAsync("john_doe");

// Complex async validation

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"

)

});

// Validate

const userData = await RegistrationSchema.parseAsync(input);

// With error handling

const result = await RegistrationSchema.safeParseAsync(input);

if (!result.success) {

// Handle validation errors

}


## Advanced Types

### Recursive Types

// Self-referential schemas

type Category = {

name: string;

subcategories: Category[];

};

const CategorySchema: z.ZodType<Category> = z.lazy(() =>

z.object({

name: z.string(),

subcategories: z.array(CategorySchema)

})

);

// Tree structure

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

// Type-safe union based on discriminator field

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>;

// TypeScript can narrow based on discriminator

function calculateArea(shape: Shape): number {

switch (shape.kind) {

case 'circle':

return Math.PI shape.radius * 2;

case 'rectangle':

return shape.width * shape.height;

case 'triangle':

return (shape.base * shape.height) / 2;

}

}


### Preprocess

// Transform before validation

const NumberFromString = z.preprocess(

(val) => (typeof val === 'string' ? parseInt(val, 10) : val),

z.number()

);

// Clean data before validation

const TrimmedString = z.preprocess(

(val) => (typeof val === 'string' ? val.trim() : val),

z.string()

);

// Parse JSON strings

const JsonSchema = z.preprocess(

(val) => (typeof val === 'string' ? JSON.parse(val) : val),

z.object({

name: z.string(),

age: z.number()

})

);

// Form data preprocessing

const FormDataSchema = z.preprocess(

(data) => {

// Convert FormData to object

if (data instanceof FormData) {

return Object.fromEntries(data.entries());

}

return data;

},

z.object({

name: z.string(),

email: z.string().email()

})

);


### Branded Types

// Create nominal 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>;

// Prevents mixing similar types

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); // ✗ Type error


## 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) => {

// data is validated and typed

console.log(data);

};

return (

<form onSubmit={handleSubmit(onSubmit)}>

<input {...register('username')} />

{errors.username &#x26;&#x26; <span>{errors.username.message}</span>}

<input {...register('email')} />

{errors.email &#x26;&#x26; <span>{errors.email.message}</span>}

<input type="number" {...register('age', { valueAsNumber: true })} />

{errors.age &#x26;&#x26; <span>{errors.age.message}</span>}

<button type="submit">Submit</button>

</form>

);

}


### tRPC

import { z } from 'zod';

import { initTRPC } from '@trpc/server';

const t = initTRPC.create();

const router = t.router;

const publicProcedure = t.procedure;

// Input/output validation

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; // Type-checked against output schema

}),

createUser: publicProcedure

.input(z.object({

name: z.string().min(2),

email: z.string().email(),

age: z.number().min(18)

}))

.mutation(async ({ input }) => {

return await db.user.create({ data: input });

})

});

export type AppRouter = typeof appRouter;


### Next.js API Routes

// app/api/users/route.ts

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);

// validatedData is typed and validated

const user = await createUser(validatedData);

return NextResponse.json(user, { status: 201 });

} catch (error) {

if (error instanceof z.ZodError) {

return NextResponse.json(

{ errors: error.flatten().fieldErrors },

{ status: 400 }

);

}

return NextResponse.json(

{ error: 'Internal server error' },

{ status: 500 }

);

}

}

// Query parameter validation

const SearchParamsSchema = z.object({

page: z.string().transform(Number).pipe(z.number().min(1)).default('1'),

limit: z.string().transform(Number).pipe(z.number().max(100)).default('10'),

sort: z.enum(['asc', 'desc']).default('asc')

});

export async function GET(request: NextRequest) {

const searchParams = Object.fromEntries(

request.nextUrl.searchParams.entries()

);

const params = SearchParamsSchema.parse(searchParams);

// params is { page: number, limit: number, sort: 'asc' | 'desc' }

const users = await getUsers(params);

return NextResponse.json(users);

}


### Express Middleware

import express from 'express';

import { z } from 'zod';

// Validation middleware

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(CreateUserSchema), async (req, res) => {

// req.body is validated (not typed in Express)

const user = await createUser(req.body);

res.json(user);

});

// Validate params, query, body

const validateRequest = (schema: {

params?: z.ZodSchema;

query?: z.ZodSchema;

body?: z.ZodSchema;

}) => {

return (req: express.Request, res: express.Response, next: express.NextFunction) => {

try {

if (schema.params) {

req.params = schema.params.parse(req.params);

}

if (schema.query) {

req.query = schema.query.parse(req.query);

}

if (schema.body) {

req.body = schema.body.parse(req.body);

}

next();

} catch (error) {

if (error instanceof z.ZodError) {

return res.status(400).json({ errors: error.issues });

}

next(error);

}

};

};

app.get(

'/users/:id',

validateRequest({

params: z.object({ id: z.string().uuid() }),

query: z.object({ include: z.string().optional() })

}),

async (req, res) => {

// Validated params and query

}

);


### Drizzle ORM

import { z } from 'zod';

import { pgTable, serial, text, integer } from 'drizzle-orm/pg-core';

import { createInsertSchema, createSelectSchema } from 'drizzle-zod';

// Define table

export const users = pgTable('users', {

id: serial('id').primaryKey(),

name: text('name').notNull(),

email: text('email').notNull().unique(),

age: integer('age')

});

// Auto-generate schemas

export const insertUserSchema = createInsertSchema(users);

export const selectUserSchema = createSelectSchema(users);

// Customize validation

export const customInsertUserSchema = createInsertSchema(users, {

email: z.string().email(),

age: z.number().min(18).optional()

});

// Use in application

type NewUser = z.infer<typeof insertUserSchema>;

type User = z.infer<typeof selectUserSchema>;

function createUser(data: unknown) {

const validatedData = insertUserSchema.parse(data);

return db.insert(users).values(validatedData);

}


### Environment Variables

// env.ts

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')

});

// Validate on startup

export const env = envSchema.parse(process.env);

// Type-safe environment variables

export type Env = z.infer<typeof envSchema>;

// Usage

console.log(Server running on port ${env.PORT});

// env.PORT is number, not string


## Best Practices

### Schema Organization

// schemas/user.schema.ts

import { z } from 'zod';

// Reusable primitives

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");

// Base schemas

export const baseUserSchema = z.object({

id: uuidSchema,

email: emailSchema,

name: z.string().min(2)

});

// Extended schemas

export const createUserSchema = baseUserSchema.omit({ id: true }).extend({

password: passwordSchema,

confirmPassword: z.string()

}).refine(

data => data.password === data.confirmPassword,

{ message: "Passwords must match", path: ["confirmPassword"] }

);

export const updateUserSchema = baseUserSchema.partial().omit({ id: true });

// Export types

export type User = z.infer<typeof baseUserSchema>;

export type CreateUser = z.infer<typeof createUserSchema>;

export type UpdateUser = z.infer<typeof updateUserSchema>;


### Performance Optimization

// Cache parsed schemas

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)!;

}

// Lazy validation for large objects

const lazyUserSchema = z.lazy(() => z.object({

// Only validated when accessed

profile: complexProfileSchema,

settings: complexSettingsSchema

}));

// Streaming validation for arrays

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', () => {

const input = {

name: ' JOHN DOE ',

email: 'USER@EXAMPLE.COM'

};

const result = UserSchema.parse(input);

expect(result.name).toBe('john doe');

expect(result.email).toBe('user@example.com');

});

});


### Common Patterns

// Conditional validation

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)

};

}

});

// Pagination schema

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(),

order: z.enum(['asc', 'desc']).default('asc')

});

// Filter schema

export const filterSchema = z.object({

search: z.string().optional(),

status: z.enum(['active', 'inactive', 'pending']).optional(),

dateFrom: z.string().datetime().optional(),

dateTo: z.string().datetime().optional()

});

// API response wrapper

export const apiResponseSchema = <T extends z.ZodTypeAny>(dataSchema: T) =>

z.object({

success: z.boolean(),

data: dataSchema.optional(),

error: z.string().optional(),

timestamp: z.string().datetime()

});

const userResponseSchema = apiResponseSchema(UserSchema);


### Migration from Yup/Joi

// Yup -> Zod

// Yup

const yupSchema = yup.object({

email: yup.string().email().required(),

age: yup.number().min(18).required()

});

// Zod equivalent

const zodSchema = z.object({

email: z.string().email(),

age: z.number().min(18)

});

// Joi -> Zod

// Joi

const joiSchema = Joi.object({

email: Joi.string().email().required(),

age: Joi.number().min(18).required()

});

// Zod equivalent (same as above)

const zodSchema = z.object({

email: z.string().email(),

age: z.number().min(18)

});

// Key differences:

// 1. Zod fields are required by default

// 2. Zod has first-class TypeScript integration

// 3. Zod schemas are immutable

// 4. Zod has better tree-shaking

BrowserAct

Let your agent run on any real-world website

Bypass CAPTCHA & anti-bot for free. Start local, scale to cloud.

Explore BrowserAct Skills →

Stop writing automation&scrapers

Install the CLI. Run your first Skill in 30 seconds. Scale when you're ready.

Start free
free · no credit card