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

定義架構

要驗證數據,您必須首先定義一個 架構。架構代表 類型,從簡單的原始值到復雜的嵌套對象和數組。

原始類型

import * as z from "zod";
 
// 原始類型
z.string();
z.number();
z.bigint();
z.boolean();
z.symbol();
z.undefined();
z.null();

強制轉換 (Coercion)

要將輸入數據強制轉換為適當的類型,請改用 z.coerce

z.coerce.string();    // String(input)
z.coerce.number();    // Number(input)
z.coerce.boolean();   // Boolean(input)
z.coerce.bigint();    // BigInt(input)

這些架構的強制轉換變體嘗試將輸入值轉換為適當的類型。

const schema = z.coerce.string();
 
schema.parse("tuna");    // => "tuna"
schema.parse(42);        // => "42"
schema.parse(true);      // => "true"
schema.parse(null);      // => "null"

默認情況下,這些強制轉換架構的輸入類型是 unknown。要指定更具體的輸入類型,請傳遞一個泛型參數:

const A = z.coerce.number();
type AInput = z.input<typeof A>; // => unknown
 
const B = z.coerce.number<number>();
type BInput = z.input<typeof B>; // => number

字面量 (Literals)

字面量架構代表 字面量類型,如 "hello world"5

const tuna = z.literal("tuna");
const twelve = z.literal(12);
const twobig = z.literal(2n);
const tru = z.literal(true);

要表示 JavaScript 字面量 nullundefined

z.null();
z.undefined();
z.void(); // 等同於 z.undefined()

允許使用多個字面量值:

const colors = z.literal(["red", "green", "blue"]);
 
colors.parse("green"); // ✅
colors.parse("yellow"); // ❌

要從字面量架構中提取允許值的集合:

colors.values; // => Set<"red" | "green" | "blue">

字串 (Strings)

Zod 提供了一些內置的字串驗證和轉換 API。要執行一些常見的字串驗證:

z.string().max(5);
z.string().min(5);
z.string().length(5);
z.string().regex(/^[a-z]+$/);
z.string().startsWith("aaa");
z.string().endsWith("zzz");
z.string().includes("---");
z.string().uppercase();
z.string().lowercase();

要執行一些簡單的字串轉換:

z.string().trim(); // 去除空白
z.string().toLowerCase(); // 轉換為小寫
z.string().toUpperCase(); // 轉換為大寫
z.string().normalize(); // 規範化 unicode 字符

字串格式 (String formats)

要驗證一些常見的字串格式:

z.email();
z.uuid();
z.url();
z.httpUrl();       // 僅限 http 或 https URL
z.hostname();
z.emoji();         // 驗證單個表情符號字符
z.base64();
z.base64url();
z.hex();
z.jwt();
z.nanoid();
z.cuid();
z.cuid2();
z.ulid();
z.ipv4();
z.ipv6();
z.mac();
z.cidrv4();        // ipv4 CIDR 塊
z.cidrv6();        // ipv6 CIDR 塊
z.hash("sha256");  // 或 "sha1", "sha384", "sha512", "md5"
z.iso.date();
z.iso.time();
z.iso.datetime();
z.iso.duration();

電子郵件 (Emails)

要驗證電子郵件地址:

z.email();

默認情況下,Zod 使用相對嚴格的電子郵件正則表達式,旨在驗證包含常見字符的普通電子郵件地址。它大致相當於 Gmail 強制執行的規則。要了解有關此正則表達式的更多信息,請參閱 這篇文章

