What Changed in Zod v4?
Zod v4 is a ground-up rewrite of the parser engine. The API is largely backward compatible, but the internals are completely different — resulting in dramatically better performance, a smaller bundle, and new schema types that were requested for years.
Installation
pnpm add zod@^4.0.0
For migration from v3, Zod v4 ships a compatibility layer:
// Still works with v4 — gradual migration path
import { z } from "zod/v3-compat";
Performance: 100x Faster
The v4 parser uses a direct evaluation strategy instead of the recursive descent used in v3. On a schema with 50+ fields:
| Benchmark | Zod v3 | Zod v4 | Speedup | |-----------|--------|--------|---------| | Parse 1000 objects | 480ms | 4.8ms | 100x | | Large nested schema | 210ms | 2.1ms | 100x | | Simple string parse | 0.8μs | 0.1μs | 8x |
This matters in hot paths — API route validation, form submission handlers, and stream processors that validate every event.
Bundle Size
| Package | v3 | v4 | Change |
|---------|-----|-----|--------|
| zod (core) | 14.2KB | 6.1KB | -57% |
| zod/mini | N/A | 2.4KB | new |
zod/mini is a tree-shakeable build with the same API — use it when bundle size is critical (browser bundles, edge functions):
import { z } from "zod/mini";
const schema = z.object({ name: z.string(), age: z.number() });
New: z.interface()
For performance-critical paths, z.interface() is a stricter variant of z.object() that skips the "strip unknown keys" step (which requires iterating all keys):
// z.object() — strips unknown keys (slower)
const UserSchema = z.object({ name: z.string(), email: z.string().email() });
// z.interface() — does not strip unknown keys (2x faster parse)
const UserInterface = z.interface({ name: z.string(), email: z.string().email() });
Use z.interface() when you control the input shape (internal APIs, server-to-server calls).
New: z.file()
Validate File objects — previously required custom refinements:
const FileSchema = z.file({
maxSize: 5 * 1024 * 1024, // 5MB
mimeType: ["image/jpeg", "image/png", "image/webp"],
});
// In a form handler
const result = FileSchema.safeParse(formData.get("avatar"));
if (!result.success) return { error: "Invalid file" };
New: z.templateLiteral()
Type-safe string pattern matching:
// Match "user-{id}" where id is a number
const UserIdSchema = z.templateLiteral(["user-", z.number()]);
// Inferred type: `user-${number}`
UserIdSchema.parse("user-123"); // ✓
UserIdSchema.parse("user-abc"); // ✗ Error
// More complex patterns
const RouteSchema = z.templateLiteral(["/api/", z.enum(["users", "posts"]), "/", z.string()]);
// Inferred: "/api/users/${string}" | "/api/posts/${string}"
Improved Error Formatting
v4's error format is cleaner and more actionable:
const result = z.object({
name: z.string(),
age: z.number().min(0),
}).safeParse({ name: 123, age: -1 });
if (!result.success) {
console.log(result.error.format());
// {
// name: { _errors: ["Expected string, received number"] },
// age: { _errors: ["Number must be greater than or equal to 0"] }
// }
}
Migration From v3
Most v3 code runs unchanged in v4. The main differences:
z.string().nonempty()→ usez.string().min(1)(nonempty() removed).transform()return types are now stricter — some transforms need explicit type annotationZodError.flatten()output structure changed slightly.default()now only applies when the value isundefined, notnull
Run the v4 codemod:
npx zod-codemod v4
References: Zod · GitHub · v4 announcement