| name | zod-v4 |
| description | Zod 4 schema patterns and API differences from Zod 3. Activate when writing or reviewing Zod schemas, validation logic, or inferred types. Zod 4 has breaking changes from Zod 3. |
Zod v4 Patterns
This project uses Zod 4 (zod@^4.x). The API has meaningful breaking changes from Zod 3. Do not guess — use the patterns below.
Breaking Changes vs Zod 3
| Feature | Zod 3 | Zod 4 |
|---|
| Error map | z.ZodError .format() | Use .issues directly or z.prettifyError(err) |
.merge() | Available on object | Use .extend() instead |
z.string().email() | Built-in | Built-in (unchanged) |
z.object().partial() | .partial() | .partial() (unchanged) |
z.infer<> | z.infer<typeof schema> | Same (unchanged) |
z.input<> | z.input<typeof schema> | Same (unchanged) |
| Coercion | z.coerce.string() | Same (unchanged) |
| JSON type | Not built-in | z.json() — native JSON type |
| Template literals | Not available | z.templateLiteral() |
z.string().check() | Not available | Use .check() for custom refinements |
Core Patterns
Always export schema + inferred type together
import { z } from 'zod';
export const productSchema = z.object({
id: z.string().uuid(),
name: z.string().min(1).max(200),
price: z.number().positive(),
category: z.enum(['electronics', 'clothing', 'food']),
});
export type Product = z.infer<typeof productSchema>;
Prefer .extend() over .merge()
const extendedSchema = baseSchema.merge(z.object({ extra: z.string() }));
const extendedSchema = baseSchema.extend({ extra: z.string() });
Use z.prettifyError() for human-readable errors
const message = error.format();
import { z } from 'zod';
const result = schema.safeParse(input);
if (!result.success) {
console.error(z.prettifyError(result.error));
}
Use .safeParse() at system boundaries, .parse() internally
const result = schema.safeParse(rawInput);
if (!result.success) return { error: z.prettifyError(result.error) };
const data = result.data;
const data = schema.parse(knownValidInput);
Use z.json() for arbitrary JSON values
const jsonSchema: z.ZodType = z.union([z.string(), z.number(), z.boolean(), z.null(), z.array(z.lazy(() => jsonSchema)), z.record(z.lazy(() => jsonSchema))]);
const metadataSchema = z.object({
id: z.string(),
extra: z.json(),
});
Use z.templateLiteral() for string patterns
const routeSchema = z.templateLiteral(['/products/', z.string().uuid()]);
API response validation in service layer
Validate API responses at the service boundary, not in components.
import { z } from 'zod';
import { productSchema } from './schemas/productSchema';
import { http } from '@/lib/@http';
const productsResponseSchema = z.array(productSchema);
export const getProducts = async (): Promise<Product[]> => {
const raw = await http.get('products').json();
return productsResponseSchema.parse(raw);
};
Form schemas with .omit() / .pick()
const createProductSchema = productSchema.omit({ id: true });
export type CreateProductInput = z.infer<typeof createProductSchema>;
const updateProductSchema = productSchema.pick({ name: true, price: true }).partial();
export type UpdateProductInput = z.infer<typeof updateProductSchema>;
Refinements with .check() (Zod 4)
const passwordSchema = z.string()
.min(8)
.check((val, ctx) => {
if (!/[A-Z]/.test(val)) {
ctx.addIssue({ message: 'Must contain an uppercase letter' });
}
});
Naming Conventions (project-specific)
- File:
<name>Schema.ts (e.g., productSchema.ts)
- Export:
export const <name>Schema = z.object({...})
- Type:
export type <Name> = z.infer<typeof <name>Schema>
- Keep schema and type in the same file — co-location is required