Headline: Zod 4 is a rewrite of the TypeScript-first schema validation library, released as the stable major in 2025. Four changes hit my code directly: string formats moved to top-level functions (
z.email()instead ofz.string().email()), the four error options collapsed into oneerrorparameter, error formatting moved to standalone helpers (z.flattenError,z.treeifyError,z.prettifyError), and.strict()/.passthrough()becamez.strictObject()/z.looseObject(). The deprecated Zod 3 APIs still work with warnings, so I migrated incrementally.
Key takeaways
- Zod 4 is the stable major of the TypeScript-first schema validator, released in 2025; it requires TypeScript 5.5 or newer.
- String formats are now top-level tree-shakeable functions —
z.email(),z.uuid(),z.url()— andz.string().email()is deprecated but still works. - A single
errorparameter replaces Zod 3'smessage,invalid_type_error,required_error, anderrorMap. - Error formatting moved to
z.flattenError(form fields),z.treeifyError(nested), andz.prettifyError(human-readable string). - The
zod/minibuild exposes the same validators through a functional, tree-shakeable API;z.infer,.parse(), and.safeParse()did not change.
I reach for Zod on almost every project to validate untrusted input at the boundary — request bodies, form data, environment variables, API responses. Zod 4 changed enough of the surface that a mechanical upgrade tripped a handful of files, so I mapped exactly what moved.
What actually changed in Zod 4?
Zod 4 is a ground-up rewrite of the TypeScript-first schema validation library, released as the stable major in 2025. The headline is performance: the Zod team's release notes report large reductions in TypeScript compiler instantiations and faster runtime parsing, which matters most in large codebases where schema types dominate type-check time. Four API changes touched my code directly — string formats moved to top-level functions, the four error options collapsed into one, error formatting moved to standalone helpers, and .strict()/.passthrough() became z.strictObject()/z.looseObject().
Why did z.string().email() become z.email()?
Zod 4 promotes string formats to standalone top-level functions — z.email(), z.uuid(), z.url(), and the ISO helpers under z.iso — instead of methods chained onto z.string(). The chained form still works but is deprecated and warns. The reason is tree-shaking: each format is its own function, so a bundle that only validates emails no longer ships the logic for every other format.
import * as z from "zod";
const User = z.object({
id: z.uuid(),
email: z.email(),
website: z.url().optional(),
age: z.number().int().min(18),
});
type User = z.infer<typeof User>;
Enter fullscreen mode Exit fullscreen mode
How do I set custom error messages in Zod 4?
Zod 4 replaces four separate error options with a single error parameter. In Zod 3 you passed message, invalid_type_error, required_error, or a full errorMap. In Zod 4 you pass one error: a string for a fixed message, or a function that receives the issue and returns a message, letting you distinguish a missing value from a wrong type in one place.
// Zod 4: one error param — string or function
z.string({ error: "Name is required" });
z.string({
error: (issue) =>
issue.input === undefined ? "Name is required" : "Name must be text",
});
Enter fullscreen mode Exit fullscreen mode
How do I turn a ZodError into form errors?
Zod 4 moves error formatting into three top-level helpers. z.flattenError(error) returns { formErrors, fieldErrors }, which maps onto a form's field-level messages. z.treeifyError(error) returns a nested object mirroring the schema shape. z.prettifyError(error) returns a human-readable multiline string for logs. The raw error.issues array is unchanged; error.format() and error.flatten() are deprecated in favor of these helpers.
const result = User.safeParse(await request.json());
if (!result.success) {
const { fieldErrors } = z.flattenError(result.error);
return Response.json({ errors: fieldErrors }, { status: 400 });
}
const user = result.data; // fully typed as User
Enter fullscreen mode Exit fullscreen mode
When should I use zod/mini instead of the full zod package?
Use zod/mini when bundle size matters — client code, edge functions, a shipped widget — and the full zod package everywhere else. zod/mini exposes the same validators through a functional API: instead of chaining .optional() you wrap with z.optional(), and refinements use .check() rather than .refine(). It is more verbose, but with no method chain, unused code tree-shakes away. Both builds share the same core, so runtime behavior is identical.
import * as z from "zod/mini";
const User = z.object({
email: z.email(),
name: z.optional(z.string()),
});
Enter fullscreen mode Exit fullscreen mode
| Concern | zod (full) | zod/mini |
|---|---|---|
| API style | Chained methods (.optional()) |
Functional wrappers (z.optional()) |
| Bundle size | Larger | Smaller, tree-shakeable |
| Ergonomics | Fluent, readable | More verbose |
| Same validators | Yes | Yes (same core) |
| Best for | Servers, general code | Edge/client, size-critical bundles |
How do I migrate from Zod 3 without breaking everything?
Migrate incrementally, because Zod 4 keeps the deprecated Zod 3 APIs working with warnings rather than removing them. During the transition Zod published the new version under the zod/v4 import path (in [email protected]) so you could adopt it file by file before [email protected]; the legacy API stays reachable at zod/v3. My process: upgrade the package, run the type-checker and tests, then clear deprecation warnings in waves — rename string-format calls, collapse error options into error, and switch .strict()/.passthrough() to the new object functions. z.infer, .parse(), and .safeParse() did not change, so most schemas kept working untouched.
FAQ
Q: Is z.string().email() removed in Zod 4?
A: No. It still works but is deprecated and warns. The recommended form is the top-level z.email(), and both validate identically.
Q: What replaced errorMap and invalid_type_error in Zod 4?
A: A single error parameter — a string for a fixed message or a function that receives the issue for conditional messages.
Q: How do I get field-level errors for a form in Zod 4?
A: Call z.flattenError(error); its fieldErrors maps each schema key to its messages. Use z.treeifyError for nested shapes.
Q: Is zod/mini a separate library?
A: No. It is a build of Zod 4 with the same validators exposed through a functional, tree-shakeable API for smaller bundles.
Q: Does Zod 4 require a specific TypeScript version?
A: Yes. Zod 4 requires TypeScript 5.5 or newer.
Originally published on devya.dev. Also on eng-ahmed.com. Built by Devya Solutions.
0 Comments
Log in to join the conversation.No comments yet. Be the first to share your thoughts.