What Changed in Zod 4: Performance, Mini, and Migration

Photo by Deutsches Spionagemuseum Berlin, CC BY-SA 4.0 via Wikimedia Commons (CC BY-SA 4.0)
No. Zod 4 ships inside the main zod package — you install it with npm install zod at version 4. The new lightweight build is a subpath import, zod/mini, from that same package. There is no separate @zod/four package to install.
Zod publishes benchmarks of roughly 14x faster string parsing, 7x faster array parsing, and 6.5x faster object parsing versus Zod 3. Just as important, it reports up to a 100x reduction in TypeScript type instantiations when chaining methods like .extend() and .omit(), which speeds up compilation and editor responsiveness on large schema files.
A single unified error parameter replaces required_error, invalid_type_error, and errorMap. You pass error as a string for the simple case, or as a function that receives the issue and can branch on whether the input was undefined. The old message field still works but is deprecated.
Use zod/mini when bundle size is a real constraint: browser bundles, serverless functions with cold-start budgets, and edge runtimes. It is about 1.9 KB gzipped and fully tree-shakable. On a Node backend, stick with core Zod — the bundle size is irrelevant there and the method-chaining API is more readable to maintain.
Yes, there is an official codemod named zod-v3-to-v4 that handles mechanical renames like migrating error parameters and moving string formats to top-level functions. It handles the bulk of the work, but you should still read every diff it produces, because codemods are reliable on routine changes and risky on unusual ones.

Photo by Deutsches Spionagemuseum Berlin, CC BY-SA 4.0 via Wikimedia Commons (CC BY-SA 4.0)
Key Takeaway
Zod 4 ships inside the main zod package and rewrites the validation core for speed: roughly 14x faster string parsing, 7x faster arrays, and a 100x cut in TypeScript instantiations on chained schemas. It adds a tree-shakable zod/mini build at about 1.9 KB gzipped, unifies error customization under one error parameter, and moves string formats to top-level functions like z.email().
I lean on Zod in almost every TypeScript service I run: it validates request bodies in NestJS, parses environment variables at boot, and guards the JSON I pull from third-party APIs. So when a major version lands and the headline is performance, I pay attention. Zod 4 is not a cosmetic bump. The core was rewritten, the type-level cost dropped hard, and a few APIs I used every day changed shape.
This is what actually changed, what it costs you to migrate, and where the new zod/mini build earns its place. Everything below is from hands-on upgrading, cross-checked against the official Zod 4 release notes and migration guide.
The runtime numbers Zod publishes are genuinely large: about 14x faster string parsing, 7x faster array parsing, and 6.5x faster object parsing versus Zod 3. Those come from flattening the internal check system and killing the old ZodEffects wrapper that used to sit around refinements and transforms. In a hot request path that validates every payload, that matters.
But the change I felt most was the type-level one. Zod reports up to a 100x reduction in tsc type instantiations when you chain methods like .extend() and .omit(). On a large schema file, that is the difference between the editor keeping up and the editor stalling. If you have ever watched TypeScript grind on a deeply composed Zod schema, this alone can justify the upgrade.
Before you migrate, run your existing test suite once on Zod 3 and note the wall-clock time. Repeat after upgrading. The runtime win is easy to see in parse-heavy tests, and it gives you a concrete number to put in the pull request description instead of quoting a benchmark you did not run.
This is the breaking change you will hit most. Zod 3 had three overlapping ways to customize messages: required_error, invalid_type_error, and errorMap, plus a plain message on refinements. Zod 4 collapses all of them into a single error parameter. It accepts a string for the simple case or a function that receives the issue for conditional logic. The old message field still works but is deprecated.
// Zod 3 — three different knobs
const schema3 = z.string({
required_error: "Name is required",
invalid_type_error: "Name must be a string",
});
// Zod 4 — one unified `error`
const schema4 = z.string({
error: (issue) =>
issue.input === undefined
? "Name is required"
: "Name must be a string",
});
// Simple string form still works everywhere:
z.string().min(5, { error: "Too short" });There is also a global error map so you can localize messages in one place, which is genuinely useful when you serve both English and Indonesian users. The mental model is cleaner: there is exactly one place to set an error, and it is the same in every position.
In Zod 3 you wrote z.string().email(). In Zod 4 the format validators are top-level functions: z.email(), z.uuid(), z.url(), z.ipv4(), z.ipv6(). The chained method forms still exist but are deprecated, and they carry a bundle-size cost because they force the whole string-format machinery into your build. The top-level functions are tree-shakable, so you only ship the validators you import.
// Deprecated (still works)
z.string().email();
z.string().uuid();
// Preferred in Zod 4
z.email();
z.uuid(); // stricter, RFC 9562
z.guid(); // looser GUID matching if you need it
z.url();z.uuid() is stricter in Zod 4 — it validates against RFC 9562 and will reject some strings that Zod 3 accepted, including certain non-standard UUID-like values. If you validate IDs coming from an older system, test with real data before you ship, or use z.guid() for the looser match.
There is an official codemod, zod-v3-to-v4, that handles the mechanical renames like the error-param migration and the string-format moves. I still read every diff it produces — codemods are great at the boring 90 percent and dangerous on the interesting 10 percent — but it saved me real time on a large codebase.
Zod 4 introduces a second entry point, zod/mini, with a functional and fully tree-shakable API. It lands at about 1.88 KB gzipped, roughly 85 percent smaller than the core build. Instead of chaining methods you compose functions, which is what makes it shake so well: unused validators never reach your bundle.
// Core Zod — method chaining
import * as z from "zod";
const User = z.object({
email: z.email(),
age: z.number().min(18),
});
// zod/mini — function composition, tree-shakable
import * as z from "zod/mini";
const UserMini = z.object({
email: z.email(),
age: z.number().check(z.minimum(18)),
});I would not reach for zod/mini on the backend. On a Node service the bundle size is irrelevant and the chaining ergonomics of core Zod are nicer to read and maintain. Where mini earns its place is the edge: serverless functions with cold-start budgets, browser bundles where every kilobyte is a real metric, and edge runtimes. The APIs share the same core, so you can use core on the server and mini on the client without learning two validation libraries.
| Concern | Core zod | zod/mini |
|---|---|---|
| Import | import * as z from "zod" | import * as z from "zod/mini" |
| Bundle (gzipped) | Larger, chaining API | ~1.9 KB, tree-shakable |
| Ergonomics | Method chaining, most readable | Function composition |
| Best fit | Node backends, servers | Edge, serverless, browser |
For most projects the migration is an afternoon, not a week. The error-param change touches many files but is mechanical, the codemod does the bulk of it, and TypeScript refuses to let you finish while anything is still broken. The payoff — faster parsing, dramatically cheaper type checking, and a real edge-sized build — is worth the diff.