Changelog
The page is a migration guide that aims to comprehensively list the breaking changes in Zod 4. (To review the performance enhancements and new features in Zod 4, read the introductory post.) Every effort was made to prevent breaking changes, but some are unavoidable. Most of the removed APIs are long-deprecated or undocumented. The changes are listed in order of impact.
Note — Zod 3 exported a large number of undocumented utility types and functions that are not considered part of the public API. Changes to those are not documented here, as they aren't considered part of Zod's public API.
To install the beta:
Error customization
This is perhaps the most visible of the removed APIs. Zod 4 standardizes the APIs for error customization under a single, unified error
param.
deprecates message
Replace message
with error
. The message
parameter is still supported but deprecated.
drops invalid_type_error
and required_error
The invalid_type_error
/ required_error
params are dropped. These can now be more cleanly with the new error
parameter.
drops errorMap
This is renamed to error
. Error maps can also now return a string or undefined
(which yields control to the next error map in the chain).
z.number()
no infinite values
POSITIVE_INFINITY
and NEGATIVE_INFINITY
are no longer considered valid values for z.number()
.
.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
). Using integers out of this range causes spontaneous rounding errors.
ZodError
no longer extends Error
It is very slow to instantiate Error
instances in JavaScript, as the initialization process snapshots the call stack. The magnitude of this performance little value and makes the "validation error" code path unacceptably slow.
In Zod 4 the ZodError
class no longer extends the plain JavaScript Error
class. Any code that relies on instanceof Error
will need to be refactored.
updates issue formats
The issue formats have been dramatically streamlined.
Below is the list of Zod 3 issues types and their Zod 4 equivalent:
While certain Zod 4 issue types have been merged, dropped, and modified, each issue remains structurally similar to Zod 3 counterpart (identical, in most cases). All issues still conform to the same base interface as Zod 3, so most common error handling logic will work without modification.
changes error map precedence
The error map precedence has been changed to be more consistent. Specifically, an error map passed into .parse()
no longer takes precedence over a schema-level error map.
deprecates .format()
The .format()
method on ZodError
has been deprecated. Instead use the top-level z.treeifyError()
function. Read the Formatting errors docs for more information.
drops .formErrors
This API was identical to .flatten()
. It exists for historical reasons and isn't documented.
drops .addIssue()
and .addIssues()
Directly modify the err.issues
array instead.
z.string()
updates
deprecates .email()
etc
String formats are now represented as schema classes of their own that extend ZodString
. As such, these APIs have been moved to the top-level z
namespace.
The method forms (z.string().email()
) still exist and work as before, but are now deprecated. They may be removed in a future version.
drops z.string().ip()
This has been replaced with separate .ipv4()
and .ipv6()
methods. Use z.union()
to combine them if you need to accept both.
z.object()
These modifier methods on the ZodObject
class determine how the schema handles unknown keys. In Zod 4, this functionality now exists in top-level functions. This aligns better with Zod's declarative-first philosophy, and puts all object variants on equal footing.
deprecates .strict()
deprecates .passthrough()
deprecates .strip()
This was never particularly useful, as it was the default behavior of z.object()
.
The equivalents for z.interface()
also exist:
drops .nonstrict()
This long-deprecated alias for .strip()
has been removed.
drops .deepPartial()
This has been long deprecated in Zod 3 and it now removed in Zod 4. There is no direct alternative to this API. There were lots of footguns in it's implementation, and it's use is generally an anti-pattern.
changes z.unknown()
optionality
The z.unknown()
and z.any()
types are no longer marked as "key optional" in the inferred types.
z.nativeEnum()
deprecated
The z.nativeEnum()
method is deprecated in favor of just z.enum()
. The z.enum()
API has been overloaded to support both.
As part of this refactor of ZodEnum
, a number of long-deprecated and redundunant features have been removed. These were all identical and only existed for historical reasons.
z.array()
changes .nonempty()
type
This now behaves identically to z.array().min(1)
. The inferred type does not change.
The old behavior is now better represented with z.tuple()
and a "rest" argument. This aligns more closely to TypeScript's type system.
z.promise()
deprecated
There's rarely a reason to use z.promise()
. If you have an input that may be a Promise
, just await
it before parsing it with Zod.
If you are using z.promise
to define an async function with z.function()
, that's no longer necessary either; see the ZodFunction
section below.
z.function()
The result of z.function()
is no longer a Zod schema. Instead, it acts as a standalone "function factory" for defining Zod-validated functions. The API has also changed; you define an input
and output
schema upfront, instead of using args()
and .returns()
methods.
adds .implementAsync()
To define an async function, use implementAsync()
instead of implement()
.
.refine()
ignores type predicates
In Zod 3, passing a type predicate as a refinement functions could still narrow the type of a schema. This wasn't documented but was discussd in some issues. This is no longer the case.
z.ostring()
, etc dropped
The undocumented convenience methods z.ostring()
, z.onumber()
, etc. have been removed. These were shorthand methods for defining optional string schemas.
z.literal()
drops symbol
support
Symbols aren't considered literal values, nor can they be simply compared with ===
. This was an oversight in Zod 3.
.create()
factories dropped
Previously all Zod classes defined a static .create()
method. These are now implemented as standalone factory functions.
z.discriminatedUnion()
You no longer need to specify a discriminator key (though you still can if you wish; it is ignored).
z.record()
drops single argument usage
Before, z.record()
could be used with a single argument. This is no longer supported.
improves enum support
Records have gotten a lot smarter. In Zod 3, passing an enum into z.record()
as a key schema would result in a partial type
In Zod 4, this is no longer the case. The inferred type is what you'd expect, and Zod ensures exhaustiveneess; that is, it makes sure all enum keys exist in the input during parsing.
z.intersection()
throws Error
on merge conflict
Zod intersection parses the input against two schemas, then attempts to merge the results. In Zod 3, when the results were unmergable, Zod through a ZodError
with a special "invalid_intersection_types"
issue.
In Zod 4, this will throw a regular Error
instead. The existence of unmergable results indicates a structural problem with the schema: an intersection of two incompatible types. Thus, a regular error is more appropriate than a validation error.
Internal changes
The typical user of Zod can likely ignore everything below this line. These changes do not impact the user-facing z
APIs.
There are too many internal changes to list here, but some may be relevant to regular users who are (intentionally or not) relying on certain implementation details. These changes will be of particular interest to library authors building tools on top of Zod.
updates generics
The generic structure of several classes has changed. Perhaps most significant is the change to the ZodType
base class:
The second generic Def
has been entirely removed. Instead the base class now only tracks Output
and Input
. While previously the Input
value defaulted to Output
, it now defaults to unknown
. This allows generic functions involving z.ZodType
to behave more intuitively in many cases.
The need for z.ZodTypeAny
has been eliminated; just use z.ZodType
instead.
adds z.core
Many utility functions and types have been moved to the new @zod/core
package, to facilitate code sharing between zod
and @zod/mini
. The contents of @zod/core
from zod
/@zod/mini
using the z.core
namespace. Check z.core
if any internal APIs you rely on are missing; they've likely been moved there.
moves ._def
The ._def
property is now moved to ._zod.def
. The structure of all internal defs is subject to change; this is relevant to library authors but won't be comprehensively documented here.
drops ZodEffects
This doesn't affect the user-facing APIs, but it's an internal change worth highlighting. It's part of a larger restructure of how Zod handles refinements.
Previously both refinements and transformations lived inside a wrapper class called ZodEffects
. That means adding either one to a schema would wrap the original schema in a ZodEffects
instance. In Zod 4, refinements now live inside the schemas themselves. More accurately, each schema contains an array of "checks"; the concept of a "check" is new in Zod 4 and generalizes the concept of a refinement to include potentially side-effectful transforms like z.toLowerCase()
.
This is particularly apparent in the @zod/mini
API, which heavily relies on the .check()
method to compose various validations together.
adds ZodTransform
Meanwhile, transforms have been moved into a dedicated ZodTransform
class. This schema class represents a input transform; in fact, you can actually define standalone transformations now:
This is primarily used in conjunction with ZodPipe
. The .transform()
method now returns an instance of ZodPipe
.
drops ZodPreprocess
As with .transform()
, the z.prepreprocess()
function now returns a ZodPipe
instance instead of a dedicated ZodPreprocess
instance.
drops ZodBranded
Branding is now handled with a direct modification to the inferred type, instead of a dedicated ZodBranded
class. The user-facing APIs remain the same.