zod の使い方

TypeScript-first schema declaration and validation library with static type inference

v4.3.6139.2M/週MITバリデーション
AI生成コンテンツ

この記事はAIによって生成されました。内容の正確性は保証されません。最新の情報は公式ドキュメントをご確認ください。

zod の使い方 — TypeScriptファーストなスキーマ定義&バリデーションライブラリ

一言でいうと

Zodは、TypeScriptの型推論と完全に統合されたスキーマ定義・バリデーションライブラリです。スキーマを一度定義するだけで、ランタイムバリデーションとTypeScriptの静的型の両方が手に入ります。

どんな時に使う?

  • APIリクエスト/レスポンスのバリデーション — 外部から受け取るJSONデータが期待通りの構造かをランタイムで検証したいとき
  • フォーム入力のバリデーション — React Hook FormやConformなどと組み合わせて、フォームの入力値を型安全に検証したいとき
  • 環境変数・設定ファイルの型安全な読み込みprocess.envの値をパースし、型付きのオブジェクトとして扱いたいとき

インストール

# npm
npm install zod

# yarn
yarn add zod

# pnpm
pnpm add zod

注意: この記事はZod v4(4.x系)を対象としています。v3以前とはAPIに差異がある部分があります。

TypeScript設定

tsconfig.jsonstrictモードを有効にすることが推奨されます。

{
  "compilerOptions": {
    "strict": true
  }
}

基本的な使い方

最も典型的なパターンは「スキーマを定義 → パース → 型推論」の3ステップです。

import { z } from "zod";

// 1. スキーマを定義
const UserSchema = z.object({
  id: z.number(),
  name: z.string(),
  email: z.string().email(),
  age: z.number().int().min(0).optional(),
});

// 2. スキーマからTypeScript型を推論
type User = z.infer<typeof UserSchema>;
// => { id: number; name: string; email: string; age?: number | undefined }

// 3. ランタイムでバリデーション(パース)
const result = UserSchema.parse({
  id: 1,
  name: "田中太郎",
  email: "tanaka@example.com",
  age: 30,
});
// => 成功すればバリデーション済みのオブジェクトが返る

console.log(result);
// { id: 1, name: "田中太郎", email: "tanaka@example.com", age: 30 }

不正なデータを渡すと例外がスローされます。

try {
  UserSchema.parse({ id: "not-a-number", name: "", email: "invalid" });
} catch (err) {
  if (err instanceof z.ZodError) {
    console.error(err.issues);
    // バリデーションエラーの詳細が配列で取得できる
  }
}

よく使うAPI

1. z.object() — オブジェクトスキーマの定義

最も頻繁に使うAPIです。ネストも自由にできます。

import { z } from "zod";

const AddressSchema = z.object({
  postalCode: z.string().regex(/^\d{3}-\d{4}$/),
  prefecture: z.string(),
  city: z.string(),
});

const PersonSchema = z.object({
  name: z.string().min(1, "名前は必須です"),
  address: AddressSchema,
});

type Person = z.infer<typeof PersonSchema>;

2. safeParse() — 例外を投げないバリデーション

parse()は失敗時に例外をスローしますが、safeParse()は結果オブジェクトを返します。実務ではこちらを使うケースが多いです。

import { z } from "zod";

const EmailSchema = z.string().email();

const success = EmailSchema.safeParse("user@example.com");
if (success.success) {
  console.log(success.data); // "user@example.com"
}

const failure = EmailSchema.safeParse("not-an-email");
if (!failure.success) {
  console.error(failure.error.issues);
  // [{ code: 'invalid_string', validation: 'email', message: 'Invalid email', path: [] }]
}

3. z.enum() / z.union() — 列挙型・ユニオン型

import { z } from "zod";

// 文字列リテラルの列挙
const RoleSchema = z.enum(["admin", "editor", "viewer"]);
type Role = z.infer<typeof RoleSchema>; // "admin" | "editor" | "viewer"

RoleSchema.parse("admin"); // OK
// RoleSchema.parse("superuser"); // => ZodError

// 判別ユニオン(discriminated union)
const EventSchema = z.discriminatedUnion("type", [
  z.object({ type: z.literal("click"), x: z.number(), y: z.number() }),
  z.object({ type: z.literal("keypress"), key: z.string() }),
]);

type Event = z.infer<typeof EventSchema>;

4. z.array() / z.record() — 配列・レコード型

import { z } from "zod";

