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

遷移指南

本遷移指南旨在按影響程度從高到低的順序列出 Zod 4 中的重大變更。要了解有關 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 的實際問題代碼不一致(沒有 required 問題代碼)。

這些現在可以用新的 error 參數清晰地表示。

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

移除 errorMap

這被重命名為 error

錯誤映射現在也可以返回純 string(而不是 {message: string})。它們還可以返回 undefined,這告訴 Zod 將控制權交給鏈中的下一個錯誤映射。

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

ZodError

更新問題格式 (updates issue formats)

問題格式已大幅簡化。

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 // new: used for z.record/z.map 
  | z.core.$ZodIssueInvalidElement // new: used for z.map/z.set
  | z.core.$ZodIssueCustom;

以下是 Zod 3 問題列表及其 Zod 4 等效項:

import * as z from "zod"; // v3
 
export type IssueFormats =
  | z.ZodInvalidTypeIssue // ♻️ renamed to z.core.$ZodIssueInvalidType
  | z.ZodTooBigIssue  // ♻️ renamed to z.core.$ZodIssueTooBig
  | z.ZodTooSmallIssue // ♻️ renamed to z.core.$ZodIssueTooSmall
  | z.ZodInvalidStringIssue // ♻️ z.core.$ZodIssueInvalidStringFormat
  | z.ZodNotMultipleOfIssue // ♻️ renamed to z.core.$ZodIssueNotMultipleOf
  | z.ZodUnrecognizedKeysIssue // ♻️ renamed to z.core.$ZodIssueUnrecognizedKeys
  | z.ZodInvalidUnionIssue // ♻️ renamed to z.core.$ZodIssueInvalidUnion
  | z.ZodCustomIssue // ♻️ renamed to z.core.$ZodIssueCustom
  | z.ZodInvalidEnumValueIssue // ❌ merged in z.core.$ZodIssueInvalidValue
  | z.ZodInvalidLiteralIssue // ❌ merged into z.core.$ZodIssueInvalidValue
  | z.ZodInvalidUnionDiscriminatorIssue // ❌ throws an Error at schema creation time
  | z.ZodInvalidArgumentsIssue // ❌ z.function throws ZodError directly
  | z.ZodInvalidReturnTypeIssue // ❌ z.function throws ZodError directly
  | z.ZodInvalidDateIssue // ❌ merged into invalid_type
  | z.ZodInvalidIntersectionTypesIssue // ❌ removed (throws regular Error)
  | z.ZodNotFiniteIssue // ❌ infinite values no longer accepted (invalid_type)

雖然某些 Zod 4 問題類型已被合併、刪除和修改,但每個問題在結構上仍然與 Zod 3 對應項相似(在大多數情況下是相同的)。所有問題仍然符合與 Zod 3 相同的基本接口,因此大多數常見的錯誤處理邏輯將無需修改即可工作。

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

更改錯誤映射優先級 (changes error map precedence)

錯誤映射優先級已更改為更加一致。具體來說,傳遞給 .parse() 的錯誤映射 不再 優先於架構級別的錯誤映射。

const mySchema = z.string({ error: () => "Schema-level error" });
 
// in Zod 3
mySchema.parse(12, { error: () => "Contextual error" }); // => "Contextual error"
 
// in 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()

如果有必要,直接推入 err.issues 數組。

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

z.number()

no infinite values

POSITIVE_INFINITYNEGATIVE_INFINITY 不再被視為 z.number() 的有效值。

.safe() no longer accepts floats

在 Zod 3 中,z.number().safe() 已被棄用。它現在的行為與 .int() 相同(見下文)。重要的是,這意味著它不再接受浮點數。

.int() accepts safe integers only

z.number().int() API 不再接受不安全整數(超出 Number.MIN_SAFE_INTEGERNumber.MAX_SAFE_INTEGER 範圍)。使用超出此範圍的整數會導致自發的捨入誤差。(另外:您應該切換到 z.int()。)

z.string() 更新

