Zod v4 使用指南:資料驗證核心功能
本文基於 Zod v4 版本撰寫。Zod 4 於 2025 年發布,帶來了顯著的效能提升、更小的 bundle size,以及多項新功能如內建 JSON Schema 轉換、遞迴物件支援等。
基本用法
定義 Schema
「Schema」是 Zod 的核心概念,它同時扮演兩個角色:執行期驗證器與編譯期型別定義。
傳統的 TypeScript 開發中,通常需要分別 維護兩套定義:一套是 TypeScript 的 interface 或 type 用於編譯期型別檢查,另一套是執行期的驗證邏輯(如手寫的 if 判斷或使用其他驗證庫)。這種分離帶來同步問題——當資料結構改變時,必須同時更新兩個地方,否則就會產生型別定義與實際驗證邏輯不一致的 bug。
Zod 採用「單一來源」(Single Source of Truth)的設計理念來解決這個問題。只需要定義一次 schema,Zod 就會同時提供:
- 執行期驗證:在程式運行時檢查資料是否符合預期結構
- 編譯期型別:透過
z.infer自動推導出對應的 TypeScript 型別
修改 schema 時,型別定義會自動跟著改變,不需要手動同步,從根本上消除了型別與驗證邏輯不同步的風險。
import { z } from "zod";
const UserSchema = z.object({
username: z.string(),
age: z.number(),
});
解析資料 (parse)
使用 .parse() 驗證資料。驗證成功時會回傳一個經過驗證的深層複製資料,這確保了原始資料不會被意外修改,同時也讓 TypeScript 能夠正確推導出回傳值的型別。
const user = UserSchema.parse({ username: "john", age: 25 });
// => { username: "john", age: 25 }
// 驗證失敗會拋出 ZodError
UserSchema.parse({ username: 123, age: "25" }); // throws ZodError
安全解析 (safeParse)
相較於 .parse() 會拋出例外 ,.safeParse() 回傳一個 discriminated union,可以用 if/else 處理成功與失敗的情況,不需要 try/catch。在表單驗證或 API 輸入處理時,通常會選擇 .safeParse(),因為驗證失敗是預期中的情況,不應該用例外來處理。
const result = UserSchema.safeParse({ username: "john", age: 25 });
if (!result.success) {
console.log(result.error); // ZodError
} else {
console.log(result.data); // { username: string; age: number }
}
非同步解析
當 schema 包含非同步操作(如 async refinements 或 async transforms)時,必須使用非同步版本的解析方法。常見的應用場景包括:驗證 email 是否已被註冊、檢查使用者名稱是否可用等需要查詢資料庫的情況。
await UserSchema.parseAsync(data);
await UserSchema.safeParseAsync(data);
型別推導 (Type Inference)
Zod 最強大的特性之一是能夠從 schema 自動推導出 TypeScript 型別。只需要定義一次 schema,就能同時獲得執行期驗證和編譯期型別檢查。
const UserSchema = z.object({
username: z.string(),
age: z.number(),
});
// 使用 z.infer 提取型別
type User = z.infer<typeof UserSchema>;
// => { username: string; age: number }
// 現在可以在任何地方使用這個型別
const user: User = { username: "john", age: 25 };
當 schema 包含轉換邏輯時,輸入與輸出型別可能不同。Zod 提供了 z.input 和 z.output 來分別提取這兩種型別:
const schema = z.string().transform((val) => val.length);
type SchemaInput = z.input<typeof schema>; // string
type SchemaOutput = z.output<typeof schema>; // number (等同於 z.infer)
原始型別 (Primitives)
Zod 提供了對應 JavaScript 所有原始型別的 schema。這 些是建構更複雜 schema 的基礎元件。
// 基本型別
z.string();
z.number();
z.bigint();
z.boolean();
z.date();
z.symbol();
// 空值型別
z.undefined();
z.null();
z.void(); // 接受 undefined
// 萬用型別
z.any();
z.unknown();
// Never 型別
z.never();
字面值 (Literals)
字面值 schema 用於驗證特定的固定值,常用於建構 discriminated union 或定義常數型別。
const tuna = z.literal("tuna");
const twelve = z.literal(12);
const isTrue = z.literal(true);
// 取得字面值
tuna.value; // "tuna"
字串驗證 (Strings)
字串是最常見的驗證對象。Zod 提供了豐富的內建驗證方法,涵蓋長度限制、格式驗證、內容檢查等常見需求。
長度與內容驗證
z.string().min(5); // 最少 5 字元
z.string().max(10); // 最多 10 字元
z.string().length(5); // 剛好 5 字元
z.string().regex(/^[a-z]+$/); // 正則表達式
z.string().includes("hello"); // 包含子字串
z.string().startsWith("https://"); // 以特定字串開頭
z.string().endsWith(".com"); // 以特定字串結尾
格式驗證
Zod 內建了許多常見格式的驗證器。在 v4 中,這些格式驗證也被提升為頂層函式,提供更好的 tree-shaking 支援。
// 常用格式
z.string().email(); // Email 格式
z.string().url(); // URL 格式
z.string().uuid(); // UUID 格式
z.string().ip(); // IPv4 或 IPv6
// 識別碼格式
z.string().nanoid();
z.string().cuid();
z.string().cuid2();
z.string().ulid();
// 日期時間格式
z.string().datetime(); // ISO 8601 完整格式
z.string().date(); // YYYY-MM-DD
z.string().time(); // HH:mm:ss
// 編碼格式
z.string().base64();
字串轉換
這些方法會在驗證過程中轉換字串,輸出的值會是轉換後的結果。例如,使用 .trim() 可以自動去除使用者輸入的前後空白,避免因為多餘空白導致的驗證失敗或資料不一致。
z.string().trim(); // 去除前後空白
z.string().toLowerCase(); // 轉小寫
z.string().toUpperCase(); // 轉大寫
自訂錯誤訊息
良好的錯誤訊息對使用者體驗至關重要。Zod 允許為每個驗證規則指定自訂訊息。
z.string().min(5, { message: "至少需要 5 個字元" });
z.string().email({ message: "無效的 Email 格式" });
z.string().url({ message: "無效的 URL" });
數字驗證 (Numbers)
數字驗證涵蓋範圍檢查、整數驗證、正負數限制等常見需求。
// 範圍驗證
z.number().gt(5); // > 5
z.number().gte(5); // >= 5 (別名: .min(5))
z.number().lt(5); // < 5
z.number().lte(5); // <= 5 (別名: .max(5))
// 整數與正負數
z.number().int(); // 整數
z.number().positive(); // > 0
z.number().nonnegative(); // >= 0
z.number().negative(); // < 0
z.number().nonpositive(); // <= 0
// 其他驗證
z.number().multipleOf(5); // 5 的倍數 (別名: .step(5))
z.number().finite(); // 有限數(非 Infinity)
z.number().safe(); // 安全整數範圍
自訂錯誤訊息
z.number().lte(100, { message: "數值不能超過 100" });
物件 (Objects)
物件是 Zod 中最常用的複合型別。Zod 提供了豐富的物件操作方法,可以靈活地組合、修改和重用 schema。
基本定義
const UserSchema = z.object({
name: z.string(),
age: z.number(),
});
type User = z.infer<typeof UserSchema>;
// => { name: string; age: number }
擴展 (extend)
.extend() 是擴展物件 schema 的推薦方式。它可以新增屬性,也可以覆寫現有屬性的 schema。相較於 z.intersection(),使用 .extend() 回傳的仍然是 ZodObject,保留了所有物件方法如 .pick()、.omit() 等。
const UserWithEmail = UserSchema.extend({
email: z.string().email(),
});
合併 (merge)
.merge() 用於合併兩個獨立的 object schema。當兩個 schema 有相同的 key 時,第二個 schema 的定義會覆蓋第一個。
const BaseUser = z.object({ name: z.string() });
const WithAge = z.object({ age: z.number() });
const User = BaseUser.merge(WithAge);
// => { name: string; age: number }
選取 (pick) 與 省略 (omit)
這兩個方法靈感來自 TypeScript 的 Pick 和 Omit 工具型別,可以從現有 schema 建立子集。舉例來說,假設有一個完整的 User schema 包含 id、name、email、passwordHash 等欄位,但在回傳給前端時只想暴露 id、name、email,這時就可以用 .omit({ passwordHash: true }) 來建立一個安全的公開版本。
const UserSchema = z.object({
id: z.string(),
name: z.string(),
email: z.string(),
});
// 只保留指定屬性
const NameOnly = UserSchema.pick({ name: true });
// => { name: string }
// 移除指定屬性
const WithoutId = UserSchema.omit({ id: true });
// => { name: string; email: string }
部分可選 (partial)
.partial() 將所有屬性變為可選,類似 TypeScript 的 Partial<T>。這個方法常用於 PATCH API 的輸入驗證——使用者可能只想更新 name 而不動 email,此時所有欄位都應該是可選的。
const PartialUser = UserSchema.partial();
// => { id?: string; name?: string; email?: string }
// 只將部分屬性變為可選
const PartialName = UserSchema.partial({ name: true });
// => { id: string; name?: string; email: string }
深層部分可選 (deepPartial)
當物件包含巢狀結構時,.deepPartial() 會遞迴地將所有層級的屬性都變為可選。
const user = z.object({
name: z.string(),
address: z.object({
city: z.string(),
country: z.string(),
}),
});
const deepPartialUser = user.deepPartial();
/*
{
name?: string;
address?: {
city?: string;
country?: string;
}
}
*/
必填 (required)
.required() 是 .partial() 的反向操作,將所有可選屬性變為必填。
const RequiredUser = PartialUser.required();
未知鍵處理
Zod 預設會忽略(strip)未定義的鍵。這是一個重要的安全特性,可以防止意外的資料注入。可以根據需求調整這個行為:
// 預設:忽略未知鍵(推薦用於 API 輸入)
const user = z.object({ name: z.string() });
user.parse({ name: "john", extra: "ignored" }); // => { name: "john" }
// 嚴格模式:拒絕未知鍵(適合需要精確控制的場景)
const strictUser = z.object({ name: z.string() }).strict();
strictUser.parse({ name: "john", extra: "error" }); // throws ZodError
// 保留未知鍵(適合需要透傳資料的場景)
const looseUser = z.object({ name: z.string() }).passthrough();
looseUser.parse({ name: "john", extra: "kept" });
// => { name: "john", extra: "kept" }
陣列 (Arrays)
基本定義
Zod 提供兩種等價的語法來定義陣列 schema。選擇哪種主要是風格偏好,但 .array() 方法在鏈式呼叫時更為簡潔。
const StringArray = z.array(z.string());
// 或
const StringArray = z.string().array();
type StringArray = z.infer<typeof StringArray>; // string[]
陣列驗證
z.array(z.string()).min(1); // 至少 1 個元素
z.array(z.string()).max(10); // 最多 10 個元素
z.array(z.string()).length(5); // 剛好 5 個元素
z.array(z.string()).nonempty(); // 非空陣列