Zod 4移行ガイド:Breaking Changes、パフォーマンス改善、新機能を完全解説
ここ3年間TypeScriptを書いてきた人なら、ほぼ確実にZodを使っているはずです。APIルート、フォームバリデーション、tRPCエンドポイント、環境変数パーサー。Zodはあらゆる場所に存在します。そしてZod 4が、そのほぼすべてを変えました。
数字を見てください。文字列パースが14倍高速化、配列パースが7倍高速化、コアバンドルは2.3倍縮小、TypeScriptのコンパイル速度は最大10倍向上。すごい改善なんですが、その代償としてbreaking changesが大量にあるんです。バージョン上げた瞬間、CIが真っ赤になります。
このガイドでは、すべてのbreaking changeを一つずつ解説し、before/afterコードを示し、チームが一週間スキーマエラーと格闘する羽目にならないための移行戦略も紹介します。
なぜここまで変わったのか
Zod 3が設計されたのは、TypeScriptの型システムがまだ成熟しておらず、バリデーションライブラリのバンドルサイズが大きな問題ではなかった時代です。でもエコシステムが進化して、コールドスタートが重要なサーバーレス、サイズ制限が厳しいEdgeランタイム、スキーマが何千もあるモノレポが当たり前になった今、Zod 3のアーキテクチャが限界を見せ始めたんです。
Zod 4は3つの根本的な問題を解決するために、ゼロから書き直されました:
- バンドルサイズ: Zod 3のメソッドチェーンAPIはツリーシェイキングがほぼ不可能でした。importひとつでライブラリ全体が引き込まれていました。
- パース性能: バリデーションパイプラインに不要なオブジェクト生成とプロトタイプチェーンのルックアップオーバーヘッドがありました。
- TypeScriptコンパイル速度: 複雑なZodスキーマが膨大な型インスタンスを生成し、大規模コードベースで
tscが極端に遅くなっていました。
根本的に解決するにはAPIを壊すしかなかった。一つずつ見ていきましょう。
Breaking Change #1: エラーパラメータの統合
最も多くのコードに影響する変更です。Zod 3ではエラーメッセージをカスタマイズする方法が3種類もあったんですよね:
// ❌ Zod 3 — 3種類のパラメータ const schema = z.string({ required_error: "名前は必須です", invalid_type_error: "名前は文字列である必要があります", }); const email = z.string().email({ message: "メールアドレスの形式が正しくありません" }); const age = z.number({ errorMap: (issue, ctx) => { if (issue.code === "too_small") return { message: "18歳以上である必要があります" }; return { message: ctx.defaultError }; }, });
Zod 4ではこれらがerrorパラメータひとつに統合されました:
// ✅ Zod 4 — 統合されたerrorパラメータ const schema = z.string({ error: "名前は必須です", }); const email = z.string().email({ error: "メールアドレスの形式が正しくありません", }); const age = z.number({ error: (issue) => { if (issue.code === "too_small") return "18歳以上である必要があります"; return "年齢が正しくありません"; }, });
messageプロパティは全メソッドで非推奨になりました。required_error、invalid_type_error、errorMapは完全に廃止。今後は統一されたerrorパラメータを使ってください。公式コードモドが大部分自動で処理してくれます:
npx @zod/codemod --transform v3-to-v4 ./src
ただし、カスタムerrorMapは手動確認が必要です。関数シグネチャが(issue, ctx) => { message: string }から(issue) => stringに変わっています。
Breaking Change #2: トップレベルフォーマットバリデーター
Zod 3では文字列フォーマットの検証にメソッドチェーンを使っていました。Zod 4ではよく使うものがトップレベル関数に昇格しました:
// ❌ Zod 3 — メソッドチェーン const emailSchema = z.string().email(); const uuidSchema = z.string().uuid(); const urlSchema = z.string().url(); // ✅ Zod 4 — トップレベル関数 const emailSchema = z.email(); const uuidSchema = z.uuid(); const urlSchema = z.url();
z.string().email()だとメール検証だけ必要でもZodStringクラス全体を引き込んでしまいます。トップレベル関数方式なら、バンドラーが使われていないコードを適切に除去できるんです。
重要: メソッドチェーン版(z.string().email())はZod 4でまだ動作しますが、公式に非推奨です。すぐに削除されることはありませんが、将来のメジャーバージョンで削除される予定です。
また、z.string().ip()とz.string().cidr()は完全に削除されました。それぞれz.ipv4()、z.ipv6()、z.cidrv4()、z.cidrv6()に置き換えてください。z.uuid()もRFC 9562/4122のvariant bitsを検証するより厳密な方式に変更され、緩いパターンが必要な場合は新しく追加されたz.guid()を使用してください。
Breaking Change #3: Coercionの入力型がunknownに変更
z.coerceネームスペースはZod 4でもそのまま残っています。何が変わったかというと、すべてのcoercedスキーマの入力型がunknownになったんです:
const schema = z.coerce.string(); type SchemaInput = z.input<typeof schema>; // Zod 3: string // Zod 4: unknown
coercionが実際に何をするかを考えれば当然ですよね。何でも受け取って変換を試みるわけですから。ただ、TypeScriptが入力型をnarrowしなくなるので、それに依存していたコードで型エラーが出る可能性があります。
z.coerce.number()、z.coerce.string()などのAPI自体は以前と同じように動作します。変わったのは推論される入力型だけです。
Breaking Change #4: Optional + Defaultの動作変更
地味に危険な変更です。Zod 3では.default()や.catch()があるスキーマに.optional()を付けると、存在しないプロパティは無視されていました。Zod 4ではデフォルト値が常に適用されます:
const schema = z.object({ theme: z.string().default("light").optional(), }); // Zod 3: { theme: undefined } → { theme: undefined } ← 存在しないプロパティは無視 // Zod 4: { theme: undefined } → { theme: "light" } ← デフォルト値が適用
動作は予測可能になりましたが、undefinedかどうかで「未提供」を判定していたコードは壊れます。
.default()に関するもう一つの変更: デフォルト値は、入力型ではなく出力型と一致する必要があります。Zod 3では.default()がデフォルト値をスキーマでパースしていましたが、Zod 4ではパースをスキップしてデフォルト値を直接返します:
// Zod 3: デフォルト値が入力型と一致、パースされる const schema = z.string() .transform(val => val.length) .default("tuna"); // string入力 → パース → 4 schema.parse(undefined); // => 4 // Zod 4: デフォルト値が出力型と一致、直接返される const schema = z.string() .transform(val => val.length) .default(0); // number出力、直接返される schema.parse(undefined); // => 0
古い動作(「プリパースデフォルト」)を再現するには、新しく追加された.prefault()を使用します:
// ✅ Zod 4: .prefault()で以前の.default()の動作を再現 const schema = z.string() .transform(val => val.length) .prefault("tuna"); // stringがtransformを通ってパースされる schema.parse(undefined); // => 4
Breaking Change #5: TypeScript Strictモード必須
Zod 4はtsconfig.jsonでstrict: trueが必要です。TypeScript 5.5以降が前提となっています:
{ "compilerOptions": { "strict": true, "target": "ES2022", "module": "ESNext" } }
新機能: @zod/mini
Edgeランタイムやサーバーレス環境でバンドルサイズが重要な場合、これが決め手になります。Zodのコアバリデーションをかなり小さなサイズで提供してくれるんです:
import { z } from "@zod/mini"; const UserSchema = z.object({ name: z.string().check(z.minLength(1)), email: z.string().check(z.email()), role: z.enum(["admin", "user", "viewer"]), });
| 機能 | zod | @zod/mini |
|---|---|---|
| コアバンドル | ~13KB gzip | ~5.5KB gzip |
.transform() | ✅ | ❌ |
.pipe() | ✅ | ❌ |
| JSON Schema生成 | ✅ | ❌ |
バリデーションだけで変換が不要なAPIルートハンドラーには、@zod/mini一択ですね。
新機能: ビルトインJSON Schema変換
もうzod-to-json-schemaをインストールする必要はありません。Zod 4はネイティブでJSON Schema生成をサポートしています:
const UserSchema = z.object({ id: z.number().int().positive(), name: z.string().min(1).max(255), email: z.email(), role: z.enum(["admin", "editor", "viewer"]), }); const jsonSchema = UserSchema.toJSONSchema();
逆方向も可能です:
const schema = z.fromJSONSchema({ type: "object", properties: { name: { type: "string" }, age: { type: "integer", minimum: 0 }, }, required: ["name"], });
OpenAPIスペック、JSON Schemaベースのフォームジェネレーター、AIツール定義(MCP、function calling)でZod↔JSON Schema変換が必要な場面で活躍します。
新機能: スキーマメタデータ
スキーマに強く型付けされたメタデータを付与できるようになりました:
const NameSchema = z.string().min(1).max(100).meta({ label: "氏名", placeholder: "山田太郎", helpText: "本名を入力してください。", }); const meta = NameSchema.meta(); // → { label: "氏名", placeholder: "山田太郎", ... }
スキーマ駆動のフォーム自動生成パターンが構築できます。メタデータは.optional()、.array()、.transform()などのスキーマ操作後も保持されます。
新機能: 国際化エラー
バリデーションエラーメッセージを言語別に自動翻訳するロケールシステムが搭載されました:
import { z } from "zod"; import { ja } from "@zod/locales/ja"; z.config({ locale: ja }); const result = z.string().min(5).safeParse("Hi"); // result.error.issues[0].message → "5文字以上である必要があります"
全スキーマにカスタムエラーマップをラップしていた日々は終わりです。
新機能: テンプレートリテラル型
Zod 4ではz.templateLiteral()が追加されました。特定のパターンに従う文字列を検証できます:
const hexColor = z.templateLiteral([ z.literal("#"), z.string().regex(/^[0-9a-fA-F]{6}$/), ]); hexColor.parse("#ff00aa"); // ✅ hexColor.parse("red"); // ❌ type HexColor = z.infer<typeof hexColor>; // => `#${string}`
CSS値、セマンティックバージョン文字列、APIエンドポイントパターンなどの構造化された文字列フォーマットを、TypeScriptの型推論付きで検証できます。
パフォーマンスベンチマーク
| 項目 | Zod 3 | Zod 4 | 改善幅 |
|---|---|---|---|
| 文字列パース | 1.0x | 14倍 | 1,300% |
| 配列パース | 1.0x | 7倍 | 600% |
| オブジェクトパース | 1.0x | 6.5倍 | 550% |
| バンドルサイズ | ~31KB gzip | ~13KB gzip | 2.3倍縮小 |
| TS型インスタンス | 1.0x | 最大10倍削減 | 900% |
TypeScriptのコンパイル速度改善は特に大きいです。スキーマが何百もあるモノレポでtscが47秒から5秒に短縮されたベンチマーク報告もあります。
移行戦略:安全なアプローチ
一括移行はやめましょう。フェーズを分けて進めるのが安全です。
フェーズ1: 事前準備
- TypeScript strictモードが有効か確認
- コードモドをdry-runで実行:
npx @zod/codemod --transform v3-to-v4 --dry-run ./src
- コードベースで
errorMapを検索し、手動移行が必要な箇所を把握 .optional().default()パターンの確認(v4で動作が異なる)- TypeScriptが5.5以上であることを確認
フェーズ2: コードモド実行
npx @zod/codemod --transform v3-to-v4 ./src git diff --stat
フェーズ3: 手動修正
# 残存するerrorMapを検索 grep -rn "errorMap" --include="*.ts" --include="*.tsx" ./src # z.coerceパターンを検索 grep -rn "z\.coerce\." --include="*.ts" --include="*.tsx" ./src # .optional().default()チェーンを検索 grep -rn "\.optional()\.default\|\.default(.*).optional" --include="*.ts" ./src
フェーズ4: テスト実行
テストスイートを実行してください。よくある失敗原因:
- エラーメッセージ形式の変更: 特定のエラーメッセージをassertしているテストが壊れる
- Coercion入力型の変更:
z.coerce.*の入力型がunknownに変更され、新たな型エラーが発生する可能性がある - Optional+Default: デフォルト値付きオプショナルフィールドの
undefinedチェックが変わる
フェーズ5: 新機能の段階的導入
zod-to-json-schema→.toJSONSchema()に置き換えz.registry()でスキーマメタデータを管理- バリデーション専用スキーマを
@zod/miniに切り替え z.templateLiteral()で構造化された文字列の検証
よくある落とし穴
落とし穴1: Peer dependency衝突
Zod 3に依存するライブラリ(旧バージョンのtRPC、react-hook-formアダプターなど)がZod 4をpeer dependencyとして受け付けない可能性があります:
npm ls zod
2026年3月時点で主要ライブラリの大半がZod 4に対応済み:tRPC v11+、@tanstack/react-form、react-hook-form v8 + @hookform/resolvers v4+。
落とし穴2: z.input / z.outputの型変更
z.infer<typeof schema>は変更なし。ただしz.inputやz.outputを使っている場合、defaultやtransform関連の型が異なる可能性があります。
落とし穴3: Discriminated unionのエラー構造変更
z.discriminatedUnion()は動作しますが内部最適化されています。失敗時のエラー構造(issueコードなど)に依存するコードがある場合はテストが必要です。
落とし穴 4: .passthrough()、.strict()、.strip()は非推奨
オブジェクトスキーマは引き続きデフォルトで未知のキーをstripしますが、.passthrough()、.strict()、.strip()はすべて非推奨になりました。Zod 4では.catchall()で明示的に未知キーを処理することが推奨されています:
// ❌ Zod 4で非推奨 const loose = z.object({ name: z.string() }).passthrough(); const strict = z.object({ name: z.string() }).strict(); // ✅ Zod 4推奨 const loose = z.object({ name: z.string() }).catchall(z.unknown()); const strict = z.object({ name: z.string() }).catchall(z.never());
これらのメソッドを多用している場合、deprecation警告が表示されます。移行計画を立ててください。
今アップグレードすべきか
今すぐ実行:
- 新規プロジェクト開始時(最初からZod 4を使う)
- Zodスキーマが原因でビルド時間が遅い場合
- Edge/サーバーレスにデプロイし、バンドルサイズを削減したい場合
- JSON Schema連携が必要な場合
もう少し待っても良い:
- 重要な依存関係がまだZod 3しかサポートしていない場合
- カスタム
errorMap関数が大量にある場合 - 移行に割くリソースがない場合
まとめ
Zod 4はTypeScriptバリデーション史上、最もインパクトのあるアップデートです。Breaking changesは多数ありますが、どれも理由があります。すべてはZodをより高速に、よりコンパクトに、よりTypeScriptネイティブにするためのものです。
移行パスは明確です:コードモドを実行し、手動修正をかけ、テストし、デプロイ。ほとんどのプロジェクトなら1日で移行完了できます。v4に上がれば、@zod/mini、ネイティブJSON Schema、メタデータシステム、テンプレートリテラル型、国際化エラーまで、サードパーティなしですべて利用可能になります。
まずはnpx @zod/codemod --transform v3-to-v4 --dry-run ./srcを実行してみてください。どれだけ自動処理されるか確認して、残りはgrepで修正すれば完了です。
関連ツールを見る
Pockitの無料開発者ツールを試してみましょう