# Zod 4 実践ガイド【2026年最新】— TypeScript型安全スキーマ検証・境界バリデーション・React Hook Form / 環境変数 / JSON Schema 連携

> 公式ドキュメント最新版（Zod 4 / zod@4.x）に忠実な実践ガイド。トップレベル文字列フォーマット（z.email 等）、error パラメータ統一、treeifyError/prettifyError/flattenError、z.toJSONSchema、メタデータ、Zod Mini まで、本番品質のコード例で「いつ・どこで・どう使うか」を解説。React Hook Form・環境変数・API境界・LLM構造化出力への応用も収録。

- 公開日: 2026-06-24
- 著者: 友田 陽大
- タグ: TypeScript, Zod, バリデーション, 型安全, Next.js, React, フロントエンド, アーキテクチャ設計
- URL: https://tomodahinata.com/blog/zod

## 要点

- Zod はスキーマを1つ書けば実行時バリデーションと静的な型が単一の正（SSoT）として手に入る
- 設計思想は Parse, don't validate で、信頼できない unknown を信頼できる型の値へ変換する
- Zod 4 では文字列フォーマットがトップレベル関数化（z.email 等）、エラー指定が error に統一された
- Zod の価値は信頼境界（フォーム・API・環境変数・外部SDK戻り値）にあり、型済み内部値の二重検証はアンチパターン
- 1つのスキーマを React Hook Form・環境変数の fail-fast・API境界・z.toJSONSchema による LLM 構造化出力まで使い回せる

---