deprecates .email() etc

字串格式現在表示為 ZodString子類,而不是簡單的內部細化。因此,這些 API 已移動到頂級 z 命名空間。頂級 API 也更簡潔,更易於 tree-shakable。

z.email();
z.uuid();
z.url();
z.emoji();         // validates a single emoji character
z.base64();
z.base64url();
z.nanoid();
z.cuid();
z.cuid2();
z.ulid();
z.ipv4();
z.ipv6();
z.cidrv4();          // ip range
z.cidrv6();          // ip range
z.iso.date();
z.iso.time();
z.iso.datetime();
z.iso.duration();

方法形式(z.string().email())仍然存在並且像以前一樣工作,但現在已被棄用。

z.string().email(); // ❌ deprecated
z.email(); // ✅ 

stricter .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

no padding in .base64url()

z.base64url()(以前是 z.string().base64url())中不再允許填充。通常,希望 base64url 字串不填充且 URL 安全。

drops z.string().ip()

這已被單獨的 .ipv4().ipv6() 方法取代。如果需要同時接受兩者,請使用 z.union() 組合它們。

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

updates z.string().ipv6()

現在使用 new URL() 構造函數進行驗證,這比舊的正則表達式方法更健壯。以前通過驗證的一些無效值現在可能會失敗。

drops 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); // should be a number
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() });

這些方法對於向後兼容性仍然可用,並且不會被刪除。它們被視為遺留方法。

棄用 .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 (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" (canonical API)
ColorSchema.Enum.Red; // ❌ removed
ColorSchema.Values.Red; // ❌ removed

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 架構。相反,它充當定義 Zod 驗證函數的獨立「函數工廠」。API 也發生了變化;您需要預先定義 inputoutput 架構,而不是使用 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 架構,請考慮 此解決方法

添加 .implementAsync()

要定義異步函數,請使用 implementAsync() 而不是 implement()

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

.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: still `unknown`

移除 ctx.path

Zod 的新解析架構不會急切地評估 path 數組。這是一個必要的更改,解鎖了 Zod 4 的巨大性能改進。

z.string().superRefine((val, ctx) => {
  ctx.path; // ❌ no longer available
});

移除函數作為第二個參數

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

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

z.ostring() 等已移除

未記錄的便捷方法 z.ostring()z.onumber() 等已被移除。這些是定義可選字串架構的簡寫方法。

z.literal()

移除 symbol 支持

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

改進枚舉支持

記錄變得更聰明了。在 Zod 3 中,將枚舉作為鍵架構傳遞給 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 交集針對兩個架構解析輸入,然後嘗試合併結果。在 Zod 3 中,當結果無法合併時,Zod 會拋出一個帶有特殊 "invalid_intersection_types" 問題的 ZodError

在 Zod 4 中,這將拋出一個常規 Error。無法合併的結果的存在表明架構存在結構問題:兩個不兼容類型的交集。因此,常規錯誤比驗證錯誤更合適。

內部變更 (Internal changes)

典型的 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 的內容也會在 z.core 命名空間下從 zodzod/mini 重新導出。

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

有關核心子庫內容的更多信息,請參閱 Zod Core 文檔。

移動 ._def

._def 屬性現在已移動到 ._zod.def。所有內部 defs 的結構可能會發生變化;這與庫作者有關,但不會在此處全面記錄。

移除 ZodEffects

這不會影響面向用戶的 API,這是一個值得強調的內部更改。這是 Zod 如何處理 細化 的更大重組的一部分。

以前,細化和轉換都位於名為 ZodEffects 的包裝類中。這意味著將任一項添加到架構中都會將原始架構包裝在 ZodEffects 實例中。在 Zod 4 中,細化現在位於架構本身內部。更準確地說,每個架構都包含一個「檢查」數組;「檢查」的概念在 Zod 4 中是新的,它將細化的概念概括為包括潛在的副作用轉換,如 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

與此同時,轉換已移動到專用的 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 保持不變。