TypeScriptで開発していると「APIレスポンスが本当にこの型か実行時に確認したい」「フォームの入力値を型安全にバリデーションしたい」という場面に必ず直面します。
Zodは「TypeScript-first スキーマ宣言 & バリデーションライブラリ」です。型定義とバリデーションロジックを1つのスキーマで同時に管理でき、z.infer<typeof schema>でTypeScriptの型を自動生成できます。依存関係ゼロで、バンドルサイズも軽量(8kb gzip)なのが特徴です。
- Zodのインストールと基本スキーマ(string・number・boolean・object・array)
z.inferでスキーマからTypeScript型を自動生成する方法parse/safeParseの使い分けとエラーハンドリングrefine/superRefineで独自バリデーションルールを追加する方法- optional・nullable・default・transform・pipe の活用
- Union・Discriminated Union・Intersection スキーマ
- フォームバリデーション・APIレスポンス検証・環境変数バリデーションの実務パターン
なお、Zodが生成する型についての理解を深めるには【TypeScript】型の書き方 完全入門も合わせてご参照ください。
Zodとは
ZodはColin McDonnell氏が開発した、TypeScript向けのスキーマバリデーションライブラリです。2024年現在、週1,500万ダウンロードを超える非常に人気の高いライブラリです。
| 特徴 | 内容 |
|---|---|
| TypeScript-first | TypeScriptのために設計。型推論が強力でジェネリクスを最大限活用 |
| 依存関係ゼロ | 外部ライブラリへの依存がなくインストールが軽量 |
| 実行時バリデーション | JavaScriptとして実行時にデータの形を検証できる |
| 型の自動生成 | z.inferでスキーマから型を生成。定義の二重管理が不要 |
| 豊富なAPI | 文字列・数値・日付・配列・Union・変換など多彩なメソッドを提供 |
Yup・JoiはJavaScriptファーストで設計されており、TypeScriptの型推論が限定的です。ZodはTypeScript型システムと完全に統合されており、スキーマ定義 = 型定義として扱える点が最大の違いです。
インストールと初期設定
# npm npm install zod # yarn yarn add zod # pnpm pnpm add zod
Zodを使うには TypeScript 4.5以上 と、
tsconfig.json で "strict": true の設定が必要です。strict モードが無効だと型推論が正しく機能しない場合があります。tsconfig.json の設定方法はこちらを参照してください。この記事は Zod v3系(2024年現在の最新安定版)をベースに解説しています。v3はTypeScript 4.5以上を対象としており、v2以前から大幅にAPIが改善されています。インストール後は
npm list zod でバージョンを確認してください。インストール後はエントリーファイルや必要なモジュールでインポートして使用します。
// ESModule
import { z } from "zod";
// CommonJS
const { z } = require("zod");
基本スキーマの定義
Zodのすべてのスキーマは z オブジェクトのメソッドから作成します。プリミティブ型に対応する基本スキーマは以下の通りです。
import { z } from "zod";
// プリミティブ型
const strSchema = z.string();
const numSchema = z.number();
const boolSchema = z.boolean();
const dateSchema = z.date();
const bigintSchema = z.bigint();
// リテラル型(特定の値のみ許可)
const statusSchema = z.literal("active");
const codeSchema = z.literal(200);
// any / unknown / never / void
const anySchema = z.any();
const unknownSchema = z.unknown();
const neverSchema = z.never();
const voidSchema = z.void();
// null / undefined
const nullSchema = z.null();
const undefinedSchema = z.undefined();
文字列スキーマの詳細バリデーション
z.string() には文字列専用のバリデーションメソッドが多数用意されています。
const emailSchema = z.string()
.min(1, "メールアドレスを入力してください")
.max(254, "メールアドレスは254文字以内です")
.email("有効なメールアドレスを入力してください");
const urlSchema = z.string().url("有効なURLを入力してください");
const uuidSchema = z.string().uuid("有効なUUIDを入力してください");
// 正規表現によるバリデーション
const zipSchema = z.string()
.regex(/^\d{3}-?\d{4}$/, "郵便番号の形式が正しくありません");
// 変換を伴うバリデーション
const trimmedSchema = z.string().trim(); // 前後空白除去
const lowerSchema = z.string().toLowerCase(); // 小文字化
const upperSchema = z.string().toUpperCase(); // 大文字化
// 文字列を特定の値に制限
const startSchema = z.string().startsWith("https://");
const includeSchema = z.string().includes("@");
数値スキーマの詳細バリデーション
const ageSchema = z.number()
.int("年齢は整数で入力してください")
.min(0, "年齢は0以上です")
.max(150, "年齢は150以下です");
const priceSchema = z.number()
.positive("価格は正の数値です")
.multipleOf(0.01, "価格は小数第2位まで");
// positive / negative / nonnegative / nonpositive
const positiveNum = z.number().positive(); // > 0
const negativeNum = z.number().negative(); // < 0
const nonNeg = z.number().nonnegative(); // >= 0
const finite = z.number().finite(); // Infinity を除外
const safe = z.number().safe(); // Number.isSafeInteger の範囲
オブジェクトスキーマ(z.object)
z.object() はオブジェクト型のスキーマを定義します。ネストしたオブジェクトや必須/省略可能フィールドも簡潔に書けます。
import { z } from "zod";
const userSchema = z.object({
id: z.number().int().positive(),
name: z.string().min(1).max(50),
email: z.string().email(),
age: z.number().int().min(0).max(150).optional(),
role: z.enum(["admin", "user", "guest"]),
createdAt: z.date(),
});
// z.infer でスキーマから型を生成
type User = z.infer<typeof userSchema>;
// type User = {
// id: number;
// name: string;
// email: string;
// age?: number | undefined;
// role: "admin" | "user" | "guest";
// createdAt: Date;
// }
z.infer<typeof schema> を使うことで、スキーマと型定義の二重管理が不要になります。スキーマを変更すると型も自動で更新されるため、型の不整合が起こりません。これがZodをTypeScript-firstと呼ぶ最大の理由です。オブジェクトの各種メソッド
const baseSchema = z.object({
id: z.number(),
name: z.string(),
});
// partial: 全フィールドをoptionalに(Partial<T>相当)
const partialSchema = baseSchema.partial();
// → { id?: number; name?: string }
// required: 全フィールドをrequiredに(Required<T>相当)
const requiredSchema = baseSchema.partial().required();
// pick: 特定フィールドのみ選択(Pick<T, K>相当)
const pickedSchema = baseSchema.pick({ name: true });
// → { name: string }
// omit: 特定フィールドを除外(Omit<T, K>相当)
const omittedSchema = baseSchema.omit({ id: true });
// → { name: string }
// extend: フィールドを追加
const extendedSchema = baseSchema.extend({
email: z.string().email(),
});
// merge: 2つのオブジェクトスキーマを結合
const addressSchema = z.object({ city: z.string(), zip: z.string() });
const fullSchema = baseSchema.merge(addressSchema);
デフォルトでZodは余分なキーを無視して除去します(strip)。
.strict() を付けると余分なキーがあるとエラーになります。.passthrough() を付けると余分なキーをそのまま通します。本番APIではstripかstrictが推奨です。配列・タプル・Recordスキーマ
// 配列スキーマ
const tagsSchema = z.array(z.string()).min(1).max(10);
const numListSchema = z.array(z.number()).nonempty();
// タプルスキーマ(各要素に異なる型を指定)
const coordSchema = z.tuple([z.number(), z.number()]);
const rgbaSchema = z.tuple([z.number(), z.number(), z.number(), z.number().optional()]);
// .rest() で可変長末尾要素を指定
const csvRowSchema = z.tuple([z.string(), z.string()]).rest(z.string());
// Recordスキーマ(辞書型)
const scoreMap = z.record(z.string(), z.number());
// → { [key: string]: number }
// Mapスキーマ
const mapSchema = z.map(z.string(), z.number());
// Setスキーマ
const setSchema = z.set(z.string()).min(1);
Union・Intersection・Discriminated Union
Unionスキーマ(z.union / .or)
// z.union: 複数の型のいずれかを許可
const strOrNum = z.union([z.string(), z.number()]);
const strOrNum2 = z.string().or(z.number()); // 同義
// enumスキーマ(文字列リテラルのUnion)
const roleSchema = z.enum(["admin", "user", "guest"]);
type Role = z.infer<typeof roleSchema>; // "admin" | "user" | "guest"
// nativeEnum: TypeScript の enum を使用
enum Direction { Up = "UP", Down = "DOWN" }
const dirSchema = z.nativeEnum(Direction);
Intersectionスキーマ(z.intersection / .and)
const baseUser = z.object({ id: z.number(), name: z.string() });
const withAdmin = z.object({ permissions: z.array(z.string()) });
// z.intersection: 両方の型を満たす必要がある
const adminUser = z.intersection(baseUser, withAdmin);
const adminUser2 = baseUser.and(withAdmin); // 同義
type AdminUser = z.infer<typeof adminUser>;
// → { id: number; name: string; permissions: string[] }
Discriminated Unionスキーマ
判別プロパティ(discriminant)を使って効率的にUnion型を定義できます。判別可能なユニオン型の詳細はこちらで解説しています。
// 通常のz.unionより型推論が精確でエラーメッセージも明確
const eventSchema = z.discriminatedUnion("type", [
z.object({
type: z.literal("click"),
x: z.number(),
y: z.number(),
}),
z.object({
type: z.literal("keydown"),
key: z.string(),
ctrlKey: z.boolean().default(false),
}),
z.object({
type: z.literal("resize"),
width: z.number(),
height: z.number(),
}),
]);
type Event = z.infer<typeof eventSchema>;
// type Event =
// | { type: "click"; x: number; y: number }
// | { type: "keydown"; key: string; ctrlKey: boolean }
// | { type: "resize"; width: number; height: number }
optional・nullable・default・nullish・transform
const schema = z.object({
// required: バリデーション失敗でエラー
name: z.string(),
// optional: undefinedを許可 → string | undefined
nickname: z.string().optional(),
// nullable: nullを許可 → string | null
deletedAt: z.string().nullable(),
// nullish: null と undefined を両方許可 → string | null | undefined
bio: z.string().nullish(),
// default: undefinedのときデフォルト値を使用
role: z.string().default("user"),
// default(関数形式): 呼び出しごとに新しい値を生成
createdAt: z.date().default(() => new Date()),
});
transformで値を変換する
.transform() はバリデーション後に値を変換します。入力型と出力型が異なるスキーマを作れます。
// 文字列を数値に変換
const numStringSchema = z.string()
.regex(/^\d+$/, "数値文字列を入力してください")
.transform(Number);
// type: string → output: number
type Input = z.input<typeof numStringSchema>; // string
type Output = z.output<typeof numStringSchema>; // number
// 文字列を日付に変換
const dateStringSchema = z.string()
.datetime({ message: "ISO8601形式で入力してください" })
.transform((s) => new Date(s));
// trimして小文字化
const normalizedEmail = z.string()
.email()
.transform((s) => s.trim().toLowerCase());
pipe でスキーマをチェーン接続
// transform後の値に追加バリデーションをかける
const portSchema = z.string()
.transform(Number)
.pipe(z.number().int().min(1).max(65535));
// 環境変数のPORT文字列を数値として安全にパース
portSchema.parse("3000"); // → 3000
portSchema.parse("0"); // → ZodError: too_small
portSchema.parse("abc"); // → ZodError: invalid_type
refine・superRefine で独自バリデーションを追加
Zodの組み込みバリデーションで対応できない複雑なルールは、.refine() や .superRefine() で独自ロジックを追加できます。
refine: 単一のカスタムバリデーション
// パスワードの複雑さチェック
const passwordSchema = z.string()
.min(8, "パスワードは8文字以上")
.refine(
(val) => /[A-Z]/.test(val),
{ message: "大文字を1文字以上含めてください" }
)
.refine(
(val) => /[a-z]/.test(val),
{ message: "小文字を1文字以上含めてください" }
)
.refine(
(val) => /[0-9]/.test(val),
{ message: "数字を1文字以上含めてください" }
);
// 日付の前後チェック(開始日 <= 終了日)
const dateRangeSchema = z.object({
startDate: z.date(),
endDate: z.date(),
}).refine(
(data) => data.startDate <= data.endDate,
{ message: "開始日は終了日以前にしてください", path: ["endDate"] }
);
superRefine: 複数フィールドにまたがる複雑なバリデーション
// パスワード確認フォームのバリデーション
const signupSchema = z.object({
username: z.string().min(3).max(20),
password: z.string().min(8),
confirm: z.string(),
}).superRefine(({ username, password, confirm }, ctx) => {
if (password !== confirm) {
ctx.addIssue({
code: z.ZodIssueCode.custom,
message: "パスワードが一致しません",
path: ["confirm"], // エラーをconfirmフィールドに紐付け
});
}
if (password.toLowerCase().includes(username.toLowerCase())) {
ctx.addIssue({
code: z.ZodIssueCode.custom,
message: "パスワードにユーザー名を含めることはできません",
path: ["password"],
});
}
});
refine: 単一ルールを追加するのに適しています。バリデーション失敗時は1つのエラーのみ返します。superRefine:
ctx.addIssue() を複数回呼べるため、複数のエラーを同時に報告できます。複数フィールド間の検証や条件付きバリデーションに向いています。parse・safeParse とエラーハンドリング
Zodにはデータを検証する2つの主要メソッドがあります。どちらを使うかでエラーの扱い方が変わります。
| メソッド | 成功時 | 失敗時 | 用途 |
|---|---|---|---|
parse(data) |
パース済みデータを返す | ZodErrorをthrow |
エラーを例外として扱いたい場合 |
safeParse(data) |
{ success: true, data } |
{ success: false, error } |
例外を使わず結果を分岐したい場合 |
parseAsync(data) |
パース済みデータをPromiseで返す | ZodErrorをreject |
非同期バリデーション(refineでasync使用時) |
safeParseAsync(data) |
{ success: true, data }のPromise |
{ success: false, error }のPromise |
非同期バリデーションで例外を避けたい場合 |
parse の使い方(例外を使うパターン)
import { z, ZodError } from "zod";
const userSchema = z.object({
id: z.number(),
name: z.string(),
});
try {
const user = userSchema.parse({ id: "wrong", name: "Alice" });
console.log(user); // 到達しない
} catch (err) {
if (err instanceof ZodError) {
// err.issues: バリデーションエラーの配列
console.log(err.issues);
// [
// {
// code: "invalid_type",
// expected: "number",
// received: "string",
// path: ["id"],
// message: "Expected number, received string"
// }
// ]
}
}
safeParse の使い方(Result型パターン)
const result = userSchema.safeParse({ id: 1, name: "Alice" });
if (result.success) {
// result.data の型は z.infer<typeof userSchema>
console.log(result.data.name); // "Alice"
} else {
// result.error は ZodError
console.log(result.error.issues);
}
ZodError のエラー整形メソッド
const result = userSchema.safeParse({ id: "bad", name: 42 });
if (!result.success) {
// flatten(): フィールドごとのエラーをまとめる(フォームに最適)
const flat = result.error.flatten();
console.log(flat.fieldErrors);
// { id: ["Expected number, received string"], name: ["Expected string, received number"] }
console.log(flat.formErrors); // フィールドに紐付かないエラー
// format(): ネストしたオブジェクト形式でエラーを取得
const formatted = result.error.format();
console.log(formatted.id?._errors); // ["Expected number, received string"]
// message: エラーメッセージを改行で結合した文字列
console.log(result.error.message);
}
flatten().fieldErrors はフォームライブラリとの相性が非常に良いです。フィールド名をキーとするエラーメッセージ配列を返すため、フォームの各フィールドの下にエラーメッセージを表示する実装が簡単になります。再帰スキーマ(lazy)と前処理(preprocess)
z.lazy:再帰的なスキーマ定義
自己参照するツリー構造などの再帰的な型は z.lazy() で定義します。
// ツリー構造のスキーマ(カテゴリの入れ子など)
type Category = {
id: number;
name: string;
children: Category[];
};
const categorySchema: z.ZodType<Category> = z.object({
id: z.number(),
name: z.string(),
children: z.lazy(() => z.array(categorySchema)),
});
// JSON形式のカテゴリデータを検証できる
categorySchema.parse({
id: 1, name: "TypeScript",
children: [
{ id: 2, name: "基礎", children: [] },
{ id: 3, name: "応用", children: [
{ id: 4, name: "ジェネリクス", children: [] }
]},
],
});
z.preprocess:バリデーション前にデータを変換
z.preprocess() はバリデーションの前にデータを変換します。transform がバリデーション後の変換なのに対し、preprocess は前処理です。フォームデータのように「すべて文字列で渡ってくる」ケースで特に役立ちます。
// 数値・文字列どちらで渡っても数値に変換してバリデーション
const numOrStrSchema = z.preprocess(
(val) => (typeof val === "string" ? Number(val) : val),
z.number().int().positive()
);
numOrStrSchema.parse("42"); // → 42 (number)
numOrStrSchema.parse(42); // → 42 (number)
numOrStrSchema.parse("abc"); // → ZodError: invalid_type
// nullish値を空文字に変換してからバリデーション
const emptyToUndefined = z.preprocess(
(val) => (val === "" ? undefined : val),
z.string().optional()
);
preprocess: バリデーション前に実行。型が確定していない
unknown を受け取ります。フォーム入力値の正規化などに使います。transform: バリデーション後に実行。型が確定した値を変換します。バリデーション済みデータの加工(文字列→日付変換など)に使います。実務パターン3本
実務パターン1:環境変数のバリデーション
Node.jsアプリで最も推奨される使い方のひとつが環境変数のバリデーションです。process.env の型はすべて string | undefined ですが、Zodで検証することで型安全な環境変数アクセスが実現できます。
// src/env.ts
import { z } from "zod";
const envSchema = z.object({
NODE_ENV: z.enum(["development", "test", "production"]),
DATABASE_URL: z.string().url("DATABASE_URLは有効なURLが必要です"),
PORT: z.string()
.transform(Number)
.pipe(z.number().int().min(1).max(65535))
.default(3000 as any),
API_KEY: z.string().min(32, "APIキーは32文字以上が必要です"),
LOG_LEVEL: z.enum(["debug", "info", "warn", "error"]).default("info"),
CORS_ORIGIN: z.string().url().optional(),
});
// アプリ起動時に一度だけ検証(失敗時はプロセスを終了)
const _result = envSchema.safeParse(process.env);
if (!_result.success) {
console.error("環境変数の設定が不正です:");
console.error(_result.error.flatten().fieldErrors);
process.exit(1);
}
export const env = _result.data;
// 他ファイルからimportして使用
// import { env } from "./env";
// env.PORT は number 型として扱える
// env.DATABASE_URL は string 型として扱える
実務パターン2:APIレスポンスの型安全な検証
fetch や axios でAPIからデータを取得する際、as UserType のような型アサーションはランタイムエラーを防げません。Zodで実行時バリデーションを行うことで型安全性を実行時まで保証できます。
// src/api/users.ts
import { z } from "zod";
// レスポンス型のスキーマ定義
const userSchema = z.object({
id: z.number(),
name: z.string(),
email: z.string().email(),
createdAt: z.string().datetime().transform((s) => new Date(s)),
});
const usersListSchema = z.object({
data: z.array(userSchema),
total: z.number(),
page: z.number(),
});
export type User = z.infer<typeof userSchema>;
// 型安全なAPIクライアント関数
export async function fetchUser(id: number): Promise<User> {
const res = await fetch(`/api/users/${id}`);
if (!res.ok) {
throw new Error(`HTTPエラー: ${res.status}`);
}
const json = await res.json();
// APIが予期しないデータを返した場合にZodErrorがthrowされる
return userSchema.parse(json);
}
// safeParseを使ってエラーを返り値で扱うパターン
export async function fetchUserSafe(id: number)
: Promise<{ success: true; data: User } | { success: false; message: string }> {
const res = await fetch(`/api/users/${id}`);
if (!res.ok) {
return { success: false, message: `HTTPエラー: ${res.status}` };
}
const json = await res.json();
const result = userSchema.safeParse(json);
if (!result.success) {
return { success: false, message: result.error.message };
}
return { success: true, data: result.data };
}
const user = await res.json() as User; のような型アサーションは、TypeScriptのコンパイルは通りますが、APIが実際に異なるデータを返してもエラーになりません。Zodのパースを挟むことで、型と実データの乖離をランタイムで検出できます。詳しくは型アサーション(as)の落とし穴もご参照ください。実務パターン3:React Hook Form + Zodでフォームバリデーション
React + TypeScriptの開発では、@hookform/resolvers/zod を使って React Hook Form と Zod を連携できます。
# 必要パッケージのインストール npm install react-hook-form @hookform/resolvers zod
// src/components/SignupForm.tsx
import { useForm } from "react-hook-form";
import { zodResolver } from "@hookform/resolvers/zod";
import { z } from "zod";
// バリデーションスキーマ定義(ここが型定義も兼ねる)
const signupSchema = z.object({
username: z.string()
.min(3, "ユーザー名は3文字以上")
.max(20, "ユーザー名は20文字以内")
.regex(/^[a-zA-Z0-9_]+$/, "英数字とアンダースコアのみ使用可能"),
email: z.string().email("有効なメールアドレスを入力してください"),
password: z.string()
.min(8, "パスワードは8文字以上")
.regex(/[A-Z]/, "大文字を含めてください")
.regex(/[0-9]/, "数字を含めてください"),
confirmPassword: z.string(),
}).refine(
(data) => data.password === data.confirmPassword,
{ message: "パスワードが一致しません", path: ["confirmPassword"] }
);
// スキーマからフォームの型を生成
type SignupFormData = z.infer<typeof signupSchema>;
export function SignupForm() {
const {
register,
handleSubmit,
formState: { errors, isSubmitting },
} = useForm<SignupFormData>({
resolver: zodResolver(signupSchema), // ZodスキーマをRHFに渡す
});
const onSubmit = async (data: SignupFormData) => {
// dataはSignupFormData型として型安全に使える
await registerUser(data);
};
return (
<form onSubmit={handleSubmit(onSubmit)}>
<input {...register("username")} />
{errors.username && <p>{errors.username.message}</p>}
<input type="email" {...register("email")} />
{errors.email && <p>{errors.email.message}</p>}
<input type="password" {...register("password")} />
{errors.password && <p>{errors.password.message}</p>}
<input type="password" {...register("confirmPassword")} />
{errors.confirmPassword && <p>{errors.confirmPassword.message}</p>}
<button type="submit" disabled={isSubmitting}>登録</button>
</form>
);
}
zodResolver を使うと、Zodのバリデーションエラーが自動的に React Hook Form の
errors オブジェクトにマッピングされます。フォームの型・バリデーションルール・エラーメッセージをZodのスキーマ1ファイルで一元管理できるため、保守性が大幅に向上します。エラーメッセージの日本語化
Zodのデフォルトエラーメッセージは英語です。日本語にするには2つの方法があります。
方法1:各スキーマに直接メッセージを指定
const userFormSchema = z.object({
name: z.string({
required_error: "名前は必須です", // undefinedのとき
invalid_type_error: "名前は文字列で入力してください", // 型が違うとき
})
.min(1, "名前を入力してください")
.max(50, "名前は50文字以内です"),
age: z.number({
required_error: "年齢は必須です",
invalid_type_error: "年齢は数値で入力してください",
})
.int("年齢は整数で入力してください")
.min(0, "年齢は0以上を入力してください")
.max(150, "年齢は150以下を入力してください"),
email: z.string()
.email("有効なメールアドレスを入力してください"),
});
方法2:z.setErrorMap でグローバルに日本語化
プロジェクト全体でデフォルトエラーメッセージを日本語にしたい場合は、z.setErrorMap() でグローバルに設定できます。
import { z, ZodIssueCode } from "zod";
// アプリのエントリーポイント(main.ts や app.ts)で一度だけ設定
z.setErrorMap((issue, ctx) => {
switch (issue.code) {
case ZodIssueCode.invalid_type:
if (issue.received === "undefined") {
return { message: "この項目は必須です" };
}
return { message: `${issue.expected}型を入力してください` };
case ZodIssueCode.too_small:
if (issue.type === "string") {
return { message: `${issue.minimum}文字以上で入力してください` };
}
return { message: `${issue.minimum}以上を入力してください` };
case ZodIssueCode.too_big:
if (issue.type === "string") {
return { message: `${issue.maximum}文字以内で入力してください` };
}
return { message: `${issue.maximum}以下を入力してください` };
case ZodIssueCode.invalid_string:
if (issue.validation === "email") return { message: "メールアドレスの形式が正しくありません" };
if (issue.validation === "url") return { message: "URLの形式が正しくありません" };
break;
}
return { message: ctx.defaultError };
});
// 設定後はすべてのZodエラーが日本語で返る
const result = z.string().min(3).safeParse("ab");
// result.error?.issues[0].message → "3文字以上で入力してください"
より本格的な多言語対応には zod-i18n-map というライブラリが便利です。日本語・英語など複数言語の翻訳ファイルが同梱されており、
z.setErrorMap() に渡すだけで完全日本語化できます。i18nextとの連携も可能です。よくあるエラーと解決策
Zodを使う際に遭遇しやすいエラーとその解決策をまとめます。TypeScriptのエラーハンドリングも合わせて参照してください。
| エラーコード | 原因 | 解決策 |
|---|---|---|
invalid_type |
期待する型と実際の型が違う | スキーマと送信データの型を確認。文字列→数値変換が必要ならtransform(Number) |
too_small |
min以下の値 | .min() の値を確認。日付・配列・文字列すべてで発生する |
too_big |
max以上の値 | .max() の値を確認 |
invalid_string |
文字列形式が不正(email/url/uuid等) | 入力値の形式を確認。.email()/.url()/.uuid()等のバリデーター |
invalid_enum_value |
enumに存在しない値 | z.enum() に渡した配列の値を確認 |
unrecognized_keys |
定義外のキーが存在(strictモード) | .passthrough() か .strip() に変更、またはスキーマに追加 |
custom |
refine/superRefineのバリデーション失敗 |
refineで指定したmessageを確認 |
よくある質問
QZodとYupはどちらを選ぶべきですか?
ATypeScriptを使っているならZodが推奨です。Zodはz.inferによる型の自動生成が強力で、スキーマと型定義を一元管理できます。YupはReact Hook Form等の一部設定ファイルでの実績が豊富ですが、TypeScriptサポートは後付けのため推論が限定的です。新規プロジェクトではZodを選ぶ理由が多いです。
Qzodはフロントエンドとバックエンドで同じスキーマを共有できますか?
Aはい。monorepoやパッケージ分割でスキーマを共有するのが一般的なパターンです。Next.jsやRemixのようなフルスタックフレームワークでは、shared/schemasフォルダに定義してクライアント・サーバー両方からimportする構成がよく使われます。
Qzodのスキーマを既存のTypeScript型から生成できますか?
A直接の変換はできません(型はコンパイル時に消えるため)。ただし ts-to-zod というツールを使うと既存の型定義から近似的なZodスキーマを自動生成できます。新規コードではZodのスキーマを先に書き、そこから型を生成するのが推奨パターンです。
Qzodは動作が遅いですか?
A一般的なアプリケーションでは問題ありません。ただし数万件規模の大量データを毎フレームバリデーションする用途には向きません。パフォーマンスが最優先の場合は Valibot(Zodの後継を目指すライブラリ)も検討する価値があります。Zodはバンドルサイズが8kb(gzip)と軽量で、フロントエンドでも気軽に使えます。
QzodでDate型のバリデーションはどうすればよいですか?
Az.date() はJavaScriptのDateオブジェクトを検証します。JSON経由では日付は文字列で送られるため、z.string().datetime().transform(s => new Date(s)) のようにtransformと組み合わせるパターンが実務では一般的です。
まとめ
Zodを使うことで、TypeScriptの静的型チェックと実行時バリデーションを1つのスキーマで統合できます。
| ユースケース | 推奨メソッド | ポイント |
|---|---|---|
| フォームバリデーション | safeParse + flatten() |
fieldErrorsでフィールドごとにエラー表示 |
| APIレスポンス検証 | parse |
型アサーションの代替。型とデータの乖離を実行時に検出 |
| 環境変数検証 | safeParse + process.exit |
アプリ起動時に1回実行し、型安全なenvオブジェクトを生成 |
| 複雑なビジネスルール | refine/superRefine |
複数フィールド間の検証も可能 |
| データ変換 | transform + pipe |
入力型と出力型を分離して変換処理を型安全に実装 |
Zodのスキーマを中心に設計することで、型定義・バリデーション・エラーメッセージの三つをひとまとめに管理できます。TypeScriptのジェネリクスや型の絞り込み(Narrowing)と組み合わせると、さらに堅牢な型安全コードを実現できます。

