name: zod-v4 description: Zod v4 coding guidelines and migration reference. ALWAYS read this when using Zod validation library
Zod v4 Guidelines
Type Inference
Every schema MUST have inferred type above it:
export type User = z.infer<typeof User>
export const User = z.object({...})
Requirements:
- ALWAYS place type above schema
- ALWAYS use same name for type & schema
- NEVER use "Schema" suffix
- ALWAYS use JSDoc (
/** */), never// - NO EXCEPTIONS - even for internal/helper schemas
String Validation
String validations are standalone functions:
// WRONG: z.string().email()
// RIGHT: z.email(), z.url(), z.uuid(), z.ip()
Error Messages
Use error param sparingly - Zod's defaults are excellent:
// WRONG: z.email({error: "Invalid email"}) // Redundant!
// RIGHT: z.email() // Zod says "Invalid email"
// RIGHT: Only for business logic:
z.string().refine((val) => /[A-Z]/.test(val), {
error: 'Must contain uppercase',
})
Number Types
- Use
z.number()for general numbers z.int()for integers only (notz.number().int())z.int32(),z.float64()for specific types- Numbers finite by default
Object Types
z.object()- strips unknowns (default)z.strictObject()- rejects extrasz.looseObject()- allows extras
Custom Validation
Use .check() for advanced validation, .refine() for simple validation:
Migration steps:
.superRefine()→.check()for advanced validation with multiple issues- In
.check():val→ctx.value,ctx.addIssue()→ctx.issues.push() z.ZodIssueCode.custom→'custom'string- Add
input: ctx.valueto issue object - Use
.refine()for simple boolean validation with single error
Error Formatting
z.prettifyError()- Human-readable formatz.treeifyError()- Tree structure format
Functions
Define function schemas with input/output types:
z.function({
input: [z.string()],
output: z.number(),
})
Records
z.record(keyType, valueType)
ISO Formats
z.iso.datetime()- ISO 8601 datetimez.iso.date()- ISO 8601 date
Additional Features
- Default:
.default()applies to output; use.prefault()for v3 behavior - File validation:
z.file().min(1024).max(5*1024*1024).mime(['image/jpeg']) - Pipe:
z.pipe(z.string(), z.number())for transformations - Async: Use
.check(async (val) => {...})for async validation - Arrays:
z.array(z.email())orz.email().array() - Optional:
.optional(),.nullable(),.nullish()
Quick Reference: v3 → v4
| v3 | v4 |
|---|---|
| z.string().email() | z.email() |
| {message: "err"} | {error: "err"} |
| .strict() | z.strictObject() |
| .format() | z.treeifyError() |
| z.string().datetime() | z.iso.datetime() |
| .args().returns() | {input:[...], output:...} |
| .superRefine() | .check() |
| ctx.addIssue() | ctx.issues.push() |
| z.ZodIssueCode.custom | 'custom' |
Complete Example
import * as z from 'zod'
/** User registration */
export type UserReg = z.infer<typeof UserReg>
export const UserReg = z.object({
email: z.email(),
password: z
.string()
.min(8)
.refine((pwd) => /[A-Z]/.test(pwd) && /\d/.test(pwd), { error: 'Need uppercase & number' }),
age: z.number().min(18),
})
/** Function with input/output types */
export type ProcessUser = z.infer<typeof ProcessUser>
export const ProcessUser = z.function({
input: [UserReg],
output: z.object({
id: z.string(),
createdAt: z.iso.datetime(),
}),
})