【TypeScript】Zod 完全ガイド|スキーマ定義・バリデーション・型推論・実務パターンを徹底解説

【TypeScript】Zod 完全ガイド|スキーマ定義・バリデーション・型推論・実務パターンを徹底解説 TypeScript

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】型の書き方 完全入門も合わせてご参照ください。

スポンサーリンク
  1. Zodとは
  2. インストールと初期設定
  3. 基本スキーマの定義
    1. 文字列スキーマの詳細バリデーション
    2. 数値スキーマの詳細バリデーション
  4. オブジェクトスキーマ(z.object)
    1. オブジェクトの各種メソッド
  5. 配列・タプル・Recordスキーマ
  6. Union・Intersection・Discriminated Union
    1. Unionスキーマ(z.union / .or)
    2. Intersectionスキーマ(z.intersection / .and)
    3. Discriminated Unionスキーマ
  7. optional・nullable・default・nullish・transform
    1. transformで値を変換する
    2. pipe でスキーマをチェーン接続
  8. refine・superRefine で独自バリデーションを追加
    1. refine: 単一のカスタムバリデーション
    2. superRefine: 複数フィールドにまたがる複雑なバリデーション
  9. parse・safeParse とエラーハンドリング
    1. parse の使い方(例外を使うパターン)
    2. safeParse の使い方(Result型パターン)
    3. ZodError のエラー整形メソッド
  10. 再帰スキーマ(lazy)と前処理(preprocess)
    1. z.lazy:再帰的なスキーマ定義
    2. z.preprocess:バリデーション前にデータを変換
  11. 実務パターン3本
    1. 実務パターン1:環境変数のバリデーション
    2. 実務パターン2:APIレスポンスの型安全な検証
    3. 実務パターン3:React Hook Form + Zodでフォームバリデーション
  12. エラーメッセージの日本語化
    1. 方法1:各スキーマに直接メッセージを指定
    2. 方法2:z.setErrorMap でグローバルに日本語化
  13. よくあるエラーと解決策
  14. よくある質問
  15. まとめ

Zodとは

ZodはColin McDonnell氏が開発した、TypeScript向けのスキーマバリデーションライブラリです。2024年現在、週1,500万ダウンロードを超える非常に人気の高いライブラリです。

特徴 内容
TypeScript-first TypeScriptのために設計。型推論が強力でジェネリクスを最大限活用
依存関係ゼロ 外部ライブラリへの依存がなくインストールが軽量
実行時バリデーション JavaScriptとして実行時にデータの形を検証できる
型の自動生成 z.inferでスキーマから型を生成。定義の二重管理が不要
豊富なAPI 文字列・数値・日付・配列・Union・変換など多彩なメソッドを提供
Yup・Joi との違い
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 の重要性
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);
strict・passthrough・strip(余分なキーの扱い)
デフォルトで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 vs superRefine の使い分け
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 vs transform
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レスポンスの型安全な検証

fetchaxios で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 };
}
外部APIの型アサーション(as)は危険
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>
  );
}
React Hook Form + Zod の利点
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 ライブラリ
より本格的な多言語対応には 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)と組み合わせると、さらに堅牢な型安全コードを実現できます。