迁移指南
本迁移指南旨在按影响从高到低的顺序列出 Zod 4 中的破坏性变更(breaking changes)。要了解更多关于 Zod 4 的性能增强和新特性,请阅读 介绍文章。
许多 Zod 的行为和 API 已变得更加直观和统一。本文档中描述的破坏性变更通常代表了 Zod 用户生活质量的重大改善。我强烈建议仔细阅读本指南。
注意 — Zod 3 导出了一些未记录的准内部实用类型和函数,这些不被视为公共 API 的一部分。对此类更改此处不作记录。
非官方 codemod — 社区维护的 codemod zod-v3-to-v4 现已可用。
错误自定义 (Error customization)
Zod 4 将错误自定义的 API 标准化为一个统一的 error 参数。以前 Zod 的错误自定义 API 是碎片化且不一致的。这在 Zod 4 中得到了清理。
弃用 message
用 error 替换 message。message 参数仍然受支持,但已被弃用。
移除 invalid_type_error 和 required_error
invalid_type_error / required_error 参数已被移除。这些是几年前匆忙添加的,作为一种比 errorMap 更简洁的错误自定义方式。它们伴随着各种隐患(不能与 errorMap 结合使用),并且与 Zod 的实际 issue 代码不一致(没有 required issue 代码)。
这些现在可以用新的 error 参数清晰地表示。
移除 errorMap
这已被重命名为 error。
错误映射(Error maps)现在也可以返回纯 string(而不是 {message: string})。它们也可以返回 undefined,告诉 Zod 将控制权交给链中的下一个错误映射。
ZodError
更新 issue 格式
issue 格式已得到极大简化。
以下是 Zod 3 issue 类型及其 Zod 4 等效项的列表:
虽然某些 Zod 4 issue 类型已被合并、删除或修改,但每个 issue 在结构上仍与 Zod 3 的对应项相似(在大多数情况下是相同的)。所有 issue 仍然符合与 Zod 3 相同的基础接口,因此大多数常见的错误处理逻辑无需修改即可工作。
更改错误映射优先级
错误映射(error map)的优先级已更改为更加一致。具体来说,传递给 .parse() 的错误映射 不再 优先于 schema 级别的错误映射。
弃用 .format()
ZodError 上的 .format() 方法已被弃用。请改用顶级 z.treeifyError() 函数。阅读 格式化错误文档 了解更多信息。
弃用 .flatten()
ZodError 上的 .flatten() 方法也已被弃用。请改用顶级 z.treeifyError() 函数。阅读 格式化错误文档 了解更多信息。
移除 .formErrors
此 API 与 .flatten() 相同。它因历史原因而存在,且未记录。
弃用 .addIssue() 和 .addIssues()
如果有必要,直接 push 到 err.issues 数组。
z.number()
无无限值
POSITIVE_INFINITY 和 NEGATIVE_INFINITY 不再被视为 z.number() 的有效值。
.safe() 不再接受浮点数
在 Zod 3 中,z.number().safe() 已被弃用。它现在的行为与 .int() 完全相同(见下文)。重要的是,这意味着它不再接受浮点数。
.int() 仅接受安全整数
z.number().int() API 不再接受不安全的整数(超出 Number.MIN_SAFE_INTEGER 和 Number.MAX_SAFE_INTEGER 范围)。使用超出此范围的整数会导致自发的舍入错误。(另外:你应该切换到 z.int()。)
z.string() 更新
弃用 .email() 等
字符串格式现在表示为 ZodString 的 子类,而不是简单的内部细化。因此,这些 API 已移至顶级 z 命名空间。顶级 API 也更简洁且更易于 tree-shake。
方法形式(z.string().email())仍然存在并像以前一样工作,但现在已被弃用。
更严格的 .uuid()
z.uuid() 现在针对 RFC 9562/4122 规范更严格地验证 UUID;具体来说,根据规范,变体位必须是 10。对于更宽松的 "类 UUID" 验证器,请使用 z.guid()。
.base64url() 中没有填充
z.base64url()(以前是 z.string().base64url())中不再允许填充。通常,base64url 字符串应该是无填充且 URL 安全的。
移除 z.string().ip()
这已被单独的 .ipv4() 和 .ipv6() 方法取代。如果你需要同时接受两者,请使用 z.union() 将它们组合。
更新 z.string().ipv6()
验证现在使用 new URL() 构造函数进行,这比旧的正则表达式方法健壮得多。一些以前通过验证的无效值现在可能会失败。
移除 z.string().cidr()
同样,这已被单独的 .cidrv4() 和 .cidrv6() 方法取代。如果你需要同时接受两者,请使用 z.union() 将它们组合。
z.coerce 更新
所有 z.coerce schemas 的输入类型现在都是 unknown。
.default() 更新
.default() 的应用发生了微妙的变化。如果输入是 undefined,ZodDefault 会短路解析过程并返回默认值。默认值必须可分配给 输出类型。
在 Zod 3 中,.default() 期望一个与 输入类型 匹配的值。ZodDefault 会解析默认值,而不是短路。因此,默认值必须可分配给 schema 的 输入类型。
为了复制旧的行为,Zod 实现了一个新的 .prefault() API。这是 "pre-parse default"(预解析默认值)的缩写。
z.object()
默认值在可选字段中应用
默认值在你的属性内部应用,即使在可选字段中也是如此。这更符合预期,并解决了 Zod 3 长期以来的可用性问题。这是一个微妙的变化,可能会导致依赖键存在性等的代码路径中断。
弃用 .strict() 和 .passthrough()
这些方法通常不再必要。请改用顶级的 z.strictObject() 和 z.looseObject() 函数。
这些方法仍然可用以保持向后兼容性,并且不会被移除。它们被视为遗留方法。
弃用 .strip()
这从来都不是特别有用,因为它是 z.object() 的默认行为。要将严格对象转换为 "常规" 对象,请使用 z.object(A.shape)。
移除 .nonstrict()
这个长期弃用的 .strip() 别名已被移除。
移除 .deepPartial()
这在 Zod 3 中早已弃用,现在在 Zod 4 中被移除。没有直接替代此 API 的方法。其实际实现中有很多隐患,使用它通常是一种反模式。
更改 z.unknown() 的可选性
z.unknown() 和 z.any() 类型不再在推断类型中标记为 "键可选"。
弃用 .merge()
ZodObject 上的 .merge() 方法已被弃用,取而代之的是 .extend()。.extend() 方法提供相同的功能,避免了关于严格性继承的歧义,并且具有更好的 TypeScript 性能。
注意:为了获得更好的 TypeScript 性能,请考虑使用对象解构而不是 .extend()。有关详细信息,请参阅 API 文档。
z.nativeEnum() 弃用
z.nativeEnum() 函数现在已被弃用,取而代之的是 z.enum()。z.enum() API 已被重载以支持类似枚举的输入。
作为这次 ZodEnum 重构的一部分,许多长期弃用和冗余的功能已被移除。这些功能都是相同的,只是因历史原因而存在。
z.array()
更改 .nonempty() 类型
这现在的行为与 z.array().min(1) 完全相同。推断类型不会改变。
旧的行为现在最好用 z.tuple() 和一个 "rest" 参数表示。这更接近 TypeScript 的类型系统。
z.promise() 弃用
很少有理由使用 z.promise()。如果你有一个可能是 Promise 的输入,只需在用 Zod 解析之前 await 它。
如果你使用 z.promise 来定义带有 z.function() 的异步函数,那也不再必要了;请参阅下面的 ZodFunction 部分。
z.function()
z.function() 的结果不再是 Zod schema。它现在作为一个独立的 "函数工厂",用于定义 Zod 验证的函数。API 也发生了变化;你首先定义 input 和 output schema,而不是使用 args() 和 .returns() 方法。
如果你迫切需要具有函数类型的 Zod schema,请考虑 这个变通方法。
新增 .implementAsync()
要定义一个异步函数,请使用 implementAsync() 而不是 implement()。
.refine()
忽略类型谓词
在 Zod 3 中,传递一个 类型谓词 作为细化函数(refinement function)仍然可以缩小 schema 的类型。这未被记录,但在一些 issue 中进行了讨论。现在情况不再如此。
移除 ctx.path
Zod 的新解析架构不会急切地评估 path 数组。这是一个必要的变更,释放了 Zod 4 巨大的性能提升。
移除函数作为第二个参数
以下可怕的重载已被移除。
z.ostring(), 等已被移除
未记录的便捷方法 z.ostring(), z.onumber(), 等已被移除。这些是定义可选字符串 schemas 的简写方法。
z.literal()
移除 symbol 支持
Symbols 不被视为字面值,也不能简单地用 === 进行比较。这是 Zod 3 中的一个疏忽。
静态 .create() 工厂已被移除
以前所有的 Zod 类都定义了一个静态 .create() 方法。这些现在被实现为独立的工厂函数。
z.record()
移除单参数用法
以前,z.record() 可以使用单个参数。这不再受支持。
改进枚举支持
Records 变得更智能了。在 Zod 3 中,将枚举作为 key schema 传递给 z.record() 会导致部分类型。
在 Zod 4 中,情况不再如此。推断类型符合预期,Zod 确保详尽性;也就是说,它确保所有枚举键在解析期间都存在于输入中。
要用可选键复制旧行为,请使用 z.partialRecord():
z.intersection()
合并冲突时抛出 Error
Zod 交叉类型针对两个 schemas 解析输入,然后尝试合并结果。在 Zod 3 中,当结果无法合并时,Zod 会抛出一个带有特殊 "invalid_intersection_types" issue 的 ZodError。
在 Zod 4 中,这将抛出一个常规的 Error。不可合并结果的存在表明 schema 存在结构问题:两个不兼容类型的交叉。因此,常规错误比验证错误更合适。
内部变更
典型的 Zod 用户可能可以忽略此行以下的所有内容。这些更改不会影响面向用户的 z API。
内部更改太多,无法在此列出,但某些更改可能与(有意或无意)依赖某些实现细节的普通用户相关。这些更改将对在 Zod 之上构建工具的库作者特别感兴趣。
更新泛型
几个类的泛型结构发生了变化。也许最重要的是 ZodType 基类的变化:
第二个泛型 Def 已被完全移除。相反,基类现在只跟踪 Output 和 Input。虽然以前 Input 值默认为 Output,但现在默认为 unknown。这允许涉及 z.ZodType 的泛型函数在许多情况下表现得更直观。
对 z.ZodTypeAny 的需求已被消除;只需使用 z.ZodType。
新增 z.core
许多实用函数和类型已移至新的 zod/v4/core 子包,以促进 Zod 和 Zod Mini 之间的代码共享。
为了方便起见,zod/v4/core 的内容也从 zod 和 zod/mini 的 z.core 命名空间下重新导出。
有关核心子库内容的更多信息,请参阅 Zod Core 文档。
移动 ._def
._def 属性现在已移至 ._zod.def。所有内部 defs 的结构都在变化;这对库作者有关,但不会在此全面记录。
移除 ZodEffects
这不会影响面向用户的 API,但这是一个值得强调的内部变更。这是 Zod 如何处理 refinements 的更大重组的一部分。
以前,refinements 和 transformations 都驻留在一个名为 ZodEffects 的包装类中。这意味着向 schema 添加其中任何一个都会将原始 schema 包装在 ZodEffects 实例中。在 Zod 4 中,refinements 现在驻留在 schemas 自身内部。更确切地说,每个 schema 包含一个 "checks" 数组;"check" 的概念在 Zod 4 中是新的,它概括了 refinement 的概念,包括潜在的副作用转换,如 z.toLowerCase()。
这在 Zod Mini API 中尤为明显,它在很大程度上依赖 .check() 方法将各种验证组合在一起。
新增 ZodTransform
同时,transforms 已移至专用的 ZodTransform 类。这个 schema 类表示输入转换;事实上,你现在实际上可以定义独立的 transformations:
这主要与 ZodPipe 结合使用。.transform() 方法现在返回 ZodPipe 的实例。
移除 ZodPreprocess
与 .transform() 一样,z.preprocess() 函数现在返回 ZodPipe 实例,而不是专用的 ZodPreprocess 实例。
移除 ZodBranded
Branding 现在通过直接修改推断类型来处理,而不是专用的 ZodBranded 类。面向用户的 API 保持不变。

