💎 Zod 4 is now stable!  Read the announcement.
Zod logo

迁移指南

本迁移指南旨在按影响从高到低的顺序列出 Zod 4 中的破坏性变更(breaking changes)。要了解更多关于 Zod 4 的性能增强和新特性,请阅读 介绍文章

npm install zod@^4.0.0

许多 Zod 的行为和 API 已变得更加直观和统一。本文档中描述的破坏性变更通常代表了 Zod 用户生活质量的重大改善。我强烈建议仔细阅读本指南。

注意 — Zod 3 导出了一些未记录的准内部实用类型和函数,这些不被视为公共 API 的一部分。对此类更改此处不作记录。

非官方 codemod — 社区维护的 codemod zod-v3-to-v4 现已可用。

错误自定义 (Error customization)

Zod 4 将错误自定义的 API 标准化为一个统一的 error 参数。以前 Zod 的错误自定义 API 是碎片化且不一致的。这在 Zod 4 中得到了清理。

弃用 message

error 替换 messagemessage 参数仍然受支持,但已被弃用。

z.string().min(5, { error: "Too short." });

移除 invalid_type_errorrequired_error

invalid_type_error / required_error 参数已被移除。这些是几年前匆忙添加的,作为一种比 errorMap 更简洁的错误自定义方式。它们伴随着各种隐患(不能与 errorMap 结合使用),并且与 Zod 的实际 issue 代码不一致(没有 required issue 代码)。

这些现在可以用新的 error 参数清晰地表示。

z.string({ 
  error: (issue) => issue.input === undefined 
    ? "This field is required" 
    : "Not a string" 
});

移除 errorMap

这已被重命名为 error

错误映射(Error maps)现在也可以返回纯 string(而不是 {message: string})。它们也可以返回 undefined,告诉 Zod 将控制权交给链中的下一个错误映射。

z.string().min(5, {
  error: (issue) => {
    if (issue.code === "too_small") {
      return `Value must be >${issue.minimum}`
    }
  },
});

ZodError

更新 issue 格式

issue 格式已得到极大简化。

import * as z from "zod"; // v4
 
type IssueFormats = 
  | z.core.$ZodIssueInvalidType
  | z.core.$ZodIssueTooBig
  | z.core.$ZodIssueTooSmall
  | z.core.$ZodIssueInvalidStringFormat
  | z.core.$ZodIssueNotMultipleOf
  | z.core.$ZodIssueUnrecognizedKeys
  | z.core.$ZodIssueInvalidValue
  | z.core.$ZodIssueInvalidUnion
  | z.core.$ZodIssueInvalidKey // 新增: 用于 z.record/z.map 
  | z.core.$ZodIssueInvalidElement // 新增: 用于 z.map/z.set
  | z.core.$ZodIssueCustom;

以下是 Zod 3 issue 类型及其 Zod 4 等效项的列表:

import * as z from "zod"; // v3
 
export type IssueFormats =
  | z.ZodInvalidTypeIssue // ♻️ 重命名为 z.core.$ZodIssueInvalidType
  | z.ZodTooBigIssue  // ♻️ 重命名为 z.core.$ZodIssueTooBig
  | z.ZodTooSmallIssue // ♻️ 重命名为 z.core.$ZodIssueTooSmall
  | z.ZodInvalidStringIssue // ♻️ z.core.$ZodIssueInvalidStringFormat
  | z.ZodNotMultipleOfIssue // ♻️ 重命名为 z.core.$ZodIssueNotMultipleOf
  | z.ZodUnrecognizedKeysIssue // ♻️ 重命名为 z.core.$ZodIssueUnrecognizedKeys
  | z.ZodInvalidUnionIssue // ♻️ 重命名为 z.core.$ZodIssueInvalidUnion
  | z.ZodCustomIssue // ♻️ 重命名为 z.core.$ZodIssueCustom
  | z.ZodInvalidEnumValueIssue // ❌ 合并入 z.core.$ZodIssueInvalidValue
  | z.ZodInvalidLiteralIssue // ❌ 合并入 z.core.$ZodIssueInvalidValue
  | z.ZodInvalidUnionDiscriminatorIssue // ❌ 在 schema 创建时抛出错误
  | z.ZodInvalidArgumentsIssue // ❌ z.function 直接抛出 ZodError
  | z.ZodInvalidReturnTypeIssue // ❌ z.function 直接抛出 ZodError
  | z.ZodInvalidDateIssue // ❌ 合并入 invalid_type
  | z.ZodInvalidIntersectionTypesIssue // ❌ 已移除 (抛出常规 Error)
  | z.ZodNotFiniteIssue // ❌ 不再接受无限值 (invalid_type)

