Skip to main content
This migration guide lists the breaking changes in Zod 4 in order of highest to lowest impact. To learn more about the performance enhancements and new features of Zod 4, read the introductory post.

Installation

To upgrade to Zod 4:
Many of Zod’s behaviors and APIs have been made more intuitive and cohesive. The breaking changes described in this document often represent major quality-of-life improvements for Zod users. I strongly recommend reading this guide thoroughly.
Note — Zod 3 exported a number of undocumented quasi-internal utility types and functions that are not considered part of the public API. Changes to those are not documented here.
Unofficial codemod — A community-maintained codemod zod-v3-to-v4 is available.

Breaking Changes

Zod 4 standardizes the APIs for error customization under a single, unified error param. Previously Zod’s error customization APIs were fragmented and inconsistent.

Deprecates message parameter

Replaces message param with error. The old message parameter is still supported but deprecated.

Drops invalid_type_error and required_error

The invalid_type_error / required_error params have been dropped. These can now be cleanly represented with the new error parameter.

Drops errorMap

This is renamed to error. Error maps can also now return a plain string (instead of {message: string}). They can also return undefined, which tells Zod to yield control to the next error map in the chain.

Updates issue formats

The issue formats have been dramatically streamlined.

Changes error map precedence

The error map precedence has been changed to be more consistent. An error map passed into .parse() no longer takes precedence over a schema-level error map.

Deprecates .format() and .flatten()

The .format() and .flatten() methods on ZodError have been deprecated. Instead use the top-level z.treeifyError() function.

Deprecates .addIssue() and .addIssues()

Directly push to err.issues array instead:

No infinite values

POSITIVE_INFINITY and NEGATIVE_INFINITY are no longer considered valid values for z.number().

.safe() no longer accepts floats

In Zod 4, z.number().safe() is deprecated. It now behaves identically to .int(), meaning it no longer accepts floats.

.int() accepts safe integers only

The z.number().int() API no longer accepts unsafe integers (outside the range of Number.MIN_SAFE_INTEGER and Number.MAX_SAFE_INTEGER).

Deprecates .email() etc

String formats are now represented as subclasses of ZodString. These APIs have been moved to the top-level z namespace.

Stricter .uuid()

The z.uuid() now validates UUIDs more strictly against the RFC 9562/4122 specification. For a more permissive validator, use z.guid().

Drops z.string().ip() and z.string().cidr()

The input type of all z.coerce schemas is now unknown.

.default() updates

The application of .default() has changed. If the input is undefined, ZodDefault short-circuits and returns the default value. The default value must be assignable to the output type.

New .prefault() API

To replicate the old behavior, use the new .prefault() API (“pre-parse default”):

Defaults applied within optional fields

Defaults inside properties are applied, even within optional fields. This may cause breakage in code paths that rely on key existence.

Deprecates .strict() and .passthrough()

Use the top-level z.strictObject() and z.looseObject() functions instead:

Deprecates .merge()

The .merge() method has been deprecated in favor of .extend():

Drops .deepPartial()

This long-deprecated method has been removed with no direct replacement.

Changes z.unknown() optionality

z.nativeEnum() deprecated

The z.nativeEnum() function is now deprecated. Use z.enum() instead, which now supports enum-like inputs:

Removes redundant enum APIs

Changes .nonempty() type

For the old behavior, use z.tuple() with a rest argument:

API restructure

The result of z.function() is no longer a Zod schema. It acts as a standalone “function factory” for defining Zod-validated functions.

Adds .implementAsync()

For async functions, use the new implementAsync() method:

Ignores type predicates

Passing a type predicate as a refinement function no longer narrows the type.

Drops ctx.path

The ctx.path property is no longer available in refinement functions:

Drops function as second argument

The following overload has been removed:

Drops single argument usage

Improves enum support

Records with enum keys now ensure exhaustiveness:
For optional keys, use z.partialRecord():

z.promise() deprecated

If you have an input that may be a Promise, just await it before parsing with Zod.

z.literal() drops symbol support

Symbols are no longer considered literal values.

Static .create() factories dropped

Previously all Zod classes defined a static .create() method. These are now implemented as standalone factory functions.

z.intersection() throws Error on merge conflict

When intersection results are unmergable, Zod now throws a regular Error instead of ZodError.

Drops convenience methods

The undocumented convenience methods z.ostring(), z.onumber(), etc. have been removed.

Internal Changes

Updates generics

The generic structure of ZodType has changed:

Adds z.core

Many utility functions and types have been moved to the new zod/v4/core sub-package:

Moves ._def

The ._def property is now moved to ._zod.def.

Drops ZodEffects

Refinements now live inside schemas themselves as “checks”. Transforms have been moved to a dedicated ZodTransform class.

Drops ZodPreprocess

The z.preprocess() function now returns a ZodPipe instance:

Drops ZodBranded

Branding is now handled with a direct modification to the inferred type, instead of a dedicated class.

Migration Steps

  1. Update dependencies: Install zod@^4.0.0
  2. Run the codemod: Consider using zod-v3-to-v4 for automated migration
  3. Update error handling: Replace message, invalid_type_error, required_error, and errorMap with the unified error parameter
  4. Update string validations: Replace method calls like .email() with top-level functions like z.email()
  5. Update object schemas: Replace .merge() with .extend(), and .strict()/.passthrough() with z.strictObject()/z.looseObject()
  6. Update function schemas: Restructure to use the new input/output API
  7. Test thoroughly: Run your test suite to catch any edge cases