この記事は Zod 公式ドキュメント（[Basics](https://zod.dev/basics)、[Defining schemas / API](https://zod.dev/api)、[Migration / v4](https://zod.dev/v4)、[Error customization](https://zod.dev/error-customization)、[Error formatting](https://zod.dev/error-formatting)、[JSON Schema](https://zod.dev/json-schema)、[Metadata](https://zod.dev/metadata)、[Zod Mini](https://zod.dev/packages/mini)）の最新版を一次情報として、実務で「どの場面で・どう書くか」を判断できるところまで踏み込みます。

---

## 1. なぜ Zod なのか（型と検証の「単一の正」）

TypeScript の型は**コンパイル時に消えます**。`tsc` を通れば型エラーは消えますが、実行時にやってくる JSON・フォーム入力・環境変数は**型システムの外側**から来るデータで、`any` あるいは `unknown` でしかありません。ここで多くのバグが生まれます。

```ts
// よくある事故：型を「主張」しているだけで、実際には検証していない
const res = await fetch("/api/user");
const user = (await res.json()) as User; // ← as は嘘をつける。実行時は何の保証もない
user.email.toLowerCase(); // email が undefined なら本番でクラッシュ
```

`as` は「コンパイラを黙らせる」だけで、**1バイトの検証も行いません**。Zod はこの境界に**実行時の検査**を差し込み、同時にその検査から**静的な型を導出**します。

```ts
import * as z from "zod";

const User = z.object({
  id: z.uuid(),
  email: z.email(),
  name: z.string().min(1),
});

type User = z.infer<typeof User>; // ← スキーマから型を生成（手書きの type 定義は不要）

const res = await fetch("/api/user");
const user = User.parse(await res.json()); // 実行時に検証し、user は User 型として安全
```

ポイントは **"Parse, don't validate"** という設計思想です。Zod は「正しいかどうかを `boolean` で返す」のではなく、「**信頼できない `unknown` を、信頼できる型の値へ変換する**」関数として働きます。検証を通過した先のコードは、型を前提に安心して書けます。これが Zod を入れる最大の理由です。

> **適用範囲の原則：Zod は信頼境界にだけ置く。** API・フォーム・環境変数・外部 SDK の戻り値など「外から来る `unknown`」を一度 `parse` したら、それ以降のアプリ内部は TypeScript の型に任せます。すでに型がついている値を関数のたびに再検証するのは、コストの無駄であり SRP 違反です。

---

## 2. 基礎：parse / safeParse と型推論

### 2-1. スキーマ定義とパース

```ts
import * as z from "zod";

const Player = z.object({
  username: z.string(),
  xp: z.number(),
});

// .parse(): 検証に成功すると「入力のディープクローン」を型付きで返す。失敗時は throw
Player.parse({ username: "billie", xp: 100 }); // => { username: "billie", xp: 100 }

// 非同期の検証（refine が Promise を返す等）を含む場合は parseAsync
await Player.parseAsync({ username: "billie", xp: 100 });
```

### 2-2. 例外を投げない `safeParse`（実務の既定）

API ハンドラやフォームでは、例外を投げる `.parse()` より、**結果オブジェクトを返す `.safeParse()`** が扱いやすく、制御フローが素直になります。返り値は判別可能なユニオンです。

```ts
const result = Player.safeParse({ username: 42, xp: "100" });

if (!result.success) {
  result.error; // ZodError インスタンス（result.error.issues に詳細）
} else {
  result.data; // { username: string; xp: number } として型がつく
}

// 非同期版
await Player.safeParseAsync(input);
```

`.parse()` で `try/catch` する場合は、`error instanceof z.ZodError` で握り、`error.issues`（検証問題の配列）を参照します。

### 2-3. 型推論：`z.infer` / `z.input` / `z.output`

スキーマから型を取り出すのが Zod の真骨頂です。**変換（`transform`）やデフォルト（`default`）があると入力型と出力型がズレる**ため、3つを使い分けます。

```ts
const FormSchema = z.object({
  // 入力は文字列、出力は数値（変換あり）
  age: z.string().transform((s) => Number.parseInt(s, 10)),
});

type In = z.input<typeof FormSchema>; // { age: string }   ← パース前（フォームの生値）
type Out = z.output<typeof FormSchema>; // { age: number }  ← パース後（業務ロジックが受け取る値）

// z.infer は z.output のエイリアス（= 検証後の最終型）
type Final = z.infer<typeof FormSchema>; // { age: number }
```

| ヘルパー | 何の型か | 主な用途 |
| ----------- | --------------------------- | ------------------------------------ |
| `z.input`   | パース**前**の入力の型      | フォームの生値・API の生ペイロード   |
| `z.output`  | パース**後**の出力の型      | 業務ロジックが受け取る確定値         |
| `z.infer`   | `z.output` と同じ           | 変換がない通常スキーマの型取得       |

---

## 3. 【Zod 4 の最新仕様】v3 からの差分マップ

既存記事の多くは Zod 3 のままです。ここを押さえると一気に最新になります。

| 項目 | Zod 3（旧） | Zod 4（最新） |
| ---------------------- | --------------------------------------- | ---------------------------------------------- |
| インポート | `import { z } from "zod"` | `import * as z from "zod"`（`{ z }` も可） |
| 文字列フォーマット | `z.string().email()` | **`z.email()`**（旧形は非推奨） |
| ISO 日付/時刻 | `z.string().datetime()` | **`z.iso.datetime()` / `z.iso.date()`** |
| エラーメッセージ指定 | `message` / `invalid_type_error` / `required_error` | **`error`（文字列 or 関数）に統一** |
| エラー整形 | `error.format()` / `error.flatten()` | **`z.treeifyError` / `z.flattenError` / `z.prettifyError`** |
| JSON Schema 変換 | 外部ライブラリが必要 | **`z.toJSONSchema()` を標準搭載** |
| メタデータ | `.describe()` のみ | **レジストリ + `.meta({ id, title, ... })`** |
| バンドル最適化 | — | **`zod/mini`（関数型・ツリーシェイク対応）** |
| `z.literal` | 単一値のみ | **複数値可** `z.literal([200, 201, 204])` |
| 環境変数 boolean | 自前実装 | **`z.stringbool()`**（"true"/"1"/"yes" 等） |

> **移行は段階的でよい。** 旧 API（`z.string().email()`、`message`）は非推奨ながら**まだ動く**ので、ビルドが壊れることはありません。新規コードから新形式に寄せ、触れたファイルを少しずつ直す（ボーイスカウト・ルール）のが安全です。

---

## 4. プリミティブ・文字列フォーマット・型強制（coerce）

### 4-1. トップレベル文字列フォーマット

Zod 4 では実務で多用するフォーマットが**トップレベル関数**になりました。`z.string().email()` のメソッド形より短く、ツリーシェイクにも有利です。

```ts
z.email(); // メール
z.uuid(); // UUID（z.uuidv4() / z.uuidv7() も）
z.url(); // URL（z.httpUrl() は http/https 限定）
z.e164(); // 電話番号（E.164）
z.jwt(); // JWT（z.jwt({ alg: "HS256" }) でアルゴリズム指定）
z.base64(); // Base64
z.ipv4(); // IPv4（z.cidrv4() で CIDR ブロック）
z.nanoid(); // nanoid / z.cuid2() / z.ulid()

// ISO 系は z.iso 名前空間に集約
z.iso.date(); // "YYYY-MM-DD"
z.iso.datetime(); // ISO 8601 日時
z.iso.time(); // "HH:MM:SS"

// メール正規表現は用途に応じて差し替え可能（厳密さ vs 寛容さのトレードオフ）
z.email({ pattern: z.regexes.html5Email }); // ブラウザ標準準拠
z.email({ pattern: z.regexes.rfc5322Email }); // RFC 5322 準拠
```

文字列の基本検証と変換も健在です。`min` / `max` / `regex` / `startsWith` などに加え、`trim()` / `toLowerCase()` などの**正規化を検証パイプラインに組み込める**のが地味に強力です（手作業の前処理を1か所に集約できる）。

```ts
// 例：入力を正規化してから検証（前後空白除去 → 小文字化 → メール形式）
const Email = z.string().trim().toLowerCase().pipe(z.email());
Email.parse("  Foo@Example.com "); // => "foo@example.com"
```

### 4-2. 型強制（coerce）と数値フォーマット

**「文字列で来るが数値として扱いたい」**——クエリパラメータ・環境変数・フォームの典型例には `z.coerce` が効きます。内部で `Number(input)` 等を通してから検証します。

```ts
z.coerce.number(); // Number(input) してから number 検証
z.coerce.boolean(); // Boolean(input)（注意：空文字以外は基本 true）
z.coerce.date(); // new Date(input)

// 数値の精密フォーマット（v4 で拡充）
z.int(); // 安全整数のみ
z.int32(); // 32bit 整数の範囲
z.number().positive().multipleOf(0.01); // 正の数・小数第2位まで（金額など）
```

> **`z.coerce.boolean()` の落とし穴：** `"false"` は**非空文字列なので `true`** になります。環境変数の `"true"/"false"` を正しく扱うには次章の `z.stringbool()` を使ってください。

---

## 5. オブジェクト設計：strict / loose / 合成 / 再帰 / 判別ユニオン

### 5-1. 未知キーの扱い（セキュリティに直結）

`z.object` は**既定で未知のキーを黙って除去（strip）**します。用途に応じて3モードを使い分けます。

```ts
z.object({ name: z.string() }); // strip（既定）：未知キーは出力から落とす
z.strictObject({ name: z.string() }); // strict：未知キーがあればエラー
z.looseObject({ name: z.string() }); // loose：未知キーをそのまま通す
```

外部からの入力では **`strictObject` で「想定外のフィールドを弾く」**のが防御的です（マスアサインメント対策）。一方、将来の後方互換を残したい公開 API のレスポンス検証では既定の strip が無難です。

### 5-2. スキーマの合成（DRY の要）

1つの基底スキーマから派生させることで、型定義の重複をなくします。

```ts
const User = z.object({
  id: z.uuid(),
  email: z.email(),
  name: z.string(),
  passwordHash: z.string(),
});

// 作成入力：id は不要、password は生文字列
const CreateUser = User.omit({ id: true, passwordHash: true }).extend({
  password: z.string().min(12),
});

// 更新入力：全フィールド任意
const UpdateUser = CreateUser.partial();

// 公開用：機密フィールドを除外
const PublicUser = User.omit({ passwordHash: true });

// 他にも：.pick() / .required() / .keyof() / .extend() / .catchall()
```

これらは**スキーマの代数演算**です。`User` を一度直せば派生先すべてに型と検証が伝播するため、ETC（Easy To Change）を満たします。

### 5-3. 再帰スキーマ（v4 でキャスト不要に）

Zod 4 では **getter** を使って、型キャストなしで再帰・相互再帰を表現できます。

```ts
const Category = z.object({
  name: z.string(),
  get subcategories() {
    return z.array(Category); // 自己参照
  },
});

// 相互再帰も自然に書ける
const User = z.object({
  email: z.email(),
  get posts() {
    return z.array(Post);
  },
});
const Post = z.object({
  title: z.string(),
  get author() {
    return User;
  },
});
```

### 5-4. 判別ユニオン（API 結果型の決定版）

「成功なら `data`、失敗なら `error`」のような**タグ付きユニオン**は `z.discriminatedUnion` が最適です。判別キーで分岐するので、通常の `z.union` よりエラーメッセージが的確で高速です。

```ts
const ApiResult = z.discriminatedUnion("status", [
  z.object({ status: z.literal("success"), data: z.string() }),
  z.object({ status: z.literal("error"), code: z.number(), message: z.string() }),
]);

const parsed = ApiResult.parse(payload);
if (parsed.status === "success") {
  parsed.data; // ← 型が success 側に絞り込まれる
} else {
  parsed.code; // ← error 側に絞り込まれる
}
```

Zod 4 では判別ユニオンの中に**ユニオンやパイプ、別の判別ユニオンをネスト**できるようになり、表現力が大きく上がりました。

---

## 6. カスタム検証と変換：refine / superRefine / transform / pipe

### 6-1. `refine`：単一の論理チェック

組み込みバリデータで表せないビジネスルールは `refine` で足します。`path` を指定すると、エラーを**特定フィールドに紐づけられる**（フォーム表示で重要）。

```ts
const SignUp = z
  .object({
    password: z.string().min(12),
    confirm: z.string(),
  })
  .refine((data) => data.password === data.confirm, {
    error: "パスワードが一致しません",
    path: ["confirm"], // confirm フィールドのエラーとして扱う
  });
```

### 6-2. `superRefine` / `check`：複数の問題を一度に追加

1回の検証で**複数のエラー**を出したい・コンテキストに応じて出し分けたい場合は `superRefine`（低レベル API。Zod 4 では `.check` も同系統）を使います。

```ts
const Password = z.string().superRefine((val, ctx) => {
  if (val.length < 12) {
    ctx.addIssue({ code: "custom", message: "12文字以上にしてください" });
  }
  if (!/[A-Z]/.test(val)) {
    ctx.addIssue({ code: "custom", message: "大文字を1文字以上含めてください" });
  }
  if (!/[0-9]/.test(val)) {
    ctx.addIssue({ code: "custom", message: "数字を1文字以上含めてください" });
  }
});
```

### 6-3. `transform` / `pipe` / `preprocess`：値の作り替え

検証と同時に**値を別の形へ変換**できます。`transform` は出力型を変え、`pipe` は「あるスキーマの出力を次のスキーマの入力につなぐ」合成です。

```ts
// transform：検証後に変換（入力 string → 出力 number）
const StrToLen = z.string().transform((s) => s.length);

// pipe：正規化 → 再検証の連結
const Slug = z
  .string()
  .trim()
  .toLowerCase()
  .pipe(z.string().regex(/^[a-z0-9-]+$/, { error: "英小文字・数字・ハイフンのみ" }));

// preprocess：検証「前」に前処理（型が不定な生入力の整形）
const Quantity = z.preprocess((v) => (v === "" ? undefined : Number(v)), z.number().int().positive());
```

> **使い分け：** 値を変えないチェックは `refine`、検証ついでに正規化したいなら `transform`/`pipe`、検証の前段で雑な入力を整えるなら `preprocess`。型を変えずに値だけ整えたい（かつ JSON Schema 生成も保ちたい）場合は v4 新設の `.overwrite()` が使えます。

---

## 7. エラー設計：メッセージ・整形・ローカライズ（日本語化）

ここは**ユーザー体験（UX）とアクセシビリティ（a11y）**に直結します。検証は「弾く」だけでなく「**何をどう直せばよいか**を伝える」ところまでが仕事です。

### 7-1. `error` パラメータへの統一

Zod 4 では、かつて3つに分かれていたエラー指定（`message` / `invalid_type_error` / `required_error`）が **`error` 一本**に統一されました。文字列でも関数でも渡せます。

```ts
// 文字列形：シンプルに固定メッセージ
z.string().min(5, { error: "5文字以上で入力してください" });
z.string({ error: "文字列を入力してください" });

// 関数形：状況（未入力 or 型不一致）で出し分け
z.string({
  error: (iss) => (iss.input === undefined ? "必須項目です" : "文字列で入力してください"),
});

// 検証コンテキストの活用（最小値などを動的に埋め込む）
z.string().min(8, {
  error: (iss) => `パスワードは${iss.minimum}文字以上必要です`,
});
```

### 7-2. エラーの整形：用途別に3つ

`ZodError` を**そのまま画面に出すのは禁物**です。用途に応じて整形します。

| 関数 | 出力 | 使う場面 |
| ----------------- | ------------------------------------------------------- | ------------------------------------ |
| `z.flattenError`  | `{ formErrors, fieldErrors }`（1階層）                  | **フォーム / API レスポンス（最頻出）** |
| `z.treeifyError`  | スキーマ構造を反映したネスト木 `{ errors, properties }` | 深くネストした複雑なスキーマ         |
| `z.prettifyError` | 記号付きの人間可読な文字列                              | CLI・ログ・デバッグ                  |

```ts
const result = SignUp.safeParse(input);
if (!result.success) {
  // フォーム用：fieldErrors.confirm?.[0] のように引ける
  const flat = z.flattenError(result.error);
  // => { formErrors: [...], fieldErrors: { confirm: ["パスワードが一致しません"] } }

  // ログ用：そのまま console に出せる読みやすい文字列
  console.error(z.prettifyError(result.error));
}
```

### 7-3. 日本語ロケールとグローバル設定

デフォルトの英語メッセージを**アプリ全体で日本語化**できます。組み込みロケールに `ja` が含まれます。

```ts
// app の最上位（例：layout やエントリ）で一度だけ設定
import * as z from "zod";
import { ja } from "zod/locales";

z.config(ja()); // 既定メッセージが日本語になる

// さらに横断ルールを上書きしたい場合
z.config({
  customError: (iss) => {
    if (iss.code === "invalid_type") return `型が不正です（期待: ${iss.expected}）`;
  },
});
```

エラーの**優先順位**は「①スキーマ定義のメッセージ → ②parse 時に渡したメッセージ → ③`z.config` のグローバル → ④ロケール既定」です。個別 > 全体の順で効くと覚えてください。

---

## 8. 応用①：React Hook Form 連携（`zodResolver`）

フォームこそ Zod の主戦場です。`@hookform/resolvers`（v5 は Zod 4 対応）の `zodResolver` を噛ませるだけで、**型・検証・エラーメッセージが一気通貫**になります。

```tsx
"use client";

import { useForm } from "react-hook-form";
import { zodResolver } from "@hookform/resolvers/zod";
import * as z from "zod";

const ContactSchema = z.object({
  name: z.string().min(2, { error: "お名前は2文字以上で入力してください" }),
  email: z.email({ error: "正しいメールアドレスを入力してください" }),
  message: z.string().min(20, { error: "20文字以上で具体的に入力してください" }),
});

type ContactInput = z.infer<typeof ContactSchema>;

export function ContactForm() {
  const {
    register,
    handleSubmit,
    formState: { errors, isSubmitting },
  } = useForm<ContactInput>({
    resolver: zodResolver(ContactSchema),
    mode: "onBlur", // フォーカスを外したタイミングで検証（過剰な再検証を避ける）
  });

  return (
    <form onSubmit={handleSubmit(async (data) => { /* data は ContactInput 型で安全 */ })} noValidate>
      <label htmlFor="name">お名前</label>
      <input
        id="name"
        {...register("name")}
        aria-invalid={!!errors.name}
        aria-describedby={errors.name ? "name-error" : undefined}
      />
      {/* a11y：エラーは aria-describedby で関連付け、role/aria-live で読み上げ */}
      {errors.name && (
        <p id="name-error" role="alert" aria-live="polite">
          {errors.name.message}
        </p>
      )}
      {/* email / message も同様 */}
      <button type="submit" disabled={isSubmitting}>送信</button>
    </form>
  );
}
```

押さえどころ：

- **`noValidate`** をフォームに付け、ブラウザ標準の検証 UI を切って Zod に一本化します（メッセージの一貫性）。
- **a11y の肝は3点セット：** エラー入力に `aria-invalid`、エラー文に `id` を振って `aria-describedby` で関連付け、`role="alert"` + `aria-live="polite"` でスクリーンリーダーに通知。
- **同じ `ContactSchema` をサーバー側でも `safeParse`** に使えば、クライアントを信用せずに二重で守れます（次章）。スキーマは1つ、適用は両端——これが DRY と安全の両立です。

---

## 9. 応用②：環境変数の起動時 fail-fast

「`process.env.X` が `undefined` で本番が落ちる」事故は、**起動時に一度だけ検証して撥ねる**ことで根絶できます。`z.coerce` と v4 新設の `z.stringbool()` が効きます。

```ts
// src/env.ts —— アプリ起動時に一度だけ評価される
import * as z from "zod";

const EnvSchema = z.object({
  NODE_ENV: z.enum(["development", "test", "production"]),
  DATABASE_URL: z.url(), // 形式まで検証
  PORT: z.coerce.number().int().positive().default(3000), // "3000"（文字列）→ 3000
  ENABLE_CACHE: z.stringbool().default(false), // "true"/"1"/"yes" → true、"false"/"0" → false
  SENTRY_DSN: z.url().optional(),
});

// 失敗したら「何の変数が・なぜ」ダメかを表示して即終了（fail-fast）
const parsed = EnvSchema.safeParse(process.env);
if (!parsed.success) {
  console.error("❌ 環境変数が不正です:\n", z.prettifyError(parsed.error));
  throw new Error("Invalid environment variables");
}

export const env = parsed.data; // 以降は env.PORT が number として型安全に使える
```

これで「秘密情報の欠落・URL の打ち間違い・boolean の解釈ミス」がデプロイ直後に**確定的に**見つかります。`.env` の値そのものはログに出さないこと（メッセージは変数名と理由に留める）。

---

## 10. 応用③：API 境界（Next.js Route Handler / Server Action）

サーバーは**クライアントを一切信用しない**のが原則です。Route Handler では受け取った `unknown` を必ず `safeParse` し、不正なら 400 を返します。

```ts
// app/api/contact/route.ts
import { NextResponse } from "next/server";
import * as z from "zod";

const ContactSchema = z.strictObject({
  // strictObject で想定外フィールドを拒否（防御的）
  name: z.string().min(2).max(50),
  email: z.email(),
  message: z.string().min(20).max(2000),
});

export async function POST(req: Request) {
  const json = await req.json().catch(() => null); // 壊れた JSON も握る
  const parsed = ContactSchema.safeParse(json);

  if (!parsed.success) {
    // クライアントには整形済みのフィールドエラーだけ返す（内部情報を漏らさない）
    return NextResponse.json(
      { error: z.flattenError(parsed.error).fieldErrors },
      { status: 400 },
    );
  }

  // parsed.data はここで完全に型安全。以降の処理は型を信頼してよい
  await sendEmail(parsed.data);
  return NextResponse.json({ ok: true });
}
```

Server Action でも同型のパターンが使えます。**「境界で1回 parse、内部は型に委ねる」**を徹底すると、コードベース全体の防御線が `route.ts` / action に集約され、レビューしやすくなります（SRP）。

---

## 11. 応用④：`z.toJSONSchema()` で OpenAPI・LLM 構造化出力へ

Zod 4 は **JSON Schema を標準で生成**できます。これにより、1つの Zod スキーマを「実行時検証」「TypeScript 型」「JSON Schema」の**3用途**で使い回せます。OpenAPI ドキュメント生成や、**LLM の構造化出力（structured output / function calling）のスキーマ**として極めて有用です。

```ts
import * as z from "zod";

const Article = z.object({
  title: z.string().meta({ description: "記事のタイトル", examples: ["Zod 4 入門"] }),
  tags: z.array(z.string()).meta({ description: "関連タグ" }),
  publishedAt: z.iso.datetime(),
});

// メタデータ（.meta）も JSON Schema に反映される
const jsonSchema = z.toJSONSchema(Article, { target: "draft-2020-12" });

// 入力 vs 出力の型差も制御できる（transform を含むスキーマで有用）
z.toJSONSchema(Article, { io: "input" }); // パース前の形
```

主なオプション：`target`（`draft-2020-12`〔既定〕/ `draft-07` / `openapi-3.0`）、`io`（`"input"` / `"output"`）、`unrepresentable`（`bigint` など表現不能型の扱い）、`reused: "ref"`（重複スキーマを `$defs` に抽出）。

> **生成AI 連携での要点：** LLM に「この JSON Schema に従って出力せよ」と渡し、返ってきた JSON を**同じ Zod スキーマで `parse`** すれば、モデル出力の妥当性を実行時に保証できます。スキーマ定義・プロンプト制約・出力検証が**単一の `Article` に集約**され、ズレようがありません。当方が手がける生成AI プロダクトでも、この「スキーマ駆動」を信頼性の土台にしています。

---

## 12. パフォーマンスと Zod Mini（バンドル最適化の判断）

Zod 4 はコア実装が刷新され、v3 比で**文字列約14倍・配列約7倍・オブジェクト約6.5倍**高速です。多くのアプリでは**無印の `zod` のまま**で十分です。

**フロントエンドのバンドルサイズが本当にクリティカル**（低速回線が主要顧客、など）な場合のみ、関数型・ツリーシェイク前提の **`zod/mini`** を検討します。

```ts
// 無印（メソッドチェーン・DX 重視）
import * as z from "zod";
const a = z.string().min(5).max(10).optional();

// Mini（関数合成・サイズ重視）。.check() に制約を渡す
import * as zm from "zod/mini";
const b = zm.optional(zm.string().check(zm.minLength(5), zm.maxLength(10)));
```

| | Zod（無印） | Zod Mini |
| -------------- | ------------------------- | --------------------------------- |
| API | メソッドチェーン | 関数合成 + `.check()` |
| バンドル（オブジェクト例） | 約 13.1kb | 約 4.0kb |
| DX / 自動補完 | ◎ | △（やや冗長） |
| 推奨 | バックエンド・大半の Web | サイズ厳格なフロント |

> **判断基準：** バックエンドではバンドルサイズの影響は無視できるため、迷わず無印。フロントでも**まず無印で計測**し、バンドル分析で Zod が支配的だと**実証できてから** Mini に切り替えるのが YAGNI に適います。Mini は英語ロケール未同梱なので、エラー文言は `z.config(...)` で明示設定が必要です。

---

## 13. ベストプラクティス＆アンチパターン

| やるべきこと（Do） | 避けるべきこと（Don't） |
| --------------------------------------------------------------- | ------------------------------------------------------------ |
| 信頼境界（API/フォーム/env/外部 SDK）で1回 `parse` する | アプリ内部の型付き値を関数ごとに再検証する |
| スキーマから `z.infer` で型を導出（単一の正） | `type` を手書きし、Zod スキーマと二重管理する |
| 同じスキーマをクライアントとサーバーの両端で使う | クライアントだけで検証し、サーバーは素通し |
| 外部入力は `z.strictObject` で想定外キーを弾く | 既定 strip 任せでマスアサインメントを許す |
| エラーは `flattenError` / `prettifyError` で整形して返す | `ZodError` を生で画面・レスポンスに出す |
| 文字列は新形式 `z.email()` / `z.iso.datetime()` | 非推奨の `z.string().email()` を新規に書く |
| env は起動時に `safeParse` で fail-fast | `process.env.X!` を `as` で握りつぶす |
| 変換は `transform`/`pipe`、前処理は `preprocess` と役割分担 | 1つの `refine` に検証も変換も詰め込む |
| `as` を使わず `parse` で `unknown` を型に変換 | `as User` で「検証した気」になる |

---

## 14. FAQ（よくある質問）

**Q. Zod と TypeScript の `type` / `interface` は何が違う？**
A. `type` / `interface` は**コンパイル時に消える**静的な約束で、実行時には1バイトも検証しません。Zod は**実行時に検証**し、その検証から**静的な型も導出**します。外から来るデータには Zod、内部のロジックには TypeScript の型、と役割分担するのが正解です。

**Q. Zod 4 で `z.string().email()` は使える？**
A. 動きますが**非推奨**です。新形式の `z.email()`（同様に `z.uuid()` / `z.url()` / `z.iso.datetime()`）を使ってください。短く、ツリーシェイクにも有利です。

**Q. `parse` と `safeParse` はどちらを使うべき？**
A. 例外で大域脱出したい簡易スクリプトなら `parse`、それ以外の実務（API・フォーム）では**結果オブジェクトを返す `safeParse` が既定**です。制御フローが素直になり、エラーを意図的に握りやすくなります。

**Q. エラーメッセージを日本語にしたい。**
A. `import { ja } from "zod/locales"` して `z.config(ja())` を**アプリ起動時に一度だけ**呼べば、既定メッセージが日本語になります。個別メッセージは各スキーマの `error` パラメータで上書きできます。

**Q. クライアントとサーバーで検証が二重になりませんか？**
A. それが正解です。スキーマ定義は**1つ**にし、クライアント（UX のため即時表示）とサーバー（クライアントを信用しないため）の**両端で適用**します。定義は単一なのでズレません（DRY）。

**Q. バンドルサイズが心配。Zod Mini を使うべき？**
A. バックエンドなら不要、フロントも**まず無印で計測**してから判断してください。Zod 4 の無印はすでに高速・軽量です。バンドル分析で Zod が支配的だと**実証できた場合のみ** `zod/mini` に切り替えます（YAGNI）。

**Q. Zod スキーマを OpenAPI や LLM の構造化出力に使える？**
A. `z.toJSONSchema()` で JSON Schema を生成できます。OpenAPI 連携、LLM の structured output のスキーマ提示と**出力の再検証**（同じスキーマで `parse`）に使え、定義が一元化されます。

---

## まとめ：スキーマは「アプリの信頼境界」である

Zod 4 を使いこなす鍵は、スキーマを「型を作る道具」ではなく「**信頼できないデータを、信頼できる型へ変換する境界線**」として設計することです。本記事の柱を振り返ると——

1. **"Parse, don't validate"** ——`as` をやめ、境界で `parse` して `unknown` を型に変換する。
2. **単一の正** ——スキーマ1つから `z.infer` で型を導出し、二重管理を撲滅する。
3. **Zod 4 の新形式** ——`z.email()`、`error` 統一、`flattenError`/`prettifyError` でエラーを正しく扱う。
4. **両端で守る** ——同じスキーマを React Hook Form と API 境界の両方に適用する。
5. **使い回す** ——env の fail-fast、`z.toJSONSchema()` による OpenAPI・LLM 連携まで、1つのスキーマを多用途に。

これらは小手先のテクニックではなく、型安全性・セキュリティ・保守性・回復性を同時に引き上げる「**設計の選択**」です。正しく適用すれば、ユーザー体験（的確なエラー表示）と開発体験（壊れにくい境界）の両方が底上げされます。

実際のプロダクトでは、ここに「**境界の網羅**」「**エラーの観測（ロギング/監視）**」「**スキーマの版管理**」まで含めて初めて本番品質になります。**型安全を軸にした境界設計やレビュー、既存コードの堅牢化が必要な場合は、お気軽にご相談ください。** 下記の事例では、業界の基幹業務を支える B2B SaaS を、型安全・回復性・保守性を重視して設計・実装した過程を紹介しています。