虽然某些 Zod 4 issue 类型已被合并、删除或修改,但每个 issue 在结构上仍与 Zod 3 的对应项相似(在大多数情况下是相同的)。所有 issue 仍然符合与 Zod 3 相同的基础接口,因此大多数常见的错误处理逻辑无需修改即可工作。

export interface $ZodIssueBase {
  readonly code?: string;
  readonly input?: unknown;
  readonly path: PropertyKey[];
  readonly message: string;
}

更改错误映射优先级

错误映射(error map)的优先级已更改为更加一致。具体来说,传递给 .parse() 的错误映射 不再 优先于 schema 级别的错误映射。

const mySchema = z.string({ error: () => "Schema-level error" });
 
// 在 Zod 3 中
mySchema.parse(12, { error: () => "Contextual error" }); // => "Contextual error"
 
// 在 Zod 4 中
mySchema.parse(12, { error: () => "Contextual error" }); // => "Schema-level error"

弃用 .format()

ZodError 上的 .format() 方法已被弃用。请改用顶级 z.treeifyError() 函数。阅读 格式化错误文档 了解更多信息。

弃用 .flatten()

ZodError 上的 .flatten() 方法也已被弃用。请改用顶级 z.treeifyError() 函数。阅读 格式化错误文档 了解更多信息。

移除 .formErrors

此 API 与 .flatten() 相同。它因历史原因而存在,且未记录。

弃用 .addIssue().addIssues()

如果有必要,直接 push 到 err.issues 数组。

myError.issues.push({ 
  // new issue
});

z.number()

无无限值

POSITIVE_INFINITYNEGATIVE_INFINITY 不再被视为 z.number() 的有效值。

.safe() 不再接受浮点数

在 Zod 3 中,z.number().safe() 已被弃用。它现在的行为与 .int() 完全相同(见下文)。重要的是,这意味着它不再接受浮点数。

.int() 仅接受安全整数

z.number().int() API 不再接受不安全的整数(超出 Number.MIN_SAFE_INTEGERNumber.MAX_SAFE_INTEGER 范围)。使用超出此范围的整数会导致自发的舍入错误。(另外:你应该切换到 z.int()。)

z.string() 更新

弃用 .email()

字符串格式现在表示为 ZodString子类,而不是简单的内部细化。因此,这些 API 已移至顶级 z 命名空间。顶级 API 也更简洁且更易于 tree-shake。

z.email();
z.uuid();
z.url();
z.emoji();         // 验证单个 emoji 字符
z.base64();
z.base64url();
z.nanoid();
z.cuid();
z.cuid2();
z.ulid();
z.ipv4();
z.ipv6();
z.cidrv4();          // ip 范围
z.cidrv6();          // ip 范围
z.iso.date();
z.iso.time();
z.iso.datetime();
z.iso.duration();

方法形式(z.string().email())仍然存在并像以前一样工作,但现在已被弃用。

z.string().email(); // ❌ 弃用
z.email(); // ✅ 

更严格的 .uuid()

z.uuid() 现在针对 RFC 9562/4122 规范更严格地验证 UUID;具体来说,根据规范,变体位必须是 10。对于更宽松的 "类 UUID" 验证器,请使用 z.guid()

z.uuid(); // 符合 RFC 9562/4122 的 UUID
z.guid(); // 任何 8-4-4-4-12 十六进制模式

.base64url() 中没有填充

