--- name: zod-4 description: > Zod 4 schema validation patterns. Trigger: When using Zod for validation - breaking changes from v3. license: Apache-2.0 metadata: version: '1.0' --- ## Breaking Changes from Zod 3 ```typescript // ❌ Zod 3 (OLD) z.string().email(); z.string().uuid(); z.string().url(); z.string().nonempty(); z.object({ name: z.string() }).required_error('Required'); // ✅ Zod 4 (NEW) z.email(); z.uuid(); z.url(); z.string().min(1); z.object({ name: z.string() }, { error: 'Required' }); ``` ## Basic Schemas ```typescript import { z } from 'zod'; // Primitives const stringSchema = z.string(); const numberSchema = z.number(); const booleanSchema = z.boolean(); const dateSchema = z.date(); // Top-level validators (Zod 4) const emailSchema = z.email(); const uuidSchema = z.uuid(); const urlSchema = z.url(); // With constraints const nameSchema = z.string().min(1).max(100); const ageSchema = z.number().int().positive().max(150); const priceSchema = z.number().min(0).multipleOf(0.01); ``` ## Object Schemas ```typescript const userSchema = z.object({ id: z.uuid(), email: z.email({ error: 'Invalid email address' }), name: z.string().min(1, { error: 'Name is required' }), age: z.number().int().positive().optional(), role: z.enum(['admin', 'user', 'guest']), metadata: z.record(z.string(), z.unknown()).optional() }); type User = z.infer; // Parsing const user = userSchema.parse(data); // Throws on error const result = userSchema.safeParse(data); // Returns { success, data/error } if (result.success) { console.log(result.data); } else { console.log(result.error.issues); } ``` ## Arrays and Records ```typescript // Arrays const tagsSchema = z.array(z.string()).min(1).max(10); const numbersSchema = z.array(z.number()).nonempty(); // Records (objects with dynamic keys) const scoresSchema = z.record(z.string(), z.number()); // { [key: string]: number } // Tuples const coordinatesSchema = z.tuple([z.number(), z.number()]); // [number, number] ``` ## Unions and Discriminated Unions ```typescript // Simple union const stringOrNumber = z.union([z.string(), z.number()]); // Discriminated union (more efficient) const resultSchema = z.discriminatedUnion('status', [ z.object({ status: z.literal('success'), data: z.unknown() }), z.object({ status: z.literal('error'), error: z.string() }) ]); ``` ## Transformations ```typescript // Transform during parsing const lowercaseEmail = z.email().transform(email => email.toLowerCase()); // Coercion (convert types) const numberFromString = z.coerce.number(); // "42" → 42 const dateFromString = z.coerce.date(); // "2024-01-01" → Date // Preprocessing const trimmedString = z.preprocess( val => (typeof val === 'string' ? val.trim() : val), z.string() ); ``` ## Refinements ```typescript const passwordSchema = 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' }); // With superRefine for multiple errors const formSchema = z .object({ password: z.string(), confirmPassword: z.string() }) .superRefine((data, ctx) => { if (data.password !== data.confirmPassword) { ctx.addIssue({ code: z.ZodIssueCode.custom, message: "Passwords don't match", path: ['confirmPassword'] }); } }); ``` ## Optional and Nullable ```typescript // Optional (T | undefined) z.string().optional(); // Nullable (T | null) z.string().nullable(); // Both (T | null | undefined) z.string().nullish(); // Default values z.string().default('unknown'); z.number().default(() => Math.random()); ``` ## Error Handling ```typescript // Zod 4: Use 'error' param instead of 'message' const schema = z.object({ name: z.string({ error: 'Name must be a string' }), email: z.email({ error: 'Invalid email format' }), age: z.number().min(18, { error: 'Must be 18 or older' }) }); // Custom error map const customSchema = z.string({ error: issue => { if (issue.code === 'too_small') { return 'String is too short'; } return 'Invalid string'; } }); ``` ## React Hook Form Integration ```typescript import { useForm } from "react-hook-form"; import { zodResolver } from "@hookform/resolvers/zod"; const schema = z.object({ email: z.email(), password: z.string().min(8), }); type FormData = z.infer; function Form() { const { register, handleSubmit, formState: { errors } } = useForm({ resolver: zodResolver(schema), }); return (
{errors.email && {errors.email.message}}
); } ``` ## Keywords zod, validation, schema, typescript, forms, parsing