💎 Zod 4 现已稳定发布! 阅读公告。
Zod logo

迁移指南

Edit this page

本迁移指南旨在按照影响程度由高到低,列出 Zod 4 中的破坏性更改。要了解有关 Zod 4 性能提升和新功能的更多信息,请阅读 介绍文章

npm install zod@^4.0.0

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

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

非官方代码迁移工具 — 社区维护了一个从 Zod v3 升级至 v4 的代码转换工具 zod-v3-to-v4 可供使用。

错误定制

Zod 4 统一整合了错误定制的 API,统一使用单一的 error 参数。之前 Zod 的错误定制 API 分散且不一致,Zod 4 中已清理优化。

废弃 message

error 取代 message。虽然 message 参数仍受支持,但已废弃。

z.string().min(5, { error: "太短了。" });

移除 invalid_type_errorrequired_error

invalid_type_error / required_error 参数被移除。它们几年前匆忙加入,作为比 errorMap 更简洁的错误定制方式,但存在诸多陷阱(不能与 errorMap 同时使用),且与 Zod 实际的问题代码不匹配(不存在 required 问题代码)。

这些情况现在可用新的 error 参数清晰表达。

z.string({ 
  error: (issue) => issue.input === undefined 
    ? "这是必填字段" 
    : "不是字符串" 
});

移除 errorMap

该参数名改为 error

错误映射函数现在也可以直接返回普通字符串(而非 {message: string} 对象)。它们也可以返回 undefined,指示 Zod 转交给链中下一个错误映射处理。

z.string().min(5, {
  error: (issue) => {
    if (issue.code === "too_small") {
      return `值必须大于 ${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 创建时抛出 Error
  | 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;
}

错误映射优先级变化

错误映射的优先级调整得更一致。具体来说,传递给 .parse() 的错误映射 不再 高于 schema 级别的错误映射。

const mySchema = z.string({ error: () => "Schema 级别错误" });
 
// 在 Zod 3 中
mySchema.parse(12, { error: () => "上下文错误" }); // => "上下文错误"
 
// 在 Zod 4 中
mySchema.parse(12, { error: () => "上下文错误" }); // => "Schema 级别错误"

废弃 .format()

ZodError.format() 方法已废弃。请改用顶层的 z.treeifyError() 函数。更多信息请参阅 错误格式化文档

废弃 .flatten()

ZodError.flatten() 方法同样废弃。请改用顶层的 z.treeifyError() 函数。更多信息请参阅 错误格式化文档

移除 .formErrors

此 API 与 .flatten() 完全相同,历史遗留且未记录。

废弃 .addIssue().addIssues()

如有必要,请直接向 err.issues 数组推入新问题。

myError.issues.push({ 
  // 新问题
});

z.number()

不再支持无限值

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

.safe() 不再接受浮点数

在 Zod 3 中,z.number().safe() 已废弃。现在它的行为与 .int() 相同(参见下文),重要的是它不再接受浮点数。

.int() 只接受安全整数

z.number().int() 不再接受不安全整数(超出 Number.MIN_SAFE_INTEGERNumber.MAX_SAFE_INTEGER 范围)。使用超出该范围的整数会引发自动舍入错误。(此外,你应该改用 z.int()。)

z.string() 更新

废弃 .email() 等方法

字符串格式现在表示为 ZodString子类,而非简单的内部 refinement。因此,这些 API 已移动到顶级 z 命名空间。顶级 API 也更简洁、更支持 Tree-shaking。

z.email();
z.uuid();
z.url();
z.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 compliant UUID
z.guid(); // any 8-4-4-4-12 hex pattern

.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 模式的输入类型现在为 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 会解析默认值,而非短路。因此默认值必须赋给 输入类型

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

为重现旧行为,Zod 提供了 .prefault() 新 API,意为“预解析默认值”。

// 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() });

这些方法仍可用以兼容旧代码,且不会被移除,被视为遗留 API。

废弃 .strip()

该方法本身价值不大,且是 z.object() 的默认行为。若需将严格对象转换为“普通”对象,使用 z.object(A.shape) 即可。

移除 .nonstrict()

此旧别名对应 .strip(),早已废弃,现在移除。

移除 .deepPartial()

在 Zod 3 中早已废弃,Zod 4 完全移除。无直接替代方案。其实现存在很多陷阱,且一般被视为反模式。

修改 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 (deprecated)
const ExtendedSchema = BaseSchema.merge(AdditionalSchema);
 
// .extend (recommended)
const ExtendedSchema = BaseSchema.extend(AdditionalSchema.shape);
 
// or use destructuring (best tsc performance)
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,请先 await 后再使用 Zod 解析。

若你用 z.promise 配合 z.function() 定义异步函数,也不再需要,详见下面的 ZodFunction 部分。

z.function()

z.function() 的结果不再是 Zod 模式,而是独立的“函数工厂”,用于定义经过 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}, 你 ${input.age} 岁了。`;
});

如果你迫切需要一个拥有函数类型的 Zod 模式,可以考虑此解决方案

新增 .implementAsync()

定义异步函数时,使用 implementAsync() 替代 implement()

myFunction.implementAsync(async (input) => {
  return `Hello ${input.name}, 你 ${input.age} 岁了。`;
});

.refine()

忽略类型谓词

在 Zod 3 中,作为校验函数的 类型谓词 可缩小模式类型。此行为未记录但曾有讨论。现已取消。

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} 超过10个字符` })
);

z.ostring() 等已删除

未记录的便利方法 z.ostring()z.onumber() 等已被移除。这些是用于定义可选字符串模式的简写方法。

z.literal()

移除 symbol 支持

符号不是字面量值,且不能简单用 === 比较。这是 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()); // ✅

增强枚举支持

Record 类型更智能。Zod 3 中传入枚举作为 key 类型时,推断为部分类型:

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 交叉类型先将输入解析为两个模式,然后尝试合并结果。Zod 3 中合并失败时,抛出带有特殊 "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。输入默认值由原来的 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 与 Zod Mini 之间的代码共享,诸多实用函数与类型已迁移至新的 zod/v4/core 子包中。

import * as z from "zod/v4/core";
 
function handleError(iss: z.$ZodError) {
  // 处理错误
}

For convenience, the contents of zod/v4/core are also re-exported from zod and zod/mini under the z.core namespace.

import * as z from "zod";
 
function handleError(iss: z.core.$ZodError) {
  // 处理错误
}

详见 Zod Core 文档了解核心子库内容。

移动 ._def

._def 属性现在移动到 ._zod.def。所有内部定义结构可能会变,主要针对库作者,无详尽说明。

移除 ZodEffects

这不影响用户 API,但值得强调,是 Zod 如何处理 refinements(细化校验)的重大内部重构。

之前,修饰和转换都存在于一个名为 ZodEffects 的包装类中。这意味着将任一项添加到模式中会将原始模式包装在一个 ZodEffects 实例中。在 Zod 4 中,修饰现在直接存在于模式内部。更准确地说,每个模式包含一个“检查”的数组;“检查”的概念在 Zod 4 中是新的,并且将修饰的概念推广到包括可能有副作用的转换,例如 z.toLowerCase()

这在 Zod Mini API 中尤为明显,该 API 强烈依赖于 .check() 方法来组合各种验证。

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

新增 ZodTransform

同时,转换移入专门的 ZodTransform 类。此模式表示输入转换;实际上,你可以定义独立转换:

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

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