z.base64url()(以前是 z.string().base64url())中不再允许填充。通常,base64url 字符串应该是无填充且 URL 安全的。

移除 z.string().ip()

这已被单独的 .ipv4().ipv6() 方法取代。如果你需要同时接受两者,请使用 z.union() 将它们组合。

z.string().ip() // ❌
z.ipv4() // ✅
z.ipv6() // ✅

更新 z.string().ipv6()

验证现在使用 new URL() 构造函数进行,这比旧的正则表达式方法健壮得多。一些以前通过验证的无效值现在可能会失败。

移除 z.string().cidr()

同样,这已被单独的 .cidrv4().cidrv6() 方法取代。如果你需要同时接受两者,请使用 z.union() 将它们组合。

z.string().cidr() // ❌
z.cidrv4() // ✅
z.cidrv6() // ✅

z.coerce 更新

所有 z.coerce schemas 的输入类型现在都是 unknown

const schema = z.coerce.string();
type schemaInput = z.input<typeof schema>;
 
// Zod 3: string;
// Zod 4: unknown;

.default() 更新

.default() 的应用发生了微妙的变化。如果输入是 undefinedZodDefault 会短路解析过程并返回默认值。默认值必须可分配给 输出类型

const schema = z.string()
  .transform(val => val.length)
  .default(0); // 应该是一个数字
schema.parse(undefined); // => 0

在 Zod 3 中,.default() 期望一个与 输入类型 匹配的值。ZodDefault 会解析默认值,而不是短路。因此,默认值必须可分配给 schema 的 输入类型

// Zod 3
const schema = z.string()
  .transform(val => val.length)
  .default("tuna");
schema.parse(undefined); // => 4

为了复制旧的行为,Zod 实现了一个新的 .prefault() API。这是 "pre-parse default"(预解析默认值)的缩写。

// Zod 3
const schema = z.string()
  .transform(val => val.length)
  .prefault("tuna");
schema.parse(undefined); // => 4

z.object()

默认值在可选字段中应用

默认值在你的属性内部应用,即使在可选字段中也是如此。这更符合预期,并解决了 Zod 3 长期以来的可用性问题。这是一个微妙的变化,可能会导致依赖键存在性等的代码路径中断。

const schema = z.object({
  a: z.string().default("tuna").optional(),
});
 
schema.parse({});
// Zod 4: { a: "tuna" }
// Zod 3: {}

弃用 .strict().passthrough()

这些方法通常不再必要。请改用顶级的 z.strictObject()z.looseObject() 函数。

// Zod 3
z.object({ name: z.string() }).strict();
z.object({ name: z.string() }).passthrough();
 
// Zod 4
z.strictObject({ name: z.string() });
z.looseObject({ name: z.string() });

这些方法仍然可用以保持向后兼容性,并且不会被移除。它们被视为遗留方法。

弃用 .strip()

这从来都不是特别有用,因为它是 z.object() 的默认行为。要将严格对象转换为 "常规" 对象,请使用 z.object(A.shape)

移除 .nonstrict()

这个长期弃用的 .strip() 别名已被移除。

移除 .deepPartial()

这在 Zod 3 中早已弃用,现在在 Zod 4 中被移除。没有直接替代此 API 的方法。其实际实现中有很多隐患,使用它通常是一种反模式。

更改 z.unknown() 的可选性

z.unknown()z.any() 类型不再在推断类型中标记为 "键可选"。

const mySchema = z.object({
  a: z.any(),
  b: z.unknown()
});
// Zod 3: { a?: any; b?: unknown };
// Zod 4: { a: any; b: unknown };

弃用 .merge()

ZodObject 上的 .merge() 方法已被弃用,取而代之的是 .extend().extend() 方法提供相同的功能,避免了关于严格性继承的歧义,并且具有更好的 TypeScript 性能。

// .merge (弃用)
const ExtendedSchema = BaseSchema.merge(AdditionalSchema);
 
// .extend (推荐)
const ExtendedSchema = BaseSchema.extend(AdditionalSchema.shape);
 