// 配列
const TagsSchema = z.array(z.string()).min(1).max(10);
TagsSchema.parse(["typescript", "zod"]); // OK

// レコード(キーが文字列、値が数値の辞書)
const ScoresSchema = z.record(z.string(), z.number());
type Scores = z.infer<typeof ScoresSchema>; // Record<string, number>

ScoresSchema.parse({ math: 90, english: 85 }); // OK

5. .transform() / .refine() — データ変換とカスタムバリデーション

import { z } from "zod";

// transform: パース後にデータを変換
const StringToNumberSchema = z.string().transform((val) => parseInt(val, 10));
const num = StringToNumberSchema.parse("42"); // 42 (number型)

// refine: カスタムバリデーションルールを追加
const PasswordSchema = z
  .string()
  .min(8, "8文字以上で入力してください")
  .refine((val) => /[A-Z]/.test(val), {
    message: "大文字を1文字以上含めてください",
  })
  .refine((val) => /[0-9]/.test(val), {
    message: "数字を1文字以上含めてください",
  });

// superRefine: 複数フィールドにまたがるバリデーション
const SignupSchema = z
  .object({
    password: z.string().min(8),
    confirmPassword: z.string(),
  })
  .superRefine((data, ctx) => {
    if (data.password !== data.confirmPassword) {
      ctx.addIssue({
        code: z.ZodIssueCode.custom,
        message: "パスワードが一致しません",
        path: ["confirmPassword"],
      });
    }
  });

類似パッケージとの比較

特徴ZodYupJoiValibot
TypeScript型推論◎ 完全対応△ 部分的× 非対応◎ 完全対応
バンドルサイズ中(~13KB gzip)中(~12KB gzip)大(不向き)小(~2KB gzip)
ランタイムブラウザ/Node.jsブラウザ/Node.js主にNode.jsブラウザ/Node.js
API設計メソッドチェーンメソッドチェーンメソッドチェーン関数ベース
エコシステム非常に豊富豊富豊富成長中
学習コスト低い低い中程度低い

選定の目安:

  • TypeScriptプロジェクトでエコシステムの充実度を重視するなら Zod
  • バンドルサイズを極限まで削りたいなら Valibot
  • 既存のJavaScriptプロジェクトで使うなら YupJoi も選択肢

注意点・Tips

1. parse() vs safeParse() の使い分け

// ❌ try-catchで囲むのは冗長になりがち
try {
  const data = schema.parse(input);
} catch (e) { /* ... */ }

// ✅ safeParse()で結果を判定する方がスッキリ書ける
const result = schema.safeParse(input);
if (!result.success) {
  // エラーハンドリング
  return result.error.issues;
}
// result.data は型安全

2. .strip() / .strict() / .passthrough() で未知のキーを制御する

const Schema = z.object({ name: z.string() });

// デフォルト(strip): 未知のキーは除去される
Schema.parse({ name: "太郎", unknown: true });
// => { name: "太郎" }

// strict: 未知のキーがあるとエラー
const StrictSchema = Schema.strict();
// StrictSchema.parse({ name: "太郎", unknown: true }); // => ZodError

// passthrough: 未知のキーもそのまま通す
const PassSchema = Schema.passthrough();
PassSchema.parse({ name: "太郎", unknown: true });
// => { name: "太郎", unknown: true }

3. エラーメッセージの日本語化

const NameSchema = z.string({
  required_error: "名前は必須です",
  invalid_type_error: "名前は文字列で入力してください",
}).min(1, "名前を入力してください");

4. 再帰的な型(ツリー構造など)の定義

import { z } from "zod";

interface Category {
  name: string;
  children: Category[];
}

const CategorySchema: z.ZodType<Category> = z.object({
  name: z.string(),
  children: z.lazy(() => z.array(CategorySchema)),
});

5. パフォーマンスに関する注意

  • 大量のデータ(数万件の配列など)をバリデーションする場合、パフォーマンスに影響が出ることがあります。ホットパスでは必要最小限のスキーマに絞ることを検討してください。
  • z.inferはコンパイル時のみの処理なので、ランタイムコストはゼロです。

まとめ

Zodは「スキーマを書けば型が付いてくる」というDX(開発者体験)の良さが最大の魅力です。TypeScriptプロジェクトにおけるバリデーションのデファクトスタンダードと言える存在であり、React Hook Form・tRPC・Next.jsなど主要なエコシステムとの統合も充実しています。APIの境界やフォーム入力など「外部からのデータが入ってくる場所」には、積極的に導入を検討してみてください。

比較記事