迁移指南
本迁移指南旨在按影响从高到低列出 Zod 4 中的重大变更。要了解更多关于 Zod 4 的性能提升和新特性,请阅读介绍文章。
🌐 This migration guide aims to list 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.
Zod 的许多行为和 API 已变得更加直观和一致。本文档中描述的重大更改通常代表了 Zod 用户使用体验的重大改进。我强烈建议仔细阅读本指南。
🌐 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.
注意 — Zod 3 导出了一些未记录的准内部工具类型和函数,这些不被视为公共 API 的一部分。对此类内容的更改在此未作记录。
非官方代码修改工具 — 社区维护的代码修改工具 zod-v3-to-v4 可用。
错误自定义
🌐 Error customization
Zod 4 将错误自定义的 API 统一标准化到一个单一的、统一的 error 参数下。之前 Zod 的错误自定义 API 是分散且不一致的。在 Zod 4 中,这一问题得到了清理。
🌐 Zod 4 standardizes the APIs for error customization under a single, unified error param. Previously Zod's error customization APIs were fragmented and inconsistent. This is cleaned up in Zod 4.
不推荐使用 message 参数
🌐 deprecates message parameter
用 error 替换 message 参数。旧的 message 参数仍然支持,但已弃用。
🌐 Replaces message param with error. The old message parameter is still supported but deprecated.
掉落 invalid_type_error 和 required_error
🌐 drops invalid_type_error and required_error
invalid_type_error / required_error 参数已被删除。这些参数是在多年前匆忙添加的,用于自定义错误,其方式比 errorMap 更简洁。它们带来了各种潜在问题(不能与 errorMap 一起使用),并且与 Zod 的实际问题代码不一致(没有 required 问题代码)。
🌐 The invalid_type_error / required_error params have been dropped. These were hastily added years ago as a way to customize errors that was less verbose than errorMap. They came with all sorts of footguns (they can't be used in conjunction with errorMap) and do not align with Zod's actual issue codes (there is no required issue code).
现在可以使用新的 error 参数来清晰表示这些。
🌐 These can now be cleanly represented with the new error parameter.
掉落 errorMap
🌐 drops errorMap
这已重命名为 error。
🌐 This is renamed to error.
错误映射现在也可以返回一个普通的 string(而不是 {message: string})。它们还可以返回 undefined,这会告诉 Zod 将控制权交给链中的下一个错误映射。
🌐 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.
ZodError
更新问题格式
🌐 updates issue formats
问题格式已显著简化。
🌐 The issue formats have been dramatically streamlined.
以下是 Zod 3 问题类型及其 Zod 4 等效类型的列表:
🌐 Below is the list of Zod 3 issues types and their Zod 4 equivalent:
虽然某些 Zod 4 的问题类型已经被合并、删除和修改,但每个问题在结构上仍然与 Zod 3 的对应问题相似(在大多数情况下是相同的)。所有问题仍然符合与 Zod 3 相同的基础接口,因此大多数常见的错误处理逻辑无需修改即可使用。
🌐 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
错误映射的优先级已更改,以便更加一致。具体来说,传入 .parse() 的错误映射 不再 优先于模式级的错误映射。
🌐 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.
弃用 .format()
🌐 deprecates .format()
ZodError 上的 .format() 方法已被弃用。请改用顶层的 z.treeifyError() 函数。有关更多信息,请阅读 格式错误文档。
🌐 The .format() method on ZodError has been deprecated. Instead use the top-level z.treeifyError() function. Read the Formatting errors docs for more information.
弃用 .flatten()
🌐 deprecates .flatten()
ZodError 上的 .flatten() 方法也已被弃用。请改用顶层的 z.treeifyError() 函数。有关更多信息,请阅读 格式错误文档。
🌐 The .flatten() method on ZodError has also been deprecated. Instead use the top-level z.treeifyError() function. Read the Formatting errors docs for more information.
掉落 .formErrors
🌐 drops .formErrors
此 API 与 .flatten() 相同。它存在的原因是历史遗留问题,并且没有文档记录。
🌐 This API was identical to .flatten(). It exists for historical reasons and isn't documented.
不推荐使用 .addIssue() 和 .addIssues()
🌐 deprecates .addIssue() and .addIssues()
如果有必要,直接推送到 err.issues 数组中。
🌐 Directly push to err.issues array instead, if necessary.
z.number()
不再使用无限值
🌐 no infinite values
POSITIVE_INFINITY 和 NEGATIVE_INFINITY 不再被视为 z.number() 的有效值。
.safe() 不再接受浮点数
🌐 .safe() no longer accepts floats
在 Zod 3 中,z.number().safe() 已被弃用。它现在的行为与 .int() 完全相同(见下文)。重要的是,这意味着它不再接受浮点数。
🌐 In Zod 3, z.number().safe() is deprecated. It now behaves identically to .int() (see below). Importantly, that means it no longer accepts floats.
.int() 仅接受安全整数
🌐 .int() accepts safe integers only
z.number().int() API 不再接受不安全的整数(超出 Number.MIN_SAFE_INTEGER 和 Number.MAX_SAFE_INTEGER 的范围)。使用超出此范围的整数会导致随机的四舍五入错误。(另外:你应该切换到 z.int()。)
🌐 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. (Also: You should switch to z.int().)
z.string() 更新
🌐 z.string() updates
不推荐使用 .email() 等
🌐 deprecates .email() etc
字符串格式现在表示为 ZodString 的子类,而不是简单的内部改进。因此,这些 API 已经被移动到顶层 z 命名空间。顶层 API 也更简洁,并且更容易进行树摇优化。
🌐 String formats are now represented as subclasses of ZodString, instead of simple internal refinements. As such, these APIs have been moved to the top-level z namespace. Top-level APIs are also less verbose and more tree-shakable.
方法形式(z.string().email())仍然存在并像以前一样工作,但现在已被弃用。
🌐 The method forms (z.string().email()) still exist and work as before, but are now deprecated.
更严格的 .uuid()
🌐 stricter .uuid()
z.uuid() 现在根据 RFC 9562/4122 规范更严格地验证 UUID;具体来说,变体位必须符合规范要求为 10。对于更宽松的“类似 UUID”的验证器,请使用 z.guid()。
🌐 The z.uuid() now validates UUIDs more strictly against the RFC 9562/4122 specification; specifically, the variant bits must be 10 per the spec. For a more permissive "UUID-like" validator, use z.guid().
.base64url() 中没有填充
🌐 no padding in .base64url()
z.base64url()(以前称作 z.string().base64url())不再允许使用填充。通常,base64url 字符串最好是不带填充且对 URL 安全的。
🌐 Padding is no longer allowed in z.base64url() (formerly z.string().base64url()). Generally it's desirable for base64url strings to be unpadded and URL-safe.
掉落 z.string().ip()
🌐 drops z.string().ip()
这已经被独立的 .ipv4() 和 .ipv6() 方法取代。如果你需要同时接受两者,请使用 z.union() 将它们结合起来。
🌐 This has been replaced with separate .ipv4() and .ipv6() methods. Use z.union() to combine them if you need to accept both.
更新 z.string().ipv6()
🌐 updates z.string().ipv6()
验证现在使用 new URL() 构造函数进行,这比旧的正则表达式方法更稳健。一些以前通过验证的无效值现在可能会失败。
🌐 Validation now happens using the new URL() constructor, which is far more robust than the old regular expression approach. Some invalid values that passed validation previously may now fail.
掉落 z.string().cidr()
🌐 drops z.string().cidr()
同样,这已被替换为单独的 .cidrv4() 和 .cidrv6() 方法。如果你需要同时接受两者,请使用 z.union() 来组合它们。
🌐 Similarly, this has been replaced with separate .cidrv4() and .cidrv6() methods. Use z.union() to combine them if you need to accept both.
z.coerce 更新
🌐 z.coerce updates
所有 z.coerce 模式的输入类型现在都是 unknown。
🌐 The input type of all z.coerce schemas is now unknown.
.default() 更新
🌐 .default() updates
.default() 的应用方式发生了微妙的变化。如果输入是 undefined,ZodDefault 会短路解析过程并返回默认值。默认值必须可以赋值给输出类型。
🌐 The application of .default() has changed in a subtle way. If the input is undefined, ZodDefault short-circuits the parsing process and returns the default value. The default value must be assignable to the output type.
在 Zod 3 中,.default() 期望一个匹配输入类型的值。ZodDefault 会解析默认值,而不是短路。因此,默认值必须可以分配给该模式的输入类型。
🌐 In Zod 3, .default() expected a value that matched the input type. ZodDefault would parse the default value, instead of short-circuiting. As such, the default value must be assignable to the input type of the schema.
为了复制旧的行为,Zod 实现了一个新的 .prefault() API。这是“预解析默认”的缩写。
🌐 To replicate the old behavior, Zod implements a new .prefault() API. This is short for "pre-parse default".
z.object()
可选字段中应用的默认值
🌐 defaults applied within optional fields
即使在可选字段中,属性内的默认值也会被应用。这更符合预期,并解决了 Zod 3 长期存在的可用性问题。这是一个细微的变化,可能会导致依赖键存在性的代码路径出现中断等问题。
🌐 Defaults inside your properties are applied, even within optional fields. This aligns better with expectations and resolves a long-standing usability issue with Zod 3. This is a subtle change that may cause breakage in code paths that rely on key existence, etc.
不推荐使用 .strict() 和 .passthrough()
🌐 deprecates .strict() and .passthrough()
这些方法通常不再必要。取而代之的是使用顶层的 z.strictObject() 和 z.looseObject() 函数。
🌐 These methods are generally no longer necessary. Instead use the top-level z.strictObject() and z.looseObject() functions.
这些方法仍然可用于向后兼容,并且不会被移除。它们被视为遗留方法。
弃用 .strip()
🌐 deprecates .strip()
这从来都不是特别有用,因为这是 z.object() 的默认行为。要将严格对象转换为“普通”对象,请使用 z.object(A.shape)。
🌐 This was never particularly useful, as it was the default behavior of z.object(). To convert a strict object to a "regular" one, use z.object(A.shape).
掉落 .nonstrict()
🌐 drops .nonstrict()
这个已被长期弃用的 .strip() 别名已被移除。
🌐 This long-deprecated alias for .strip() has been removed.
掉落 .deepPartial()
🌐 drops .deepPartial()
在 Zod 3 中,这已经被长期弃用,并且在 Zod 4 中已被移除。这个 API 没有直接的替代方案。它的实现中存在很多潜在的陷阱,并且其使用通常被认为是一种反模式。
🌐 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 its implementation, and its use is generally an anti-pattern.
更改 z.unknown() 的可选性
🌐 changes z.unknown() optionality
z.unknown() 和 z.any() 类型在推断类型中不再标记为“键可选”。
🌐 The z.unknown() and z.any() types are no longer marked as "key optional" in the inferred types.
弃用 .merge()
🌐 deprecates .merge()
ZodObject 上的 .merge() 方法已被弃用,推荐使用 .extend()。.extend() 方法提供相同的功能,避免了严格继承的歧义,并且在 TypeScript 中性能更好。
🌐 The .merge() method on ZodObject has been deprecated in favor of .extend(). The .extend() method provides the same functionality, avoids ambiguity around strictness inheritance, and has better TypeScript performance.
注意:为了获得更好的 TypeScript 性能,请考虑使用对象解构代替 .extend()。有关更多详细信息,请参阅API 文档 。
z.nativeEnum() 已弃用
🌐 z.nativeEnum() deprecated
z.nativeEnum() 函数现在已被弃用,建议改用 z.enum()。z.enum() API 已被重载以支持类似枚举的输入。
🌐 The z.nativeEnum() function is now deprecated in favor of just z.enum(). The z.enum() API has been overloaded to support an enum-like input.
作为对 ZodEnum 进行此重构的一部分,许多长期废弃和多余的功能已被移除。这些功能完全相同,且仅出于历史原因而存在。
🌐 As part of this refactor of ZodEnum, a number of long-deprecated and redundant features have been removed. These were all identical and only existed for historical reasons.
z.array()
更改 .nonempty() 类型
🌐 changes .nonempty() type
现在这与 z.array().min(1) 的行为完全相同。推断的类型不会改变。
🌐 This now behaves identically to z.array().min(1). The inferred type does not change.
旧的行为现在通过 z.tuple() 和一个“剩余”参数更好地表示。这与 TypeScript 的类型系统更加一致。
🌐 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() 已弃用
🌐 z.promise() deprecated
几乎没有理由使用 z.promise()。如果你的输入可能是 Promise,只需在用 Zod 解析它之前 await。
🌐 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.
如果你使用 z.promise 与 z.function() 定义一个异步函数,那也不再是必须的;请参阅下面的 ZodFunction 部分。
z.function()
z.function() 的结果不再是 Zod 模式。相反,它作为一个独立的“函数工厂”来定义经过 Zod 验证的函数。API 也已更改;你需要预先定义 input 和 output 模式,而不是使用 args() 和 .returns() 方法。
🌐 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.
如果你迫切需要一个带有函数类型的 Zod 模式,可以考虑这个变通方法。
🌐 If you have a desperate need for a Zod schema with a function type, consider this workaround.
添加 .implementAsync()
🌐 adds .implementAsync()
要定义一个异步函数,使用 implementAsync() 而不是 implement()。
🌐 To define an async function, use implementAsync() instead of implement().
.refine()
忽略类型谓词
🌐 ignores type predicates
在 Zod 3 中,将 类型谓词 作为精炼函数仍然可以缩小模式的类型。这在文档中没有说明,但在一些问题中曾经讨论过。现在情况不再如此。
🌐 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 discussed in some issues. This is no longer the case.
掉落 ctx.path
🌐 drops ctx.path
Zod 的新解析架构不会预评估 path 数组。这是一个必要的更改,它解锁了 Zod 4 显著的性能提升。
🌐 Zod's new parsing architecture does not eagerly evaluate the path array. This was a necessary change that unlocks Zod 4's dramatic performance improvements.
删除函数作为第二个参数
🌐 drops function as second argument
已删除以下令人头疼的过载。
🌐 The following horrifying overload has been removed.
z.ostring() 等已删除
🌐 z.ostring(), etc dropped
未记录的便利方法 z.ostring()、z.onumber() 等已被移除。这些是用于定义可选字符串模式的简写方法。
🌐 The undocumented convenience methods z.ostring(), z.onumber(), etc. have been removed. These were shorthand methods for defining optional string schemas.
z.literal()
取消对 symbol 的支持
🌐 drops symbol support
符号不被视为字面值,也不能简单地与 === 进行比较。这是 Zod 3 中的一个疏忽。
🌐 Symbols aren't considered literal values, nor can they be simply compared with ===. This was an oversight in Zod 3.
静态 .create() 工厂已被删除
🌐 static .create() factories dropped
以前所有 Zod 类都定义了一个静态 .create() 方法。现在这些方法已作为独立的工厂函数实现。
🌐 Previously all Zod classes defined a static .create() method. These are now implemented as standalone factory functions.
z.record()
删除单个参数用法
🌐 drops single argument usage
以前,z.record() 可以使用单个参数。现在不再支持此用法。
🌐 Before, z.record() could be used with a single argument. This is no longer supported.
改进枚举支持
🌐 improves enum support
记录变得更加智能。在 Zod 3 中,将枚举传入 z.record() 作为键模式将导致部分类型
🌐 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
在 Zod 4 中,情况不再如此。推断的类型正如你所期望的那样,Zod 确保了穷尽性;也就是说,它在解析过程中确保所有枚举键都存在于输入中。
🌐 In Zod 4, this is no longer the case. The inferred type is what you'd expect, and Zod ensures exhaustiveness; that is, it makes sure all enum keys exist in the input during parsing.
要使用可选键复制旧行为,请使用 z.partialRecord():
🌐 To replicate the old behavior with optional keys, use z.partialRecord():
z.intersection()
在合并冲突时抛出 Error
🌐 throws Error on merge conflict
Zod 交集会将输入与两个模式进行解析,然后尝试合并结果。在 Zod 3 中,当结果无法合并时,Zod 会抛出一个带有特殊 "invalid_intersection_types" 问题的 ZodError。
🌐 Zod intersection parses the input against two schemas, then attempts to merge the results. In Zod 3, when the results were unmergable, Zod threw a ZodError with a special "invalid_intersection_types" issue.
在 Zod 4 中,这将改为抛出一个常规的 Error。不可合并结果的存在表明模式存在结构性问题:两个不兼容类型的交集。因此,常规错误比验证错误更合适。
🌐 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
Zod 的典型用户可能可以忽略本行以下的所有内容。这些更改不会影响面向用户的 z API。
这里有太多内部更改无法一一列出,但某些更改可能与依赖于某些实现细节的常规用户相关,无论是有意还是无意。这些更改对于在 Zod 上构建工具的库作者尤其有兴趣。
🌐 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
几个类的通用结构已发生变化。也许最重要的是对 ZodType 基类的更改:
🌐 The generic structure of several classes has changed. Perhaps most significant is the change to the ZodType base class:
第二个通用的 Def 已经被完全移除。取而代之的是,基类现在只跟踪 Output 和 Input。之前 Input 的值默认是 Output,现在默认是 unknown。这使得涉及 z.ZodType 的通用函数在许多情况下表现得更直观。
🌐 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.
z.ZodTypeAny 的需求已经被消除;只需使用 z.ZodType 即可。
🌐 The need for z.ZodTypeAny has been eliminated; just use z.ZodType instead.
添加 z.core
🌐 adds z.core
为了方便 Zod 和 Zod Mini 之间的代码共享,许多实用函数和类型已被移动到新的 zod/v4/core 子包中。
🌐 Many utility functions and types have been moved to the new zod/v4/core sub-package, to facilitate code sharing between Zod and Zod Mini.
为了方便,zod/v4/core 的内容也通过 zod 和 zod/mini 在 z.core 命名空间下重新导出。
🌐 For convenience, the contents of zod/v4/core are also re-exported from zod and zod/mini under the z.core namespace.
有关核心子库内容的更多信息,请参阅 Zod Core 文档。
🌐 Refer to the Zod Core docs for more information on the contents of the core sub-library.
移动 ._def
🌐 moves ._def
._def 属性现在已移动到 ._zod.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.
掉落 ZodEffects
🌐 drops ZodEffects
这不会影响面向用户的 API,但这是一个值得关注的内部更改。它是 Zod 如何处理精炼的更大重构的一部分。
🌐 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.
以前,改进(refinements)和转换(transformations)都存在于一个名为 ZodEffects 的封装类中。这意味着将其中任何一个添加到模式(schema)中都会将原始模式封装在一个 ZodEffects 实例中。在 Zod 4 中,改进现在存在于模式本身中。更准确地说,每个模式包含一个“检查”(checks)数组;“检查”的概念在 Zod 4 中是新的,它将改进的概念概括化,以包括可能具有副作用的转换(如 z.toLowerCase())。
🌐 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().
这一点在 Zod Mini API 中尤为明显,它在很大程度上依赖 .check() 方法将各种验证组合在一起。
🌐 This is particularly apparent in the Zod Mini API, which heavily relies on the .check() method to compose various validations together.
添加 ZodTransform
🌐 adds ZodTransform
与此同时,变换已被移入一个专用的 ZodTransform 类。这个 schema 类表示一个输入变换;实际上,你现在可以定义独立的变换:
🌐 Meanwhile, transforms have been moved into a dedicated ZodTransform class. This schema class represents an input transform; in fact, you can actually define standalone transformations now:
这主要与 ZodPipe 一起使用。.transform() 方法现在返回一个 ZodPipe 实例。
🌐 This is primarily used in conjunction with ZodPipe. The .transform() method now returns an instance of ZodPipe.
掉落 ZodPreprocess
🌐 drops ZodPreprocess
与 .transform() 相同,z.preprocess() 函数现在返回一个 ZodPipe 实例,而不是专用的 ZodPreprocess 实例。
🌐 As with .transform(), the z.preprocess() function now returns a ZodPipe instance instead of a dedicated ZodPreprocess instance.
掉落 ZodBranded
🌐 drops ZodBranded
品牌化现在通过对推断类型的直接修改来处理,而不是使用专门的 ZodBranded 类。面向用户的 API 保持不变。
🌐 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.