// 或者使用解构 (最佳 tsc 性能)
const ExtendedSchema = z.object({
  ...BaseSchema.shape,
  ...AdditionalSchema.shape,
});

注意:为了获得更好的 TypeScript 性能,请考虑使用对象解构而不是 .extend()。有关详细信息,请参阅 API 文档

z.nativeEnum() 弃用

z.nativeEnum() 函数现在已被弃用,取而代之的是 z.enum()z.enum() API 已被重载以支持类似枚举的输入。

enum Color {
  Red = "red",
  Green = "green",
  Blue = "blue",
}
 
const ColorSchema = z.enum(Color); // ✅

作为这次 ZodEnum 重构的一部分,许多长期弃用和冗余的功能已被移除。这些功能都是相同的,只是因历史原因而存在。

ColorSchema.enum.Red; // ✅ => "Red" (规范 API)
ColorSchema.Enum.Red; // ❌ 移除
ColorSchema.Values.Red; // ❌ 移除

z.array()

更改 .nonempty() 类型

这现在的行为与 z.array().min(1) 完全相同。推断类型不会改变。

const NonEmpty = z.array(z.string()).nonempty();
 
type NonEmpty = z.infer<typeof NonEmpty>; 
// Zod 3: [string, ...string[]]
// Zod 4: string[]

旧的行为现在最好用 z.tuple() 和一个 "rest" 参数表示。这更接近 TypeScript 的类型系统。

z.tuple([z.string()], z.string());
// => [string, ...string[]]

z.promise() 弃用

很少有理由使用 z.promise()。如果你有一个可能是 Promise 的输入,只需在用 Zod 解析之前 await 它。

如果你使用 z.promise 来定义带有 z.function() 的异步函数,那也不再必要了;请参阅下面的 ZodFunction 部分。

z.function()

z.function() 的结果不再是 Zod schema。它现在作为一个独立的 "函数工厂",用于定义 Zod 验证的函数。API 也发生了变化;你首先定义 inputoutput schema,而不是使用 args().returns() 方法。

const myFunction = z.function({
  input: [z.object({
    name: z.string(),
    age: z.number().int(),
  })],
  output: z.string(),
});
 
myFunction.implement((input) => {
  return `Hello ${input.name}, you are ${input.age} years old.`;
});

如果你迫切需要具有函数类型的 Zod schema,请考虑 这个变通方法

新增 .implementAsync()

要定义一个异步函数,请使用 implementAsync() 而不是 implement()

myFunction.implementAsync(async (input) => {
  return `Hello ${input.name}, you are ${input.age} years old.`;
});

.refine()

忽略类型谓词

在 Zod 3 中,传递一个 类型谓词 作为细化函数(refinement function)仍然可以缩小 schema 的类型。这未被记录,但在一些 issue 中进行了讨论。现在情况不再如此。

const mySchema = z.unknown().refine((val): val is string => {
  return typeof val === "string"
});
 
type MySchema = z.infer<typeof mySchema>; 
// Zod 3: `string`
// Zod 4: 仍然是 `unknown`

移除 ctx.path

Zod 的新解析架构不会急切地评估 path 数组。这是一个必要的变更,释放了 Zod 4 巨大的性能提升。

z.string().superRefine((val, ctx) => {
  ctx.path; // ❌ 不再可用
});

移除函数作为第二个参数

以下可怕的重载已被移除。

const longString = z.string().refine(
  (val) => val.length > 10,
  (val) => ({ message: `${val} is not more than 10 characters` })
);

z.ostring(), 等已被移除

未记录的便捷方法 z.ostring(), z.onumber(), 等已被移除。这些是定义可选字符串 schemas 的简写方法。

z.literal()

移除 symbol 支持

Symbols 不被视为字面值,也不能简单地用 === 进行比较。这是 Zod 3 中的一个疏忽。

静态 .create() 工厂已被移除

以前所有的 Zod 类都定义了一个静态 .create() 方法。这些现在被实现为独立的工厂函数。

z.ZodString.create(); // ❌ 

z.record()

移除单参数用法

以前,z.record() 可以使用单个参数。这不再受支持。