/^(?!\.)(?!.*\.\.)([a-z0-9_'+\-\.]*)[a-z0-9_+-]@([a-z0-9][a-z0-9\-]*\.)+[a-z]{2,}$/i

要自定義電子郵件驗證行為,您可以將自定義正則表達式傳遞給 pattern 參數。

z.email({ pattern: /your regex here/ });

Zod 導出了幾個您可能有用的正則表達式。

// Zod 的默認電子郵件正則表達式
z.email();
z.email({ pattern: z.regexes.email }); // 等效
 
// 瀏覽器用於驗證 input[type=email] 字段的正則表達式
// https://developer.mozilla.org/en-US/docs/Web/HTML/Element/input/email
z.email({ pattern: z.regexes.html5Email });
 
// 經典的 emailregex.com 正則表達式 (RFC 5322)
z.email({ pattern: z.regexes.rfc5322Email });
 
// 允許 Unicode 的寬鬆正則表達式(適用於國際電子郵件)
z.email({ pattern: z.regexes.unicodeEmail });

UUIDs

要驗證 UUID:

z.uuid();

要指定特定的 UUID 版本:

// 支持 "v1", "v2", "v3", "v4", "v5", "v6", "v7", "v8"
z.uuid({ version: "v4" });
 
// 為了方便起見
z.uuidv4();
z.uuidv6();
z.uuidv7();

RFC 9562/4122 UUID 規範要求第 8 個字節的前兩位為 10。其他類似 UUID 的標識符不強制執行此約束。要驗證任何類似 UUID 的標識符:

z.guid();

URLs

要驗證任何兼容 WHATWG 的 URL:

const schema = z.url();
 
schema.parse("https://example.com"); // ✅
schema.parse("http://localhost"); // ✅
schema.parse("mailto:[email protected]"); // ✅

正如你所看到的,這是相當寬鬆的。在內部,它使用 new URL() 構造函數來驗證輸入;這種行為可能因平台和運行時而異,但它是驗證任何給定 JS 運行時/引擎上的 URI/URL 的最嚴格方法。

要針對特定正則表達式驗證主機名:

const schema = z.url({ hostname: /^example\.com$/ });
 
schema.parse("https://example.com"); // ✅
schema.parse("https://zombo.com"); // ❌

要針對特定正則表達式驗證協議,請使用 protocol 參數。

const schema = z.url({ protocol: /^https$/ });
 
schema.parse("https://example.com"); // ✅
schema.parse("http://example.com"); // ❌

Web URLs — 在許多情況下,您會希望專門驗證 Web URL。這是推薦的架構:

const httpUrl = z.url({
  protocol: /^https?$/,
  hostname: z.regexes.domain
});

這將協議限制為 http/https,並確保主機名是用於 z.regexes.domain 正則表達式的有效域名:

/^([a-zA-Z0-9](?:[a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?\.)+[a-zA-Z]{2,}$/

要規範化 URL,請使用 normalize 標誌。這將用 new URL() 返回的 規範化 URL 覆蓋輸入值。

new URL("HTTP://ExAmPle.com:80/./a/../b?X=1#f oo").href
// => "http://example.com/b?X=1#f%20oo"

ISO 日期時間 (ISO datetimes)

您可能已經注意到,Zod 字串包含一些與日期/時間相關的驗證。這些驗證是基於正則表達式的,因此它們不如完整的日期/時間庫嚴格。但是,它們對於驗證用戶輸入非常方便。

z.iso.datetime() 方法強制執行 ISO 8601;默認情況下,不允許時區偏移:

const datetime = z.iso.datetime();
 
datetime.parse("2020-01-01T06:15:00Z"); // ✅
datetime.parse("2020-01-01T06:15:00.123Z"); // ✅
datetime.parse("2020-01-01T06:15:00.123456Z"); // ✅ (arbitrary precision)
datetime.parse("2020-01-01T06:15:00+02:00"); // ❌ (offsets not allowed)
datetime.parse("2020-01-01T06:15:00"); // ❌ (local not allowed)

要允許時區偏移:

const datetime = z.iso.datetime({ offset: true });
 
// 允許時區偏移
datetime.parse("2020-01-01T06:15:00+02:00"); // ✅
 
// 基本偏移不允許
datetime.parse("2020-01-01T06:15:00+02");    // ❌
datetime.parse("2020-01-01T06:15:00+0200");  // ❌
 
// Z 仍然支持
datetime.parse("2020-01-01T06:15:00Z"); // ✅ 

要允許不合格(無時區)的日期時間:

const schema = z.iso.datetime({ local: true });
schema.parse("2020-01-01T06:15:01"); // ✅
schema.parse("2020-01-01T06:15"); // ✅ 秒數可選

要限制允許的時間 precision(精度)。默認情況下,秒數是可選的,並且允許任意亞秒精度。

const a = z.iso.datetime();
a.parse("2020-01-01T06:15Z"); // ✅
a.parse("2020-01-01T06:15:00Z"); // ✅
a.parse("2020-01-01T06:15:00.123Z"); // ✅
 
const b = z.iso.datetime({ precision: -1 }); // 分鐘精度(無秒)
b.parse("2020-01-01T06:15Z"); // ✅
b.parse("2020-01-01T06:15:00Z"); // ❌
b.parse("2020-01-01T06:15:00.123Z"); // ❌
 
const c = z.iso.datetime({ precision: 0 }); // 僅秒精度
c.parse("2020-01-01T06:15Z"); // ❌
c.parse("2020-01-01T06:15:00Z"); // ✅
c.parse("2020-01-01T06:15:00.123Z"); // ❌
 
const d = z.iso.datetime({ precision: 3 }); // 僅毫秒精度
d.parse("2020-01-01T06:15Z"); // ❌
d.parse("2020-01-01T06:15:00Z"); // ❌
d.parse("2020-01-01T06:15:00.123Z"); // ✅

ISO 日期 (ISO dates)

z.iso.date() 方法驗證格式為 YYYY-MM-DD 的字串。

const date = z.iso.date();
 
date.parse("2020-01-01"); // ✅
date.parse("2020-1-1"); // ❌
date.parse("2020-01-32"); // ❌

ISO 時間 (ISO times)

z.iso.time() 方法驗證格式為 HH:MM[:SS[.s+]] 的字串。默認情況下秒數是可選的,亞秒小數也是如此。

const time = z.iso.time();
 
time.parse("03:15"); // ✅
time.parse("03:15:00"); // ✅
time.parse("03:15:00.9999999"); // ✅ (任意精度)

不允許任何類型的偏移量。

time.parse("03:15:00Z"); // ❌ (不允許 `Z`)
time.parse("03:15:00+02:00"); // ❌ (不允許偏移)

使用 precision 參數限制允許的小數精度。

z.iso.time({ precision: -1 }); // HH:MM (分鐘精度)
z.iso.time({ precision: 0 });  // HH:MM:SS (秒精度)
z.iso.time({ precision: 1 });  // HH:MM:SS.s (十分之一秒精度)
z.iso.time({ precision: 2 });  // HH:MM:SS.ss (百分之一秒精度)
z.iso.time({ precision: 3 });  // HH:MM:SS.sss (毫秒精度)

IP 地址 (IP addresses)

const ipv4 = z.ipv4();
ipv4.parse("192.168.0.0"); // ✅
 
const ipv6 = z.ipv6();
ipv6.parse("2001:db8:85a3::8a2e:370:7334"); // ✅

IP 塊 (CIDR)

使用 CIDR 表示法 驗證 IP 地址範圍。

const cidrv4 = z.cidrv4();
cidrv4.parse("192.168.0.0/24"); // ✅
 
const cidrv6 = z.cidrv6();
cidrv6.parse("2001:db8::/32"); // ✅

MAC 地址 (MAC Addresses)

驗證標準 48 位 MAC 地址 IEEE 802

const mac = z.mac(); 
mac.parse("00:1A:2B:3C:4D:5E");  // ✅
mac.parse("00-1a-2b-3c-4d-5e");  // ❌ 默認以冒號分隔
mac.parse("001A:2B3C:4D5E");     // ❌ 僅限標準格式
mac.parse("00:1A:2b:3C:4d:5E");  // ❌ 禁止大小寫混合
 
// 自定義分隔符
const dashMac = z.mac({ delimiter: "-" });
dashMac.parse("00-1A-2B-3C-4D-5E"); // ✅

JWTs

驗證 JSON Web Tokens

z.jwt();
z.jwt({ alg: "HS256" });

哈希 (Hashes)

要驗證加密哈希值:

z.hash("md5");
z.hash("sha1");
z.hash("sha256");
z.hash("sha384");
z.hash("sha512");

默認情況下,z.hash() 像往常一樣期望十六進制編碼。您可以使用 enc 參數指定不同的編碼:

z.hash("sha256", { enc: "hex" });       // 默認
z.hash("sha256", { enc: "base64" });    // base64 編碼
z.hash("sha256", { enc: "base64url" }); // base64url 編碼(無填充)

自定義格式 (Custom formats)

要定義您自己的字串格式:

const coolId = z.stringFormat("cool-id", ()=>{
  // arbitrary validation here
  return val.length === 100 && val.startsWith("cool-");
});
 
// a regex is also accepted
z.stringFormat("cool-id", /^cool-[a-z0-9]{95}$/);

此架構將產生 "invalid_format" 問題,這比細化或 z.custom() 產生的 "custom" 錯誤更具描述性。

myFormat.parse("invalid input!");
// ZodError: [
//   {
//     "code": "invalid_format",
//     "format": "cool-id",
//     "path": [],
//     "message": "Invalid cool-id"
//   }
// ]

模板字串 (Template literals)

New — 在 [email protected] 中引入。

要定義模板字串架構:

const schema = z.templateLiteral([ "hello, ", z.string(), "!" ]);
// `hello, ${string}!`

z.templateLiteral API 可以處理任意數量的字串字面量(例如 "hello")和架構。可以傳遞任何推斷類型可分配給 string | number | bigint | boolean | null | undefined 的架構。

z.templateLiteral([ "hi there" ]);
// `hi there`
 
z.templateLiteral([ "email: ", z.string() ]);
// `email: ${string}`
 
z.templateLiteral([ "high", z.literal(5) ]);
// `high5`
 
z.templateLiteral([ z.nullable(z.literal("grassy")) ]);
// `grassy` | `null`
 
z.templateLiteral([ z.number(), z.enum(["px", "em", "rem"]) ]);
// `${number}px` | `${number}em` | `${number}rem`

數字 (Numbers)

使用 z.number() 驗證數字。它允許任何有限數字。

const schema = z.number();
 
schema.parse(3.14);      // ✅
schema.parse(NaN);       // ❌
schema.parse(Infinity);  // ❌

Zod 實現了一些特定於數字的驗證:

z.number().gt(5);
z.number().gte(5);                     // alias .min(5)
z.number().lt(5);
z.number().lte(5);                     // alias .max(5)
z.number().positive();       
z.number().nonnegative();    
z.number().negative(); 
z.number().nonpositive(); 
z.number().multipleOf(5);              // alias .step(5)

如果(由於某種原因)您想驗證 NaN,請使用 z.nan()

z.nan().parse(NaN);              // ✅
z.nan().parse("anything else");  // ❌

整數 (Integers)

要驗證整數:

z.int();     // 限制為安全整數範圍
z.int32();   // 限制為 int32 範圍

BigInts

要驗證 BigInts:

z.bigint();

Zod 包含一些特定於 bigint 的驗證。

z.bigint().gt(5n);
z.bigint().gte(5n);                    // alias `.min(5n)`
z.bigint().lt(5n);
z.bigint().lte(5n);                    // alias `.max(5n)`
z.bigint().positive(); 
z.bigint().nonnegative(); 
z.bigint().negative(); 
z.bigint().nonpositive(); 
z.bigint().multipleOf(5n);             // alias `.step(5n)`

布爾值 (Booleans)

要驗證布爾值:

z.boolean().parse(true); // => true
z.boolean().parse(false); // => false

日期 (Dates)

使用 z.date() 驗證 Date 實例。

z.date().safeParse(new Date()); // success: true
z.date().safeParse("2022-01-12T06:15:00.000Z"); // success: false

要自定義錯誤消息:

z.date({
  error: issue => issue.input === undefined ? "Required" : "Invalid date"
});

Zod 提供了一些特定於日期的驗證。

z.date().min(new Date("1900-01-01"), { error: "Too old!" });
z.date().max(new Date(), { error: "Too young!" });

枚舉 (Enums)

使用 z.enum 針對一組固定的允許 字串 值驗證輸入。

const FishEnum = z.enum(["Salmon", "Tuna", "Trout"]);
 
FishEnum.parse("Salmon"); // => "Salmon"
FishEnum.parse("Swordfish"); // => ❌

小心 — 如果您將字串數組聲明為變量,Zod 將無法正確推斷每個元素的確切值。

const fish = ["Salmon", "Tuna", "Trout"];
 
const FishEnum = z.enum(fish);
type FishEnum = z.infer<typeof FishEnum>; // string

要解決此問題,請始終將數組直接傳遞給 z.enum() 函數,或者使用 as const

const fish = ["Salmon", "Tuna", "Trout"] as const;
 
const FishEnum = z.enum(fish);
type FishEnum = z.infer<typeof FishEnum>; // "Salmon" | "Tuna" | "Trout"

支持類似枚舉的對象字面量 ({ [key: string]: string | number })。

const Fish = {
  Salmon: 0,
  Tuna: 1
} as const
 
const FishEnum = z.enum(Fish)
FishEnum.parse(Fish.Salmon); // => ✅
FishEnum.parse(0); // => ✅
FishEnum.parse(2); // => ❌

您還可以傳入外部聲明的 TypeScript 枚舉。

enum Fish {
  Salmon = 0,
  Tuna = 1
}
 
const FishEnum = z.enum(Fish);
FishEnum.parse(Fish.Salmon); // => ✅
FishEnum.parse(0); // => ✅
FishEnum.parse(2); // => ❌

Zod 4 — 這取代了 Zod 3 中的 z.nativeEnum() API。

注意,不推薦 使用 TypeScript 的 enum 關鍵字。

enum Fish {
  Salmon = "Salmon",
  Tuna = "Tuna",
  Trout = "Trout",
}
 
const FishEnum = z.enum(Fish);

.enum

要將架構的值提取為類似枚舉的對象:

const FishEnum = z.enum(["Salmon", "Tuna", "Trout"]);
 
FishEnum.enum;
// => { Salmon: "Salmon", Tuna: "Tuna", Trout: "Trout" }

.exclude()

要創建一個新的枚舉架構,排除某些值:

const FishEnum = z.enum(["Salmon", "Tuna", "Trout"]);
const TunaOnly = FishEnum.exclude(["Salmon", "Trout"]);

.extract()

要創建一個新的枚舉架構,提取某些值:

const FishEnum = z.enum(["Salmon", "Tuna", "Trout"]);
const SalmonAndTroutOnly = FishEnum.extract(["Salmon", "Trout"]);

布爾字串 (Stringbools)

💎 Zod 4 新功能

在某些情況下(例如解析環境變量),將某些字串「布爾」值解析為純 boolean 值很有價值。為了支持這一點,Zod 4 引入了 z.stringbool()

const strbool = z.stringbool();
 
strbool.parse("true")         // => true
strbool.parse("1")            // => true
strbool.parse("yes")          // => true
strbool.parse("on")           // => true
strbool.parse("y")            // => true
strbool.parse("enabled")      // => true
 
strbool.parse("false");       // => false
strbool.parse("0");           // => false
strbool.parse("no");          // => false
strbool.parse("off");         // => false
strbool.parse("n");           // => false
strbool.parse("disabled");    // => false
 
strbool.parse(/* anything else */); // ZodError<[{ code: "invalid_value" }]>

要自定義真值和假值:

// these are the defaults
z.stringbool({
  truthy: ["true", "1", "yes", "on", "y", "enabled"],
  falsy: ["false", "0", "no", "off", "n", "disabled"],
});

默認情況下,該架構是 不區分大小寫 的;在與 truthy/falsy 值進行比較之前,所有輸入都會轉換為小寫。要使其區分大小寫:

z.stringbool({
  case: "sensitive"
});

可選 (Optionals)

要使架構 可選(即允許 undefined 輸入)。

z.optional(z.literal("yoda")); // or z.literal("yoda").optional()

這將返回一個包裝原始架構的 ZodOptional 實例。要提取內部架構:

optionalYoda.unwrap(); // ZodLiteral<"yoda">

可空 (Nullables)

要使架構 可空(即允許 null 輸入)。

z.nullable(z.literal("yoda")); // or z.literal("yoda").nullable()

這將返回一個包裝原始架構的 ZodNullable 實例。要提取內部架構:

nullableYoda.unwrap(); // ZodLiteral<"yoda">

可空可選 (Nullish)

要使架構 可空可選(即既可選又可空):

const nullishYoda = z.nullish(z.literal("yoda"));

有關 nullish 概念的更多信息,請參閱 TypeScript 手冊。

Unknown

Zod 旨在與 TypeScript 的類型系統一一對應。因此,Zod 提供了用於表示以下特殊類型的 API:

// 允許任何值
z.any(); // inferred type: `any`
z.unknown(); // inferred type: `unknown`

Never

任何值都不會通過驗證。

z.never(); // inferred type: `never`

對象 (Objects)

要定義對象類型:

  // 默認情況下所有屬性都是必需的
  const Person = z.object({
    name: z.string(),
    age: z.number(),
  });
 
  type Person = z.infer<typeof Person>;
  // => { name: string; age: number; }

默認情況下,所有屬性都是必需的。要使某些屬性可選:

const Dog = z.object({
  name: z.string(),
  age: z.number().optional(),
});
 
Dog.parse({ name: "Yeller" }); // ✅

默認情況下,無法識別的鍵會從解析結果中 剝離

Dog.parse({ name: "Yeller", extraKey: true });
// => { name: "Yeller" }

z.strictObject

定義一個 嚴格 架構,當發現未知鍵時拋出錯誤:

const StrictDog = z.strictObject({
  name: z.string(),
});
 
StrictDog.parse({ name: "Yeller", extraKey: true });
// ❌ throws

z.looseObject

定義一個 寬鬆 架構,允許未知鍵通過:

const LooseDog = z.looseObject({
  name: z.string(),
});
 
LooseDog.parse({ name: "Yeller", extraKey: true });
// => { name: "Yeller", extraKey: true }

.catchall()

定義一個 捕獲所有架構,用於驗證任何無法識別的鍵:

const DogWithStrings = z.object({
  name: z.string(),
  age: z.number().optional(),
}).catchall(z.string());
 
DogWithStrings.parse({ name: "Yeller", extraKey: "extraValue" }); // ✅
DogWithStrings.parse({ name: "Yeller", extraKey: 42 }); // ❌

.shape

要訪問內部架構:

Dog.shape.name; // => string schema
Dog.shape.age; // => number schema

.keyof()

從對象架構的鍵創建 ZodEnum 架構:

const keySchema = Dog.keyof();
// => ZodEnum<["name", "age"]>

.extend()

要向對象架構添加其他字段:

const DogWithBreed = Dog.extend({
  breed: z.string(),
});

此 API 可用於覆蓋現有字段!請小心使用這種能力!如果兩個架構共享鍵,B 將覆蓋 A。

替代方案:展開語法 — 您也可以通過完全創建一個新的對象架構來完全避免 .extend()。這使得結果架構的嚴格性級別在視覺上顯而易見。

const DogWithBreed = z.object({ // or z.strictObject() or z.looseObject()...
  ...Dog.shape,
  breed: z.string(),
});

您也可以使用它一次合併多個對象。

const DogWithBreed = z.object({
  ...Animal.shape,
  ...Pet.shape,
  breed: z.string(),
});

這種方法有幾個優點:

  1. 使用語言級功能(展開語法)而不是庫特定的 API
  2. 在 Zod 和 Zod Mini 中使用相同的語法
  3. 它更 tsc 高效——.extend() 方法在大型架構上可能很昂貴,而且由於 TypeScript 限制,當鏈接調用時,它會變得非常昂貴
  4. 如果您願意,您可以使用 z.strictObject()z.looseObject() 更改結果架構的嚴格性級別

.safeExtend()

.safeExtend() 方法與 .extend() 類似,但它不允許您用不可分配的架構覆蓋現有屬性。換句話說,.safeExtend() 的結果將具有一個推斷類型,該類型 extends 原始類型(在 TypeScript 意義上)。

z.object({ a: z.string() }).safeExtend({ a: z.string().min(5) }); // ✅
z.object({ a: z.string() }).safeExtend({ a: z.any() }); // ✅
z.object({ a: z.string() }).safeExtend({ a: z.number() });
//                                       ^  ❌ ZodNumber is not assignable 

使用 .safeExtend() 擴展包含細化的架構。(在包含細化的架構上使用常規 .extend() 會拋出錯誤。)

const Base = z.object({
  a: z.string(),
  b: z.string()
}).refine(user => user.a === user.b);
 
// Extended inherits the refinements of Base
const Extended = Base.safeExtend({
  a: z.string().min(10)
});

.pick()

受 TypeScript 內置的 PickOmit 實用程序類型的啟發,Zod 提供了用於從對象架構中選取和省略某些鍵的專用 API。

從這個初始架構開始:

const Recipe = z.object({
  title: z.string(),
  description: z.string().optional(),
  ingredients: z.array(z.string()),
});
// { title: string; description?: string | undefined; ingredients: string[] }

要選取某些鍵:

const JustTheTitle = Recipe.pick({ title: true });

.omit()

要省略某些鍵:

const RecipeNoId = Recipe.omit({ id: true });

.partial()

為了方便起見,Zod 提供了一個專用 API,用於使部分或所有屬性可選,其靈感來自內置的 TypeScript 實用程序類型 Partial

要使所有字段可選:

const PartialRecipe = Recipe.partial();
// { title?: string | undefined; description?: string | undefined; ingredients?: string[] | undefined }

要使某些屬性可選:

const RecipeOptionalIngredients = Recipe.partial({
  ingredients: true,
});
// { title: string; description?: string | undefined; ingredients?: string[] | undefined }

.required()

Zod 提供了一個 API,用於使部分或所有屬性 必需,其靈感來自 TypeScript 的 Required 實用程序類型。

要使所有屬性必需:

const RequiredRecipe = Recipe.required();
// { title: string; description: string; ingredients: string[] }

要使某些屬性必需:

const RecipeRequiredDescription = Recipe.required({description: true});
// { title: string; description: string; ingredients: string[] }

遞歸對象 (Recursive objects)

要定義自引用類型,請在鍵上使用 getter。這讓 JavaScript 在運行時解析循環架構。

const Category = z.object({
  name: z.string(),
  get subcategories(){
    return z.array(Category)
  }
});
 
type Category = z.infer<typeof Category>;
// { name: string; subcategories: Category[] }

雖然支持遞歸架構,但將循環數據傳入 Zod 會導致無限循環。

您也可以表示 相互遞歸類型

const User = z.object({
  email: z.email(),
  get posts(){
    return z.array(Post)
  }
});
 
const Post = z.object({
  title: z.string(),
  get author(){
    return User
  }
});

所有對象 API(.pick().omit().required().partial() 等)都按預期工作。

循環錯誤 (Circularity errors)

由於 TypeScript 的限制,遞歸類型推斷可能很挑剔,並且僅在某些情況下有效。一些更復雜的類型可能會觸發遞歸類型錯誤,如下所示:

const Activity = z.object({
  name: z.string(),
  get subactivities() {
    // ^ ❌ 'subactivities' implicitly has return type 'any' because it does not
    // have a return type annotation and is referenced directly or indirectly
    // in one of its return expressions.ts(7023)
 
    return z.nullable(z.array(Activity));
  },
});

在這些情況下,您可以通過在違規的 getter 上添加類型註釋來解決錯誤:

const Activity = z.object({
  name: z.string(),
  get subactivities(): z.ZodNullable<z.ZodArray<typeof Activity>> {
    return z.nullable(z.array(Activity));
  },
});

數組 (Arrays)

要定義數組架構:

const stringArray = z.array(z.string()); // or z.string().array()

訪問數組元素的內部架構。

stringArray.unwrap(); // => string schema

Zod 實現了一些特定於數組的驗證:

z.array(z.string()).min(5); // must contain 5 or more items
z.array(z.string()).max(5); // must contain 5 or fewer items
z.array(z.string()).length(5); // must contain 5 items exactly

元組 (Tuples)

與數組不同,元組通常是固定長度的數組,為每個索引指定不同的架構。

const MyTuple = z.tuple([
  z.string(),
  z.number(),
  z.boolean()
]);
 
type MyTuple = z.infer<typeof MyTuple>;
// [string, number, boolean]

要添加可變參數("rest")參數:

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

聯合 (Unions)

聯合類型 (A | B) 代表邏輯「或」。Zod 聯合架構將按順序根據每個選項檢查輸入。返回第一個驗證成功的值。

const stringOrNumber = z.union([z.string(), z.number()]);
// string | number
 
stringOrNumber.parse("foo"); // passes
stringOrNumber.parse(14); // passes

要提取內部選項架構:

stringOrNumber.options; // [ZodString, ZodNumber]

判別聯合 (Discriminated unions)

判別聯合 是一種特殊的聯合,其中 a) 所有選項都是對象架構,且 b) 共享一個特定的鍵(「鑑別器」)。根據鑑別器鍵的值,TypeScript 能夠按預期「縮小」類型簽名。

type MyResult =
  | { status: "success"; data: string }
  | { status: "failed"; error: string };
 
function handleResult(result: MyResult){
  if(result.status === "success"){
    result.data; // string
  } else {
    result.error; // string
  }
}

您可以用常規的 z.union() 來表示它。但常規聯合是 天真 的——它們按順序根據每個選項檢查輸入,並返回第一個通過的選項。對於大型聯合來說,這可能很慢。

因此,Zod 提供了一個 z.discriminatedUnion() API,它使用 鑑別器鍵 來使解析更高效。

const MyResult = z.discriminatedUnion("status", [
  z.object({ status: z.literal("success"), data: z.string() }),
  z.object({ status: z.literal("failed"), error: z.string() }),
]);

每個選項都應該是一個 對象架構,其鑑別器屬性(上例中的 status)對應於某個字面量值或值集,通常是 z.enum()z.literal()z.null()z.undefined()

交叉 (Intersections)

交叉類型 (A & B) 代表邏輯「與」。

const a = z.union([z.number(), z.string()]);
const b = z.union([z.number(), z.boolean()]);
const c = z.intersection(a, b);
 
type c = z.infer<typeof c>; // => number

這對於交叉兩個對象類型很有用。

const Person = z.object({ name: z.string() });
type Person = z.infer<typeof Person>;
 
const Employee = z.object({ role: z.string() });
type Employee = z.infer<typeof Employee>;
 
const EmployedPerson = z.intersection(Person, Employee);
type EmployedPerson = z.infer<typeof EmployedPerson>;
// Person & Employee

合併對象架構時,優先使用 A.extend(B) 而不是交叉。使用 .extend() 會給您一個新的對象架構,而 z.intersection(A, B) 返回一個 ZodIntersection 實例,該實例缺少 pickomit 等常用對象方法。

記錄 (Records)

記錄架構用於驗證 Record<string, string> 等類型。

const IdCache = z.record(z.string(), z.string());
type IdCache = z.infer<typeof IdCache>; // Record<string, string>
 
IdCache.parse({
  carlotta: "77d2586b-9e8e-4ecf-8b21-ea7e0530eadd",
  jimmie: "77d2586b-9e8e-4ecf-8b21-ea7e0530eadd",
});

鍵架構可以是任何可分配給 string | number | symbol 的 Zod 架構。

const Keys = z.union([z.string(), z.number(), z.symbol()]);
const AnyObject = z.record(Keys, z.unknown());
// Record<string | number | symbol, unknown>

創建包含由枚舉定義的鍵的對象架構:

const Keys = z.enum(["id", "name", "email"]);
const Person = z.record(Keys, z.string());
// { id: string; name: string; email: string }

Zod 4 — 在 Zod 4 中,如果您將 z.enum 作為第一個參數傳遞給 z.record(),Zod 將詳盡地檢查所有枚舉值是否作為鍵存在於輸入中。此行為與 TypeScript 一致:

type MyRecord = Record<"a" | "b", string>;
const myRecord: MyRecord = { a: "foo", b: "bar" }; // ✅
const myRecord: MyRecord = { a: "foo" }; // ❌ 缺少必需鍵 `b`

在 Zod 3 中,未檢查詳盡性。要複製舊行為,請使用 z.partialRecord()

如果您想要一個 partial(部分)記錄類型,請使用 z.partialRecord()。這會跳過 Zod 通常使用 z.enum()z.literal() 鍵架構運行的特殊詳盡性檢查。

const Keys = z.enum(["id", "name", "email"]).or(z.never()); 
const Person = z.partialRecord(Keys, z.string());
// { id?: string; name?: string; email?: string }

Maps

const StringNumberMap = z.map(z.string(), z.number());
type StringNumberMap = z.infer<typeof StringNumberMap>; // Map<string, number>
 
const myMap: StringNumberMap = new Map();
myMap.set("one", 1);
myMap.set("two", 2);
 
StringNumberMap.parse(myMap);

Sets

const NumberSet = z.set(z.number());
type NumberSet = z.infer<typeof NumberSet>; // Set<number>
 
const mySet: NumberSet = new Set();
mySet.add(1);
mySet.add(2);
NumberSet.parse(mySet);

可以使用以下實用程序方法進一步約束 Set 架構。

z.set(z.string()).min(5); // must contain 5 or more items
z.set(z.string()).max(5); // must contain 5 or fewer items
z.set(z.string()).size(5); // must contain 5 items exactly

文件 (Files)

要驗證 File 實例:

const fileSchema = z.file();
 
fileSchema.min(10_000); // minimum .size (bytes)
fileSchema.max(1_000_000); // maximum .size (bytes)
fileSchema.mime("image/png"); // MIME type
fileSchema.mime(["image/png", "image/jpeg"]); // multiple MIME types

Promises

已棄用z.promise() 在 Zod 4 中已棄用。Promise 架構的有效用例極少。如果您懷疑某個值可能是 Promise,只需在用 Zod 解析之前 await 它。

Instanceof

您可以使用 z.instanceof 來檢查輸入是否為類的實例。這對於驗證來自第三方庫的導出類很有用。

class Test {
  name: string;
}
 
const TestSchema = z.instanceof(Test);
 
TestSchema.parse(new Test()); // ✅
TestSchema.parse("whatever"); // ❌

屬性 (Property)

要針對 Zod 架構驗證類實例的特定屬性:

const blobSchema = z.instanceof(URL).check(
  z.property("protocol", z.literal("https:" as string, "Only HTTPS allowed"))
);
 
blobSchema.parse(new URL("https://example.com")); // ✅
blobSchema.parse(new URL("http://example.com")); // ❌

z.property() API 適用於任何數據類型(但在與 z.instanceof() 結合使用時最有用)。

const blobSchema = z.string().check(
  z.property("length", z.number().min(10))
);
 
blobSchema.parse("hello there!"); // ✅
blobSchema.parse("hello."); // ❌

細化 (Refinements)

每個 Zod 架構都存儲一個 細化 數組。細化是一種執行 Zod 尚未提供本機 API 的自定義驗證的方法。

.refine()

const myString = z.string().refine((val) => val.length <= 255);

細化函數不應拋出異常。相反,它們應該返回一個 falsy 值來表示失敗。拋出的錯誤不會被 Zod 捕獲。

error

要自定義錯誤消息:

const myString = z.string().refine((val) => val.length > 8, { 
  error: "Too short!" 
});

abort

默認情況下,來自檢查的驗證問題被認為是 可繼續的;也就是說,Zod 將按順序執行 所有 檢查,即使其中一個導致驗證錯誤。這通常是可取的,因為這意味著 Zod 可以一次顯示儘可能多的錯誤。

const myString = z.string()
  .refine((val) => val.length > 8, { error: "Too short!" })
  .refine((val) => val === val.toLowerCase(), { error: "Must be lowercase" });
  
 
const result = myString.safeParse("OH NO");
result.error?.issues;
/* [
  { "code": "custom", "message": "Too short!" },
  { "code": "custom", "message": "Must be lowercase" }
] */

要將特定細化標記為 不可繼續,請使用 abort 參數。如果檢查失敗,驗證將終止。

const myString = z.string()
  .refine((val) => val.length > 8, { error: "Too short!", abort: true })
  .refine((val) => val === val.toLowerCase(), { error: "Must be lowercase", abort: true });
 
 
const result = myString.safeParse("OH NO");
result.error?.issues;
// => [{ "code": "custom", "message": "Too short!" }]

path

要自定義錯誤路徑,請使用 path 參數。這通常僅在對象架構的上下文中有用。

const passwordForm = z
  .object({
    password: z.string(),
    confirm: z.string(),
  })
  .refine((data) => data.password === data.confirm, {
    message: "Passwords don't match",
    path: ["confirm"], // path of error
  });

這將在關聯問題中設置 path 參數:

const result = passwordForm.safeParse({ password: "asdf", confirm: "qwer" });
result.error.issues;
/* [{
  "code": "custom",
  "path": [ "confirm" ],
  "message": "Passwords don't match"
}] */

要定義異步細化,只需傳遞一個 async 函數:

const userId = z.string().refine(async (id) => {
  // verify that ID exists in database
  return true;
});

如果您使用異步細化,則必須使用 .parseAsync 方法來解析數據!否則 Zod 會拋出錯誤。

const result = await userId.parseAsync("abc123");

when

注意 — 這是一個高級用戶功能,絕對可能被濫用,從而增加源自細化內部的未捕獲錯誤的可能性。

默認情況下,如果已經遇到任何 不可繼續 的問題,則細化不會運行。Zod 會在將值傳遞給任何細化函數之前,確保值的類型簽名是正確的。

const schema = z.string().refine((val) => {
  return val.length > 8
});
 
schema.parse(1234); // invalid_type: refinement won't be executed

在某些情況下,您希望更精細地控制細化何時運行。例如,考慮這個「密碼確認」檢查:

const schema = z
  .object({
    password: z.string().min(8),
    confirmPassword: z.string(),
    anotherField: z.string(),
  })
  .refine((data) => data.password === data.confirmPassword, {
    message: "Passwords do not match",
    path: ["confirmPassword"],
  });
 
schema.parse({
  password: "asdf",
  confirmPassword: "asdf",
  anotherField: 1234 // ❌ this error will prevent the password check from running
});

anotherField 上的錯誤將阻止執行密碼確認檢查,即使該檢查不依賴於 anotherField。要控制細化何時運行,請使用 when 參數:

const schema = z
  .object({
    password: z.string().min(8),
    confirmPassword: z.string(),
    anotherField: z.string(),
  })
  .refine((data) => data.password === data.confirmPassword, {
    message: "Passwords do not match",
    path: ["confirmPassword"],
 
    // 如果 password 和 confirmPassword 有效則運行
    when(payload) { 
      return schema 
        .pick({ password: true, confirmPassword: true }) 
        .safeParse(payload.value).success; 
    },  
  });
 
schema.parse({
  password: "asdf",
  confirmPassword: "asdf",
  anotherField: 1234 // ❌ this error will not prevent the password check from running
});

.superRefine()

常規的 .refine API 僅生成一個帶有 "custom" 錯誤代碼的問題,但 .superRefine() 可以使用任何 Zod 的 內部問題類型 創建多個問題。

const UniqueStringArray = z.array(z.string()).superRefine((val, ctx) => {
  if (val.length > 3) {
    ctx.addIssue({
      code: "too_big",
      maximum: 3,
      origin: "array",
      inclusive: true,
      message: "Too many items 😡",
      input: val,
    });
  }
 
  if (val.length !== new Set(val).size) {
    ctx.addIssue({
      code: "custom",
      message: `No duplicates allowed.`,
      input: val,
    });
  }
});
 

.check()

注意.check() API 是一個更低級的 API,通常比 .superRefine() 更複雜。在對性能敏感的代碼路徑中,它可能更快,但也更為冗長。

編解碼器 (Codecs)

New — 在 Zod 4.1 中引入。有關更多信息,請參閱專用的 編解碼器 頁面。

編解碼器是一種特殊的架構,它在其他兩個架構之間實現 雙向轉換

const stringToDate = z.codec(
  z.iso.datetime(),  // input schema: ISO date string
  z.date(),          // output schema: Date object
  {
    decode: (isoString) => new Date(isoString), // ISO string → Date
    encode: (date) => date.toISOString(),       // Date → ISO string
  }
);

常規的 .parse() 操作執行 前向轉換。它調用編解碼器的 decode 函數。

stringToDate.parse("2024-01-15T10:30:00.000Z"); // => Date

您也可以使用頂級 z.decode() 函數。與 .parse()(接受 unknown 輸入)不同,z.decode() 期望強類型的輸入(本例中為 string)。

z.decode(stringToDate, "2024-01-15T10:30:00.000Z"); // => Date

要執行 反向轉換,請使用逆操作:z.encode()

z.encode(stringToDate, new Date("2024-01-15")); // => "2024-01-15T00:00:00.000Z"

有關更多信息,請參閱專用的 編解碼器 頁面。該頁面包含常用編解碼器的實現,您可以將其複製/粘貼到您的項目中:

管道 (Pipes)

架構可以鏈接在一起形成「管道」。管道在與 轉換 結合使用時主要很有用。

const stringToLength = z.string().pipe(z.transform(val => val.length));
 
stringToLength.parse("hello"); // => 5

轉換 (Transforms)

注意 — 對於雙向轉換,請使用 編解碼器

轉換是一種特殊的架構,它執行單向轉換。它們不驗證輸入,而是接受任何內容並對數據執行某些轉換。要定義轉換:

const castToString = z.transform((val) => String(val));
 
castToString.parse("asdf"); // => "asdf"
castToString.parse(123); // => "123"
castToString.parse(true); // => "true"

轉換函數不應拋出異常。拋出的錯誤不會被 Zod 捕獲。

要在轉換內部執行驗證邏輯,請使用 ctx。要報告驗證問題,請將新問題推送到 ctx.issues(類似於 .check() API)。

const coercedInt = z.transform((val, ctx) => {
  try {
    const parsed = Number.parseInt(String(val));
    return parsed;
  } catch (e) {
    ctx.issues.push({
      code: "custom",
      message: "Not a number",
      input: val,
    });
 
    // this is a special constant with type `never`
    // returning it lets you exit the transform without impacting the inferred return type
    return z.NEVER;
  }
});

最常見的是,轉換與 管道 結合使用。這種組合對於執行一些初始驗證,然後將解析後的數據轉換為另一種形式很有用。

const stringToLength = z.string().pipe(z.transform(val => val.length));
 
stringToLength.parse("hello"); // => 5

.transform()

將一些架構通過管道傳輸到轉換是一種常見模式,因此 Zod 提供了一個方便的 .transform() 方法。

const stringToLength = z.string().transform(val => val.length); 

轉換也可以是異步的:

const idToUser = z
  .string()
  .transform(async (id) => {
    // fetch user from database
    return db.getUserById(id); 
  });
 
const user = await idToUser.parseAsync("abc123");

如果您使用異步轉換,則在解析數據時必須使用 .parseAsync.safeParseAsync!否則 Zod 會拋出錯誤。

.preprocess()

將轉換通過管道傳輸到另一個架構是另一種常見模式,因此 Zod 提供了一個方便的 z.preprocess() 函數。

const coercedInt = z.preprocess((val) => {
  if (typeof val === "string") {
    return Number.parseInt(val);
  }
  return val;
}, z.int());

默認值 (Defaults)

要為架構設置默認值:

const defaultTuna = z.string().default("tuna");
 
defaultTuna.parse(undefined); // => "tuna"

或者,您可以傳遞一個函數,該函數將在需要生成默認值時重新執行:

const randomDefault = z.number().default(Math.random);
 
randomDefault.parse(undefined);    // => 0.4413456736055323
randomDefault.parse(undefined);    // => 0.1871840107401901
randomDefault.parse(undefined);    // => 0.7223408162401552

Prefaults

在 Zod 中,設置 default(默認)值將使解析過程短路。如果輸入是 undefined,則急切地返回默認值。因此,默認值必須可分配給架構的 輸出類型

const schema = z.string().transform(val => val.length).default(0);
schema.parse(undefined); // => 0

有時,定義 prefault(「預解析默認」)值很有用。如果輸入是 undefined,則將解析 prefault 值。解析過程 會短路。因此,prefault 值必須可分配給架構的 輸入類型

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

如果您想將某些輸入值傳遞給一些變異細化,這也很有用。

const a = z.string().trim().toUpperCase().prefault("  tuna  ");
a.parse(undefined); // => "TUNA"
 
const b = z.string().trim().toUpperCase().default("  tuna  ");
b.parse(undefined); // => "  tuna  "

捕獲 (Catch)

使用 .catch() 定義在發生驗證錯誤時返回的後備值:

const numberWithCatch = z.number().catch(42);
 
numberWithCatch.parse(5); // => 5
numberWithCatch.parse("tuna"); // => 42

或者,您可以傳遞一個函數,該函數將在需要生成捕獲值時重新執行。

const numberWithRandomCatch = z.number().catch((ctx) => {
  ctx.error; // the caught ZodError
  return Math.random();
});
 
numberWithRandomCatch.parse("sup"); // => 0.4413456736055323
numberWithRandomCatch.parse("sup"); // => 0.1871840107401901
numberWithRandomCatch.parse("sup"); // => 0.7223408162401552

品牌類型 (Branded types)

TypeScript 的類型系統是 結構化的,這意味著結構上等效的兩種類型被認為是相同的。

type Cat = { name: string };
type Dog = { name: string };
 
const pluto: Dog = { name: "pluto" };
const simba: Cat = pluto; // works fine

在某些情況下,在 TypeScript 內部模擬 名義類型 可能是可取的。這可以通過 品牌類型(也稱為「不透明類型」)來實現。

const Cat = z.object({ name: z.string() }).brand<"Cat">();
const Dog = z.object({ name: z.string() }).brand<"Dog">();
 
type Cat = z.infer<typeof Cat>; // { name: string } & z.$brand<"Cat">
type Dog = z.infer<typeof Dog>; // { name: string } & z.$brand<"Dog">
 
const pluto = Dog.parse({ name: "pluto" });
const simba: Cat = pluto; // ❌ not allowed

在後台,這是通過將「品牌」附加到架構的推斷類型來工作的。

const Cat = z.object({ name: z.string() }).brand<"Cat">();
type Cat = z.infer<typeof Cat>; // { name: string } & z.$brand<"Cat">

有了這個品牌,任何普通(無品牌)數據結構都不再可分配給推斷類型。您必須使用架構解析一些數據以獲取品牌數據。

請注意,品牌類型不會影響 .parse 的運行時結果。這是一個僅靜態的構造。

只讀 (Readonly)

要將架構標記為只讀:

const ReadonlyUser = z.object({ name: z.string() }).readonly();
type ReadonlyUser = z.infer<typeof ReadonlyUser>;
// Readonly<{ name: string }>

新架構的推斷類型將被標記為 readonly。請注意,在 TypeScript 中,這僅影響對象、數組、元組、 SetMap

z.object({ name: z.string() }).readonly(); // { readonly name: string }
z.array(z.string()).readonly(); // readonly string[]
z.tuple([z.string(), z.number()]).readonly(); // readonly [string, number]
z.map(z.string(), z.date()).readonly(); // ReadonlyMap<string, Date>
z.set(z.string()).readonly(); // ReadonlySet<string>

輸入將像往常一樣被解析,然後結果將被 Object.freeze() 凍結以防止修改。

const result = ReadonlyUser.parse({ name: "fido" });
result.name = "simba"; // throws TypeError

JSON

要驗證任何可 JSON 編碼的值:

const jsonSchema = z.json();

這是一個方便的 API,它返回以下聯合架構:

const jsonSchema = z.lazy(() => {
  return z.union([
    z.string(params), 
    z.number(), 
    z.boolean(), 
    z.null(), 
    z.array(jsonSchema), 
    z.record(z.string(), jsonSchema)
  ]);
});

函數 (Functions)

Zod 提供了一個 z.function() 實用程序,用於定義 Zod 驗證的函數。這樣,您可以避免將驗證代碼與業務邏輯混合。

const MyFunction = z.function({
  input: [z.string()], // parameters (must be an array or a ZodTuple)
  output: z.number()  // return type
});
 
type MyFunction = z.infer<typeof MyFunction>;
// (input: string) => number

函數架構有一個 .implement() 方法,它接受一個函數並返回一個自動驗證其輸入和輸出的新函數。

const computeTrimmedLength = MyFunction.implement((input) => {
  // TypeScript knows input is a string!
  return input.trim().length;
});
 
computeTrimmedLength("sandwich"); // => 8
computeTrimmedLength(" asdf "); // => 4

如果輸入無效,此函數將拋出 ZodError

computeTrimmedLength(42); // throws ZodError

如果您只關心驗證輸入,可以省略 output 字段。

const MyFunction = z.function({
  input: [z.string()], // parameters (must be an array or a ZodTuple)
});
 
const computeTrimmedLength = MyFunction.implement((input) => input.trim.length);

使用 .implementAsync() 方法創建異步函數。

const computeTrimmedLengthAsync = MyFunction.implementAsync(
  async (input) => input.trim().length
);
 
computeTrimmedLengthAsync("sandwich"); // => Promise<8>

自定義 (Custom)

您可以使用 z.custom() 為任何 TypeScript 類型創建 Zod 架構。這對於為 Zod 開箱即用不支持的類型(如模板字串字面量)創建架構很有用。

const px = z.custom<`${number}px`>((val) => {
  return typeof val === "string" ? /^\d+px$/.test(val) : false;
});
 
type px = z.infer<typeof px>; // `${number}px`
 
px.parse("42px"); // "42px"
px.parse("42vw"); // throws;

如果您不提供驗證函數,Zod 將允許任何值。這可能很危險!

z.custom<{ arg: string }>(); // performs no validation

您可以通過傳遞第二個參數來自定義錯誤消息和其他選項。此參數的工作方式與 .refine 的 params 參數相同。

z.custom<...>((val) => ..., "custom error message");