Skip to main content
What Changed in Zod 4, and How We Migrated Production Schemas

What Changed in Zod 4, and How We Migrated Production Schemas

July 20, 2026
Full-Stack Engineering
5 min read

Key takeaways

- Zod 4 is the stable major of the TypeScript-first schema validator, released in 2025, focused on faster type-checking, faster runtime parsing, and a smaller core; it requires TypeScript 5.5 or newer.

- String formats are now top-level tree-shakeable functions — z.email(), z.uuid(), z.url() — and the chained form z.string().email() is deprecated but still works.

- A single error parameter replaces Zod 3's four options: message, invalid_type_error, required_error, and errorMap; pass a string or a function that receives the issue.

- Error formatting moved to three standalone helpers: z.flattenError for form field errors, z.treeifyError for nested shapes, and z.prettifyError for a human-readable string.

- The zod/mini build exposes the same validators through a functional, tree-shakeable API for size-critical bundles, while z.infer, .parse(), and .safeParse() did not change.

At Devya we reach for Zod on almost every project to validate untrusted input at the boundary — request bodies, form data, environment variables, third-party API responses. Zod 4 changed enough of the surface that a mechanical upgrade tripped a handful of files across our codebases, so we mapped exactly what moved. These are our field notes.

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 our 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(). Zod 4 also added a zod/mini build for smaller bundles and built-in JSON Schema conversion through z.toJSONSchema().

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 z.string().email() still works but is deprecated and logs a warning.

The reason is tree-shaking: each format is its own function, so a bundle that only validates emails no longer ships the validation logic for every other format it never uses. In an object schema, the id field becomes z.uuid(), the email field z.email(), an optional website z.url().optional(), and the TypeScript type is derived with z.infer of the schema.

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 depending on the case.

In Zod 4 you pass one error: a string for a fixed message, or a function that receives the issue and returns a message. That single function can distinguish a missing value from a wrong type by checking whether issue.input is undefined, all in one place.

How do I turn a ZodError into form errors?

Zod 4 moves error formatting out of methods on the error object and into three top-level helpers. z.flattenError(error) returns an object with formErrors and fieldErrors, which maps cleanly onto a form's field-level messages. z.treeifyError(error) returns a nested object mirroring the schema shape, for deeply nested data. z.prettifyError(error) returns a human-readable multiline string for logs and CLIs.

The raw error.issues array is unchanged, and the older error.format() and error.flatten() methods are deprecated in favor of these helpers. A typical route handler calls safeParse on the request body; on failure it reads fieldErrors from z.flattenError and returns them with a 400 status, and on success the data property is fully typed.

When should we use zod/mini instead of the full zod package?

Use zod/mini when bundle size matters — client code, edge functions, a shipped widget — and use 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().

That is more verbose, but because there is no method chain, unused code tree-shakes away and the bundle shrinks. Both builds share the same core, so a schema's runtime behavior is identical. In short: chained methods and readability in the full package; functional wrappers and a smaller footprint in zod/mini.

How do we 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 outright. During the transition Zod published the new version under the zod/v4 import path, in the zod@3.25 release, so teams could adopt it file by file before zod@4.0 landed; the legacy API stays reachable at zod/v3.

Our process: upgrade the package, run the type-checker and the test suite, then clear the deprecation warnings in waves — rename string-format calls, collapse the error options into a single error, and switch .strict()/.passthrough() to z.strictObject()/z.looseObject(). Because z.infer, .parse(), and .safeParse() did not change, most schemas kept working untouched.

FAQ

**Q:** Is z.string().email() removed in Zod 4?

**A:** No. It still works but is deprecated and logs a warning. 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. Pass a string for a fixed message, or a function that receives the issue for conditional messages such as distinguishing a missing value from a wrong type.

**Q:** How do we 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(error) for deeply 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.