// Zod 3
z.record(z.string()); // ✅
 
// Zod 4
z.record(z.string()); // ❌
z.record(z.string(), z.string()); // ✅

改进枚举支持

Records 变得更智能了。在 Zod 3 中,将枚举作为 key schema 传递给 z.record() 会导致部分类型。

const myRecord = z.record(z.enum(["a", "b", "c"]), z.number()); 
// { a?: number; b?: number; c?: number; }

在 Zod 4 中,情况不再如此。推断类型符合预期,Zod 确保详尽性;也就是说,它确保所有枚举键在解析期间都存在于输入中。

const myRecord = z.record(z.enum(["a", "b", "c"]), z.number());
// { a: number; b: number; c: number; }

要用可选键复制旧行为,请使用 z.partialRecord()

const myRecord = z.partialRecord(z.enum(["a", "b", "c"]), z.number());
// { a?: number; b?: number; c?: number; }

z.intersection()

合并冲突时抛出 Error

Zod 交叉类型针对两个 schemas 解析输入,然后尝试合并结果。在 Zod 3 中,当结果无法合并时,Zod 会抛出一个带有特殊 "invalid_intersection_types" issue 的 ZodError

在 Zod 4 中,这将抛出一个常规的 Error。不可合并结果的存在表明 schema 存在结构问题:两个不兼容类型的交叉。因此,常规错误比验证错误更合适。

内部变更

典型的 Zod 用户可能可以忽略此行以下的所有内容。这些更改不会影响面向用户的 z API。

内部更改太多,无法在此列出,但某些更改可能与(有意或无意)依赖某些实现细节的普通用户相关。这些更改将对在 Zod 之上构建工具的库作者特别感兴趣。

更新泛型

几个类的泛型结构发生了变化。也许最重要的是 ZodType 基类的变化:

// Zod 3
class ZodType<Output, Def extends z.ZodTypeDef, Input = Output> {
  // ...
}
 
// Zod 4
class ZodType<Output = unknown, Input = unknown> {
  // ...
}

第二个泛型 Def 已被完全移除。相反,基类现在只跟踪 OutputInput。虽然以前 Input 值默认为 Output,但现在默认为 unknown。这允许涉及 z.ZodType 的泛型函数在许多情况下表现得更直观。

function inferSchema<T extends z.ZodType>(schema: T): T {
  return schema;
};
 
inferSchema(z.string()); // z.ZodString

z.ZodTypeAny 的需求已被消除;只需使用 z.ZodType

新增 z.core

许多实用函数和类型已移至新的 zod/v4/core 子包,以促进 Zod 和 Zod Mini 之间的代码共享。

import * as z from "zod/v4/core";
 
function handleError(iss: z.$ZodError) {
  // do stuff
}

为了方便起见,zod/v4/core 的内容也从 zodzod/miniz.core 命名空间下重新导出。

import * as z from "zod";
 
function handleError(iss: z.core.$ZodError) {
  // do stuff
}

有关核心子库内容的更多信息,请参阅 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() 方法将各种验证组合在一起。

import * as z from "zod/mini";
 
z.string().check(
  z.minLength(10),
  z.maxLength(100),
  z.toLowerCase(),
  z.trim(),
);

新增 ZodTransform

同时,transforms 已移至专用的 ZodTransform 类。这个 schema 类表示输入转换;事实上,你现在实际上可以定义独立的 transformations:

import * as z from "zod";
 
const schema = z.transform(input => String(input));
 
schema.parse(12); // => "12"

这主要与 ZodPipe 结合使用。.transform() 方法现在返回 ZodPipe 的实例。

z.string().transform(val => val); // ZodPipe<ZodString, ZodTransform>

移除 ZodPreprocess

.transform() 一样,z.preprocess() 函数现在返回 ZodPipe 实例,而不是专用的 ZodPreprocess 实例。

z.preprocess(val => val, z.string()); // ZodPipe<ZodTransform, ZodString>

移除 ZodBranded

Branding 现在通过直接修改推断类型来处理,而不是专用的 ZodBranded 类。面向用户的 API 保持不变。