Zod(ゾッド)は、TypeScriptの型定義とランタイムの検証を1つのスキーマにまとめられるライブラリです。この記事では、npm・yarn・pnpmでのインストールから、z.inferによる型推論、parseとsafeParseの使い分け、nullable・optional・nullishの違い、カスタム検証までを実コードで解説します。2025年5月に安定版が出たZod 4では文字列フォーマットのAPIとエラー指定が変わり、2026年9月時点のnpm最新版は4.6.5です。4.6で追加されたz.compileとvalidate、バンドルサイズを削るZod Mini、他ライブラリと互換を取るStandard Schemaまで、旧版からの書き換え点を含めて追いかけます。
まとめ:Zod 4.6系での導入から検証までの要点
- インストール:
npm install zod/yarn add zod/pnpm add zodの1行で導入できます。追加の依存やビルド設定は不要で、型定義は本体に同梱されているためTypeScriptからそのまま参照できます。 - 型推論:
z.infer<typeof schema>でスキーマからTypeScriptの型を生成し、型と検証ロジックの二重管理をなくします。 - データ検証:
parse()は失敗時にZodErrorをthrowし、safeParse()は{ success, data | error }を返すため try/catch なしで分岐できます。4.6で追加されたvalidate()は真偽値だけを返します。 - null許容の使い分け:
optional()は undefined、nullable()は null、nullish()は両方を許容します。キーの欠落だけを許して明示的な undefined を弾くならexactOptional()を使います。 - Zod 4系の変更:
z.string().email()はz.email()へ、エラー指定のmessageはerrorへ、ZodErrorの.format()はz.treeifyError()へ移りました。deepPartial()と1引数のz.record()は削除されています。 - 版の選び方:フロントで数kBを削りたいなら
zod/mini、そうでなければ通常のZodで構いません。検証が秒間数万回走る経路だけz.compile()を検討します。
Zodとは何か?型安全なバリデーションを実現するライブラリの概要
Zodは、TypeScript環境で型安全なスキーマ検証を行うためのライブラリです。スキーマを1つ書けば、そこから静的な型と実行時の検証の両方を取り出せます。JSONなどの外部データを受け取る場面では、従来は型定義と検証コードを別々に書く必要がありました。Zodはこの2つを1本化します。オブジェクト・配列・ユニオン型といった構造にも対応し、エラー文面の差し替えやnull許容の制御も細かく指定できます。公式リポジトリはMITライセンスで公開されており、依存パッケージを持たない設計です。
Zodの開発目的とTypeScriptの型システムに寄せた設計背景
Zodは、TypeScriptを前提とした検証ライブラリとして設計されています。型定義と検証ルールの一貫性を保つことが出発点にありました。YupやJoiといった先行ライブラリは柔軟さを持つ一方、TypeScriptの型との整合を完全には担保できず、型を手で書き直す作業が残りました。Zodではスキーマを定義するだけで、z.inferから型をそのまま取り出せます。同じ構造を2度書かずに済むため、スキーマ変更による型のずれが起きません。TypeScript本体のバージョン更新に追随する形で開発が続いており、型システムとの結びつきの強さがこのライブラリの中心にあります。
実行時の検証が必要な理由と静的な型だけでは防げない不具合の例
TypeScriptの型はコンパイル時に消えます。APIレスポンスに as User と型アサーションを書いても、実際に届いたJSONが仕様どおりかどうかは何も保証されません。外部APIの仕様変更で age が文字列で返ってきた場合、型の上では number のまま扱われ、計算結果が NaN になって初めて気づきます。Zodは境界で値を検証し、仕様に合わない入力をその場で弾きます。検証の対象は外部APIのレスポンス、フォームの入力値、環境変数、Webhookのペイロードなど、アプリケーションの外から入ってくるデータ全般です。境界だけに検証を置き、内側は推論された型で扱うのが基本形になります。
他のバリデーションライブラリと比べたZodの記述量と型推論の差
YupやJoiと比べたときのZodの違いは、型推論の精度と記述量です。Yupでも InferType で型を取り出せますが、オプショナル項目や変換を挟むと推論結果が期待とずれ、型注釈の補助が必要になる場面があります。JoiはTypeScriptの型生成を標準では持たず、型は別途手書きで定義する必要がある設計です。Zodは z.object({ name: z.string() }) と書いた時点で { name: string } が確定し、.optional() や .transform() を重ねても推論が追随します。実務上の差が出るのは、スキーマを変更したときです。型の修正漏れがコンパイルエラーとして表面化するため、変更範囲を機械的に追えます。
GitHubのスター数と週間ダウンロード実績から見る採用の広がり
Zodのリポジトリはcolinhacks/zodで公開され、スター数は4万を超える規模にあります。npmでの週間ダウンロード数は数千万回の水準で、TypeScript向けの検証ライブラリとしては最大規模です。採用事例としては、tRPCやReact Hook Formのリゾルバ、Next.jsのサーバーアクションの入力検証など、スキーマを受け取る側のライブラリがZodを第一級で扱う構成が定着しました。後述するStandard Schemaに対応したことで、Zodスキーマをそのまま受け取れるライブラリはさらに広がっています。採用判断の材料としては、更新頻度と周辺エコシステムの対応状況の2点を見ると実態に近い評価ができます。
Zodが選ばれる理由とフロントエンド開発における位置づけの整理
フロントエンドでZodが使われる典型は、フォーム入力の検証とAPIレスポンスの検証です。React Hook FormやTanStack Formはリゾルバ経由でZodスキーマを受け取れるため、UI側のルール定義とサーバー側の検証を同じスキーマで揃えられます。実装の詳細はReactのZodフォームバリデーション実装で確認できる内容です。バンドルサイズを気にする場面はありますが、通常のZodでもオブジェクト型を含むスキーマで13.1kB(gzip)程度と公式が実測値を示しており、削りたい場合はZod Miniという選択肢が別に用意されています。
ZodをTypeScriptプロジェクトに導入するためのインストール手順
Zodの導入は、パッケージマネージャで本体を入れるだけで完了します。npm・yarn・pnpmのいずれでも手順は変わりません。ランタイムの依存パッケージを持たないため、インストール後に追加設定を書く必要もありません。型定義ファイルは本体に同梱されており、@types系のパッケージを別途入れる必要はない構成です。ここでは実際に手を動かす順に、インストール、バージョン固定、tsconfig.jsonの確認、モノレポでの扱い、動作確認までを追います。
npm・yarn・pnpmを用いたZodの基本的なインストール方法
3つのパッケージマネージャそれぞれのコマンドは次のとおりです。いずれも1行で終わります。
# npm
npm install zod
# yarn
yarn add zod
# pnpm
pnpm add zod
# 導入後に版を確認する
npm ls zod
インストール後は import * as z from "zod" でスキーマ定義に入れます。公式のBasic usageで標準の書き方として示されているのが、この名前空間インポートです。旧来の import { z } from "zod" も動きますが、名前空間インポートのほうがツリーシェイキングの効きが読みやすくなります。
package.jsonでのバージョン固定とZod 4系への更新手順
2026年9月時点のnpm最新版は4.6.5です。版を固定したい場合は npm install zod@4.6.5 のように指定し、package.json のキャレット指定を外します。更新は npm update zod または pnpm update zod で行います。Zodはセマンティックバージョニングに従っており、4系の中のマイナー更新で既存コードが壊れる設計にはなっていません。ただし3系から4系への更新は破壊的変更を含むため、後述の移行章と公式のMigration guideを先に読んでから上げる手順を取ります。ロックファイルをコミットしておけば、CIと手元で同じ版が入ります。
TypeScriptとZodの連携で確認するtsconfig.jsonの設定
Zodの型推論を正しく効かせるには、tsconfig.json で strict を有効にします。特に strictNullChecks がオフだと、optional() や nullable() が生成する string | undefined といった型が潰れ、Zodを入れた意味が薄れます。Zod 4系はTypeScript 5.5以上を要求する構成で、それ未満では型定義の一部が解決できません。既存プロジェクトが古いTypeScriptで止まっている場合は、Zodを入れる前にコンパイラの更新を済ませます。TypeScriptの型の基礎についてはTypeScriptとは?型・型推論・型注釈の書き分けで整理しています。
モノレポや複数パッケージへZodを導入するときの依存関係の整理
モノレポでZodを使う場合、各パッケージが別々の版を持つと型の互換が崩れます。z.inferが返す型は内部のブランド型に依存するため、4.5系と4.6系が同居すると「同じに見えるのに代入できない型」が生まれます。pnpm workspacesならルートのpackage.jsonで版を揃え、overridesで単一版に固定する運用にしてください。スキーマそのものは共有パッケージへ切り出し、API層・クライアント層・テスト層から同じスキーマをimportする構成にします。この形にしておくと、リクエストとレスポンスの型がサーバーとクライアントで自動的に一致します。
インストール直後の動作確認と型補完が効かないときの確認の進め方
導入直後は、小さなスキーマを1つ書いて挙動を見ます。const schema = z.string() を定義し、schema.parse("hello") が値をそのまま返すこと、schema.parse(42) がZodErrorを投げることの2点を確認すれば、ランタイム側は動いています。型補完が効かない場合に見る箇所は3つです。TypeScriptのバージョンが5.5未満でないか、エディタが参照するTypeScriptがワークスペース版になっているか、skipLibCheckを切った状態で他ライブラリの型エラーが連鎖していないか。VS Codeなら「TypeScript: Restart TS Server」で解決する場合もあります。
Zodのスキーマ定義方法と基本型・オブジェクト構造の検証手順
Zodの使い方は、スキーマを組み立てるところから始まります。型に対応する関数(z.string()、z.number()、z.boolean()など)でスキーマを作り、入力がそれに合うかを検証する流れです。スキーマには制約や変換を重ねられるため、単なる型チェックを超えた条件も表現できます。公式のAPIリファレンスには利用できるスキーマ関数が一覧で並んでおり、迷ったときはここを引くのが早道です。この章では基本型から入れ子のオブジェクトまで、書き方を順に見ていきます。
文字列や数値などの基本型に対するスキーマの定義方法と制約の付け方
基本型の定義は直感的です。文字列は z.string()、数値は z.number()、真偽値は z.boolean() と書きます。制約は連鎖メソッドで足します。5文字以上なら z.string().min(5)、整数かつ0以上なら z.number().int().min(0) という具合です。Zod 4系で変わったのが文字列フォーマットの扱いで、メールアドレスは z.email()、UUIDは z.uuid()、URLは z.url() といったトップレベル関数に移りました。z.jwt()、z.creditCard()、z.iban()、z.e164() のように、実務でよく使う形式も標準で揃っています。
import * as z from "zod";
const Account = z.object({
id: z.uuid(),
email: z.email(),
homepage: z.httpUrl(),
age: z.number().int().min(0).max(120),
});
Account.parse({
id: "018f0a0a-2b4c-7d8e-9f01-23456789abcd",
email: "user@example.com",
homepage: "https://example.com",
age: 34,
});
配列やブール値の検証とz.arrayで要素数を制限する書き方
配列は要素の型を渡して z.array(z.string()) と定義します。要素数の下限と上限は .min() と .max() で指定でき、z.array(z.string()).min(1).max(5) と書けば、許容されるのは1件以上5件以下の文字列配列です。オブジェクトの配列も z.array(z.object({ ... })) の形でそのまま表現できます。真偽値は z.boolean() で、フォームの「同意する」チェックのように true だけを通したい場合は z.literal(true) を使うほうが意図が明確です。タグの一覧やチェックボックス群のように件数に上限があるデータでは、要素数の制約を最初から入れておくと、後段の表示処理で件数を気にせずに済みます。
z.objectによるオブジェクト構造の記述とstrictObjectの差
オブジェクトは z.object({ name: z.string(), age: z.number() }) のように書きます。ここで押さえておきたいのが未知キーの扱いです。z.object() は定義していないキーを黙って取り除きます。z.strictObject() は未知キーがあればエラーにし、z.looseObject() はそのまま通します。外部APIのレスポンスを受けるなら追加フィールドが増えても壊れない z.object()、設定ファイルの検証ならタイプミスを検出できる z.strictObject() という使い分けが実務に合う選択です。既存スキーマからの派生は .extend()、.pick()、.omit()、.partial() で作れます。なお、3系にあった deepPartial() は4系で削除されました。
Zodで使える連鎖メソッドと制約を積み重ねるときの書き順の考え方
連鎖メソッドは書いた順に評価されます。z.string().trim().min(1) と z.string().min(1).trim() では結果が変わり、前者は空白を除いた後の長さを見ます。ユーザー入力を扱うなら、前者が意図に合う評価順序です。数値には .int()、.positive()、.nonnegative()、.multipleOf() が用意されています。正規表現は .regex() で足せますが、標準のフォーマット関数で表現できるものはそちらを使うほうが、エラーコードが構造化されて後処理が楽になります。制約を重ねるときの順序は「正規化してから検証」が原則です。
初学者がつまずきやすいZodの構文ルールと実際の記述例の読み解き
最初に戸惑いやすいのが、スキーマと型が別物だという点です。const User = z.object({...}) で作った User は値であり、型として使うには type User = z.infer<typeof User> と別に書きます。同じ名前を値と型の両方に割り当てられるのはTypeScriptの仕様で、意図した書き方です。もう1つは、parse が入力をそのまま返すのではなく、変換後の値を返す点です。.trim() や .transform() を挟んだスキーマでは、戻り値を受け取って使わないと正規化が反映されません。戻り値を捨てて元の変数を使い続ける書き方は、見つけにくい不具合の原因になります。
z.inferによる型推論とZodスキーマからの型生成の仕組み
Zodの中心にあるのが、スキーマからTypeScriptの型を取り出す z.infer です。型と検証を別々に管理すると、片方だけ直して食い違う事故が起きます。スキーマを1本にすれば、構造を変えた瞬間に型も変わり、影響範囲をコンパイルエラーとして一覧することが可能です。変換を挟む場合は入力型と出力型が分かれるため、z.input と z.output の使い分けも合わせて押さえます。
z.inferの基本的な書き方とTypeScript型を取り出す手順
z.infer はスキーマから静的型を取り出すユーティリティ型です。スキーマを定義した後、type User = z.infer<typeof UserSchema> と書くだけで型が手に入ります。const UserSchema = z.object({ name: z.string(), age: z.number() }) に対しては { name: string; age: number } が得られます。typeof が必要なのは、UserSchema が値だからです。入れ子や配列、ユニオンを含む構造でも、推論結果はスキーマの形をそのまま写します。リファクタリング時にプロパティ名を変えれば、型を参照している箇所がすべてエラーになるため、修正漏れが残りません。
スキーマから型を生成する利点と型の二重定義を避ける設計の実例
型生成の利点は、定義を1箇所に集められることです。APIの入力検証を例に取ると、リクエストボディのスキーマを z.object() で定義し、その型を z.infer でハンドラの引数型に使えば、検証を通った値だけが型付きで流れます。フロントエンド側でも同じスキーマをimportすれば、送信するオブジェクトの形にも型による制約が有効です。OpenAPI定義を併用しているプロジェクトなら、スキーマからドキュメントを生成する経路も取れます。具体的な手順はZodからOpenAPIを生成する方法にまとめました。定義の源泉を1つに保つほど、仕様と実装のずれは起きにくくなります。
自動生成された型と手書きで定義した型との間で生じる整合性の違い
手書きの型定義とスキーマを両方持つと、更新のたびに2箇所を直す作業が発生します。片方を忘れれば、コンパイルは通るのに実行時に弾かれる、あるいはその逆という状態になります。z.inferを使えばこの乖離は構造的に起きません。プロパティ名の変更や型の変更は、スキーマを直した時点で型側へ伝播します。逆に、既存の型定義が先にあってそれに合わせたい場合は、後述の satisfies でスキーマ側が型に適合しているかを検査する方向を取ります。どちらの向きに寄せるかを最初に決め、プロジェクト内で混在させないのが運用上の要点です。
ユニオン型やリテラル型へz.inferを適用したときの型の結果
ユニオンやリテラルもそのまま型に落ちます。const Status = z.union([z.literal("active"), z.literal("inactive")]) に対して z.infer<typeof Status> は "active" | "inactive" になります。値の集合が決まっている場合は z.enum(["active", "inactive"]) のほうが簡潔で、.options から選択肢の配列を取り出すことも可能です。取り出した配列はセレクトボックスの選択肢生成にそのまま流せるため、定数の二重管理が消えます。ステータス値やカテゴリ識別子のように、UIとAPIの両方で同じ値の集合を使う箇所と相性の良い書き方です。
z.inputとz.outputの違いとtransformを挟む場合の型
.transform() を挟むと、入力の型と出力の型が食い違います。const s = z.string().transform((val) => val.length) の場合、受け取る値は string、返る値は number です。このとき z.input<typeof s> は string、z.output<typeof s> は number を返します。z.infer は z.output と同じです。フォームの送信値を数値へ変換する、日付文字列をDateへ変換するといった処理をスキーマに入れる場合、フォーム側の型には z.input、処理側の型には z.output を当てる必要があります。ここを取り違えると、変換前の値に変換後の型が付く状態になります。
parseとsafeParseによるZodのデータ検証とエラーハンドリング手法
検証の入口は parse と safeParse の2つです。parse は失敗時に例外を投げ、safeParse は結果オブジェクトを返します。4.6ではこれに validate が加わり、真偽値だけを返す3つ目の選択肢ができました。どれを使うかは、失敗が想定内か想定外か、エラーの詳細が必要かどうかで決まります。公式のBasic usageにはこの3つの使い分けが並べて示されています。
parseによる同期検証の書き方とZodError発生時の扱い方
parse は同期で検証し、合わなければ ZodError を投げます。const user = UserSchema.parse(data) と書き、失敗を捕まえたい場合は try/catch で囲む必要がある方式です。捕捉側では error instanceof z.ZodError で判定してから error.issues を読みます。この書き方が向くのは、失敗が起きたら処理を続ける意味がない箇所です。環境変数の検証、起動時の設定読み込み、内部で生成したデータの整合性チェックなどが該当します。逆に、ユーザー入力のように失敗が日常的に起きる経路で parse を使うと、例外処理が制御フローになってしまい読みにくくなります。非同期の検証を含む場合に使うのは parseAsync です。
safeParseで例外を投げずに成否を分岐する書き方と戻り値
safeParse は例外を投げず、判別可能な結果オブジェクトを返します。success が true なら data に検証済みの値が、false なら error にZodErrorが入る構造です。TypeScriptは result.success の判定で型を絞り込むため、分岐の内側では安全に result.data を触れます。
const result = UserSchema.safeParse(req.body);
if (!result.success) {
return res.status(400).json({
errors: z.treeifyError(result.error),
});
}
const user = result.data; // ここでは型が確定している
ユーザー入力やAPIリクエストの検証はこちらが基本形になります。非同期の検証を含むスキーマでは safeParseAsync を使います。
型の絞り込みと例外処理を両立させるための検証まわりの設計指針
検証の置き場所を決めるところから設計が始まります。境界で1度だけ検証し、内側は推論済みの型で扱う。この原則を守ると、同じデータを何度も検証する無駄が消えます。境界とは、HTTPハンドラの入口、外部APIのレスポンスを受け取った直後、フォーム送信のハンドラ、キューから取り出したメッセージの入口です。parseとsafeParseの選択は、その境界で失敗が想定内かどうかで決めます。想定外ならparseで落とし、監視に載せる。想定内ならsafeParseで分岐し、利用者へ返す文面を組み立てる。この2つを混ぜないだけで、エラー処理の見通しが変わります。
ネストしたオブジェクトでparseとsafeParseを選ぶ基準
入れ子の構造を検証すると、エラーは階層のどこで起きたかを issue.path に配列で持ちます。{ user: { profile: { age: "x" } } } なら path は ["user", "profile", "age"] です。画面の該当フィールドへエラーを結び付けるなら、このパスが要ります。したがって入れ子が深く、フィールド単位で表示を制御したい場面では safeParse を選びます。一方、階層のどこかが壊れていたら全体を捨てる扱いでよい内部データなら parse で十分です。判断の軸は入れ子の深さではなく、エラーを誰にどう見せるかにあります。
検証失敗時のログ出力とz.prettifyErrorによる整形の手順
ZodErrorをそのままログへ流すと読みにくいため、4系では整形関数が用意されています。z.treeifyError(error) はフィールド階層に沿った入れ子のオブジェクトを返し、APIのエラーレスポンスに向く形式です。z.prettifyError(error) は人が読む前提の複数行テキストを返し、サーバーログやCLIの出力に向きます。3系にあった error.format() と error.flatten() は非推奨になったため、書き換えの対象です。ログへ出す際は、入力値そのものを丸ごと記録しないよう注意します。パスワードやトークンが含まれる経路では、issue.path と issue.code だけを残す形が安全です。
nullable・optional・nullishの違いとZodでの適切な使い分け
値が「無い」ことを許す書き方は3つあります。optional()、nullable()、nullish() です。見た目が似ているため混同されますが、生成される型も検証結果も異なります。さらに4系では、キーの欠落だけを許して明示的な undefined を弾く exactOptional() が加わりました。フォームやAPIの仕様に合わせてこの4つを選び分けられると、検証の精度が一段上がります。
optionalとnullableの違いと生成される型への影響の比較
違いは許容する値そのものにあります。z.string().optional() の型は string | undefined で、値が未定義でも通ります。z.string().nullable() の型は string | null で、null という値そのものを許す定義です。この差はデータの出どころで決まります。JSONは undefined を表現できないため、APIレスポンスで「値が無い」を表すと null になります。一方、JavaScriptのオブジェクトでキー自体が無い状態は undefined です。RDBのNULL可能カラムをそのままJSONにすると null が来るため、レスポンスのスキーマは nullable() が実態に合います。
const A = z.object({ nickname: z.string().optional() });
const B = z.object({ nickname: z.string().nullable() });
A.parse({}); // OK(キーが無い)
A.parse({ nickname: null }); // エラー
B.parse({ nickname: null }); // OK
B.parse({}); // エラー(キーが必須)
nullishによるnullとundefinedをまとめて許容する書き方
nullish() は両方を受け入れます。z.string().nullish() の型は string | null | undefined となり、.optional().nullable() と同じ結果です。外部システムからのデータで null と undefined が混在する場合や、旧仕様との互換を保ったまま新しいフィールドを足す場合に使います。ただし許容範囲が広がるぶん、想定していない値が素通りする余地も増える点には注意が必要です。とりあえず nullish() を付けて回る運用は、検証を入れている意味を薄めます。どちらが来るか分からないのではなく、どちらも来ると確認できた箇所にだけ使うのが筋です。
任意入力のフォーム項目とスキーマの整合を取るときの選択の基準
HTMLのフォームは、未入力のテキスト欄を空文字として送ります。undefined でも null でもありません。そのため z.string().optional() を素直に当てると、空文字が「入力あり」として通ってしまいます。空文字を未入力として扱うなら、z.string().min(1).optional().or(z.literal("")) のような書き方か、前処理で空文字を undefined へ落とす変換が必要です。実装ではリゾルバ側の仕様も絡むため、React Hook Formと組み合わせる場合の具体的な書き方はReactのZodフォームバリデーション実装で確認できる内容です。フロントとサーバーで同じスキーマを共有するなら、この空文字の扱いは最初に決めておきます。
条件分岐を含むスキーマ設計でnullableを使う具体的な例
「未成年なら保護者の同意書が必須、成人なら不要」という要件を考えます。同意書のフィールドは nullable() で null を許しておき、年齢との関係を superRefine で検証します。フィールド単体の型だけでは表現できない条件を、スキーマ全体のレベルで受ける形です。
const Applicant = z
.object({
age: z.number().int(),
guardianConsent: z.string().nullable(),
})
.superRefine((val, ctx) => {
if (val.age < 18 && val.guardianConsent === null) {
ctx.addIssue({
code: "custom",
path: ["guardianConsent"],
message: "18歳未満の場合は保護者の同意が必要です",
});
}
});
exactOptionalでキー欠落と明示的なundefinedを分ける
4系で加わった exactOptional() は、キーが存在しないことは許すが、キーがあって値が undefined である状態は弾きます。z.string().optional() は両方を許すため、PATCHリクエストのように「指定されなかった項目は変更しない、明示的に消したい項目は null を送る」という仕様では区別が付きません。exactOptional() を使うと、送られてこなかった項目と、意図して空にした項目をスキーマの段階で分けられます。部分更新のAPIを設計する場面で効いてくる機能です。既存の optional() をすべて置き換える必要はなく、区別が要る箇所だけで使います。
refineとsuperRefineによるカスタム検証とエラー文面の設計
標準の制約で表現できない条件は、refine と superRefine で書きます。前者は1つの値に対する真偽判定、後者は複数フィールドをまたぐ条件や複数のエラーを同時に出す場面で使う関数です。合わせて、エラー文面の指定方法が4系で error パラメータへ統一された点も押さえます。ここは3系のコードをそのまま持ち込むと非推奨の警告が出る箇所です。
refineを用いたZodのカスタム検証の基礎と失敗時の戻り方
refine は真偽値を返す関数を受け取ります。z.string().refine((val) => val.length > 8, { error: "8文字を超える必要があります" }) のように書き、false を返した時点でエラーとなる仕組みです。チェーンで複数回重ねられるため、条件ごとに分けて書くと後から読みやすくなります。注意点は、refine が型を絞り込まないことです。検証は通っても、推論される型は元のままです。型レベルでも絞りたい場合は z.custom に型引数を与えるか、transform で変換後の型を明示します。非同期の判定(データベースの重複確認など)を書く場合は、parseAsync または safeParseAsync での呼び出しが必要になります。
superRefineで複数フィールドをまたぐ条件を検証する書き方
superRefine は、値と ctx を受け取り、ctx.addIssue() で任意の数のエラーを追加します。パスワードと確認用パスワードの一致、開始日と終了日の前後関係、選択した支払方法に応じた必須項目の切り替えなど、フィールド間の関係を表す条件はここに書きます。path を指定すると、エラーが特定のフィールドに紐づくため、画面側で該当欄の下に文面を出すことが可能です。エラーコードは "custom" のほか、"too_big" や "too_small" など組み込みのコードも指定できます。複数の違反を同時に検出して一度に返せるのが、refine を並べる書き方との違いです。
ZodErrorのissues構造を理解してフィールド単位で扱う手順
ZodErrorは issues 配列を持ちます。各要素は code、path、message、それに違反の種類ごとの追加情報(minimum、expected など)を含みます。3系にあった error.errors は削除されたため、参照は error.issues に統一する必要があるため注意してください。画面へ結び付けるときは path.join(".") をキーにしたマップへ変換すると扱いやすくなります。文面だけを並べるなら error.issues.map((issue) => issue.message) で足ります。エラーコードを見て処理を分けたい場合は、文面ではなく issue.code で分岐する設計にしてください。文面は多言語化で変わりうるため、判定条件に使うべきではありません。
errorパラメータへ統一されたエラー文面の指定方法と移行の要点
4系ではエラー指定が error パラメータに一本化されました。z.string().min(5, { message: "短すぎます" }) は z.string().min(5, { error: "短すぎます" }) と書きます。型そのものに対する文面は z.string({ error: (issue) => issue.input === undefined ? "必須項目です" : "文字列を入力してください" }) のように、関数で分岐させます。3系の invalid_type_error と required_error はこの形に置き換わり、errorMap も error へ改名されました。Migration guideに対応表が載っているため、既存コードの一括置換はそれを見ながら進めます。
多言語対応のためのlocales読み込みと動的なエラー文面の設計
4系は多言語のエラー文面を本体に同梱しています。import { ja } from "zod/locales" のように読み込み、z.config(z.locales.ja()) で既定の文面を日本語へ切り替えられます。プロジェクト側でi18nライブラリを使っているなら、文面を直接書く代わりに issue.code と issue.path から翻訳キーを組み立てる設計にすると扱いやすい構成です。この形なら、スキーマ定義に表示言語の知識を持ち込まずに済みます。ユーザーごとに言語が変わるサーバーサイドでは、リクエスト単位で文面を組み立てる処理を境界に置き、スキーマ自体は言語非依存のまま保ちます。
ネストしたデータ構造や再帰的なZodスキーマを定義する書き方
実際のアプリケーションが扱うのは、単純な型だけではありません。入れ子のオブジェクト、判別可能なユニオン、ツリー構造といった形が日常的に出てきます。Zodにはこれらを表現する関数が一通り用意されているため、表現が可能です。この章では、構造が複雑になったときにスキーマをどう組み立て、どう分割して保つかを見ます。
z.objectの入れ子構造で階層データを検証するスキーマの記述
入れ子は z.object() をそのまま入れ子にします。z.object({ name: z.string(), address: z.object({ city: z.string(), zip: z.string() }) }) と書けば、階層のまま検証されます。実務では内側のスキーマを名前付きで切り出し、外側から参照する形にするのが実務での組み立て方です。住所スキーマを const Address = z.object({...}) として定義しておけば、請求先と配送先の両方から使い回せます。z.infer で取り出す型も階層構造をそのまま保つため、TypeScript側の扱いは通常のオブジェクト型と変わりません。分割の粒度は、再利用する単位と一致させるのが目安です。
z.arrayやz.tupleを使った配列型の複雑な検証設計の書き方
同じ型の要素が並ぶなら z.array()、位置ごとに型が違う固定長なら z.tuple() を使います。z.tuple([z.string(), z.number()]) は1番目が文字列、2番目が数値である2要素の配列だけを通します。CSVの行やグラフの座標データのように、位置に意味がある構造が、この書き方の用途です。可変長の末尾を許す場合は z.tuple([z.string()], z.number()) の形でrest要素を指定できます。オブジェクトの配列は z.array(z.object({...})) で、要素数の制約と組み合わせれば「1件以上100件以下の明細行」といった業務上の制約もスキーマに書けます。
z.unionとz.discriminatedUnionの違いと使い分けの判断基準
z.union() は候補のスキーマを順に試し、どれか1つが通れば成功とします。単純ですが、失敗したときのエラーが全候補ぶん積み上がり、原因が読みにくくなります。候補のオブジェクトに共通の判別キーがあるなら z.discriminatedUnion("type", [...]) を使うのが適切です。判別キーの値でまず候補を1つに絞ってから検証するため、エラーが該当スキーマのものだけになり、処理も速くなります。イベント種別や支払方法のように type フィールドで分岐する構造では、こちらが既定の選択になります。判別キーを持たない異種の値を許す場合にだけ使うのが z.union() です。
再帰的なデータ構造をz.lazyとgetterで定義する2つの方法
カテゴリツリーやコメントの返信のように自己参照する構造は、z.lazy() で遅延評価にして定義します。4系ではオブジェクトのプロパティにgetterを使う書き方も追加され、型注釈が要らない場面が増えました。
type Category = {
id: string;
name: string;
children?: Category[];
};
const CategorySchema: z.ZodType<Category> = z.object({
id: z.string(),
name: z.string(),
children: z.array(z.lazy(() => CategorySchema)).optional(),
});
再帰の深さに上限がある場合は、入れ子の段数を検証する処理を superRefine で足しておくと、極端に深い入力による処理時間の増大を防げます。
巨大なスキーマを分割して見通しよく保つ命名と合成のうえの工夫
1つの z.object() に数十のフィールドを並べると、差分が読めなくなります。関心ごとにスキーマを分け、.extend() やスプレッド構文で合成する形にします。z.object({ ...BaseUser.shape, ...Profile.shape }) と書けば、元のスキーマを保ったまま合成することが可能です。命名はスキーマ側に Schema の接尾辞を付けるか、型側に付けるかをプロジェクトで統一します。混在させると、値と型のどちらを参照しているのか読み取れなくなります。.describe() で各フィールドに説明を持たせておくと、OpenAPI生成やフォームのラベル生成への転用も可能です。
TypeScriptの既存型との整合性チェックとsatisfiesの使い分け
既存の型定義が先にあるプロジェクトでは、スキーマを型に合わせる方向の検査が要ります。型とスキーマが食い違っていても、どちらも単体では正しく見えるため、実行して初めて気づく事故になります。ここで使うのがTypeScriptの satisfies 演算子です。スキーマが目的の型に適合しているかを、ビルド時に検査できます。
satisfies演算子の基本構文とZodスキーマへ適用するときの書き方
satisfies は、値がある型に適合していることを検査しつつ、推論された具体的な型を保つ演算子です。const config = {...} satisfies AppConfig と書くと、AppConfig に適合しなければコンパイルエラーになり、かつ config の型は書いたリテラルの形のまま残ります。Zodと組み合わせる場合は const UserSchema = z.object({...}) satisfies z.ZodType<User> という形で記述する方法です。型注釈で : z.ZodType<User> と書く方法もありますが、その場合はスキーマ固有のメソッドが型から消えます。satisfies なら .extend() などを後から呼べます。
型の重複を避けるための型定義とスキーマ設計を分けるときの考え
どちらを源泉にするかで設計が変わります。新規のコードならスキーマを源泉にし、z.infer で型を取り出すのが素直です。一方、既存の型定義が広く使われている、あるいは外部が公開する型に合わせる必要がある場合は、型を源泉にしてスキーマ側を satisfies で検査します。混在させると、どちらが正なのか判断できない箇所が生まれます。方針はディレクトリ単位でもよいので明文化しておいてください。判断に迷う場合は、その型が外部との契約かどうかを見ます。契約なら型が源泉、内部データならスキーマが源泉です。
ZodとTypeScriptの型定義を同期させる運用ルールの決め方
同期を保つ運用は、書き方のルールに落とすと安定します。スキーマは const XxxSchema、型は type Xxx = z.infer<typeof XxxSchema> と隣に並べて定義し、同じファイルからexportします。リクエストとレスポンスのスキーマは共有パッケージへ置き、サーバーとクライアントの両方から同じものをimportする構成です。スキーマを変更するプルリクエストでは、型エラーが出た箇所が影響範囲そのものになります。レビューでは、スキーマの差分と型エラーの発生箇所が一致しているかを見れば、修正漏れを検出できます。
ビルド時に検出できるスキーマと型の不整合についての検知の実例
具体例を挙げます。APIのレスポンス型に status: "active" | "inactive" | "pending" を追加したとします。スキーマ側が z.enum(["active", "inactive"]) のままなら、satisfies z.ZodType<ApiResponse> を書いていれば、その行でエラーとなる仕組みです。書いていなければ、実行時に pending が届いた瞬間に検証で弾かれ、本番で初めて気づきます。逆にスキーマ側だけ増やして型を直し忘れた場合も、z.infer を使っていれば型が自動で広がるため、分岐の網羅チェックがコンパイルエラーになります。どちらの向きでも、ビルド時に気づける構成にしておくことが検知の条件です。
型整合性の検査をCIへ組み込むときの手順と確認しておきたい項目
CIで実行するのは tsc --noEmit による型検査です。テストが通っていても型検査を省いていると、satisfies による検査は動きません。合わせて確認したい項目が3つあります。ワークスペース内のZodが単一版に揃っているか、skipLibCheck の設定で検査対象が意図どおりか、スキーマを含むパッケージが型検査の対象に入っているか。モノレポでパッケージごとに tsconfig.json が分かれている場合、ルートからの一括実行で漏れが出ることがあります。検査対象のファイル数をログに出しておくと、漏れに気づけます。
Yup・Joi・Valibot・ArkTypeとの比較とZodを選ぶ判断基準
TypeScript向けの検証ライブラリはZodだけではありません。YupとJoiは以前から使われてきた選択肢で、ValibotとArkTypeは後発です。選定は「型推論の精度」「バンドルサイズ」「エコシステムの対応」の3点で決まります。結論を先に書けば、特別な制約が無いプロジェクトではZodを選ぶのが妥当です。バンドルサイズが厳しい場合はValibotかZod Mini、型レベルの表現力を重視するならArkTypeが候補に入ります。
Yupとの違いを記述量とTypeScriptへの対応の観点から比較する
YupはReactのフォーム検証で長く使われてきました。JavaScript前提で設計された経緯があり、TypeScriptの型推論は後付けです。InferType で型は取れますが、オプショナル項目の推論が string | undefined ではなく緩い型になる、変換を挟むと推論が崩れるといった差が出ます。Zodは最初からTypeScript前提で設計されているため、この手のずれが起きません。既存プロジェクトがYupで動いているなら、無理に移行する必要はありません。新規にスキーマを足す箇所からZodへ寄せ、両者を並行させる移行の仕方が現実的です。
JoiとのNode.jsサーバー用途での違いと使い分けの判断材料
JoiはNode.jsサーバー向けの検証ライブラリで、日付や条件付き検証の記述力に定評があります。ただし型生成は標準では持たず、TypeScriptの型は手で書くことになります。ブラウザ向けのバンドルサイズも大きく、フロントエンドとスキーマを共有する構成には向きません。サーバー側だけで完結し、既にJoiで書かれた検証資産が積み上がっているなら、そのままで問題はありません。フロントとサーバーで同じスキーマを使いたい、あるいは型を1箇所に集めたいという要件が出た時点が、Zodへの切り替えを検討する境目になります。
Valibotとの違いとバンドルサイズを優先する場合の選び方
Valibotは関数型のAPIとツリーシェイキングを前提に設計された後発のライブラリです。使う機能ぶんだけがバンドルに入るため、フロントエンドのサイズ要件が厳しい場合に効きます。記述は pipe() で処理を並べる形で、Zodの連鎖メソッドとは書き味が違います。両者の具体的な差はValibotとは?Zodとの違いとpipe構文の使い方で比較しました。なお、サイズだけが動機なら、ライブラリを替えずに zod/mini へ切り替える選択肢もあります。エコシステムの対応状況まで含めて考えると、Zodの資産を捨てないほうが総合的に有利な場面は多くあります。
ArkTypeとの違いと型レベルの表現を重視する場合の選定基準
ArkTypeは、TypeScriptの型構文に近い文字列でスキーマを書くライブラリです。type({ name: "string", age: "number>0" }) のように、型定義とほぼ同じ見た目で検証ルールを表現できます。学習対象がTypeScriptの型構文そのものであるため、既存の型定義からの移植でも読みやすい記述が可能です。詳細はArkTypeとは?TypeScript実行時型検証の使い方とZodとの違いで扱っています。選定の軸は周辺ライブラリの対応です。フォームライブラリやAPIフレームワークがZodスキーマを直接受け取る前提で作られている場合、ArkTypeを選ぶと接続部分を自分で書くことになります。
ExpressやNext.jsのAPI検証でZodを組み込む実装パターン
APIの入力検証は、ハンドラの先頭で safeParse を呼び、失敗なら400を返す形が基本です。Expressならミドルウェアに切り出し、req.body・req.query・req.params それぞれのスキーマを受け取る汎用の関数を1つ作ると、各ルートの記述が1行で済みます。Next.jsのRoute HandlerやServer Actionsでも同じ考え方で、入口で検証してから業務処理へ渡します。REST APIとGraphQLで設計が変わる点はREST APIとGraphQLの違いと使い分けに整理しました。既存システムとのAPI連携やバックエンドの設計をまとめて相談したい場合は、API開発・システム連携で対応しています。
Zod 3からZod 4系への移行で変わったAPIと書き換えの勘所
Zod 4の安定版は2025年5月に公開され、同年7月にnpmの zod パッケージの既定が4系へ切り替わりました。3系のコードをそのまま4系で動かすと、非推奨の警告や型エラーが出る箇所があります。書き換えの対象は、文字列フォーマット、エラー指定、z.record、削除されたメソッド、既定値の扱いの5つに整理することが可能です。公式のMigration guideに全項目が載っているため、実作業はそれを参照しながら進めます。
文字列フォーマットがトップレベル関数へ移ったときの書き換えの要点
3系の z.string().email() は4系で非推奨になり、z.email() が推奨形です。同様に .uuid()、.url()、.ip() なども z.uuid()、z.url()、z.ipv4() というトップレベル関数へ移りました。単なる書き換えではなく、検証内容が厳密になった項目もあります。z.uuid() はRFC 4122に準拠する検証になり、従来のUUID風の文字列を通したい場合に使うのは z.guid() です。メールアドレスの正規表現も見直されているため、移行後にテストデータが弾かれるようになったら、この変更が原因である可能性を先に疑います。
errorへの統一とmessage指定からの具体的な書き換えの例
エラー文面の指定は error に統一されました。{ message: "..." } は動きますが非推奨です。型エラー用の invalid_type_error、必須エラー用の required_error、そして errorMap はいずれも廃止され、error に関数を渡す形へ集約されました。
// Zod 3
z.string({
required_error: "必須項目です",
invalid_type_error: "文字列を入力してください",
});
// Zod 4
z.string({
error: (issue) =>
issue.input === undefined ? "必須項目です" : "文字列を入力してください",
});
あわせて、スキーマに指定したエラーマップが parse() の引数で渡したものより優先される順序へ変わりました。共通の文面を全体に当てる運用では、この優先順位の変化で結果が変わる場合があります。
z.recordの引数変更とdeepPartialなど削除されたAPIの代替
z.record() は1引数の呼び出しが廃止され、キーと値の両方を渡す形になりました。z.record(z.string()) は z.record(z.string(), z.string()) と書き換えます。キーに z.enum() を渡した場合、4系では列挙した全キーが必須になります。一部だけを許す場合に使うのは z.partialRecord() です。削除されたAPIとしては deepPartial() が代表で、入れ子を含めて任意化する処理は各階層で .partial() を明示する形に置き換えます。ZodError の .errors も削除され、参照は .issues だけになりました。
defaultとprefaultの挙動差とtransformを挟む場合の注意点
.default() の意味が変わりました。4系では既定値が出力型に代入できる必要があります。z.string().transform((val) => val.length).default(0) のように、変換後の型に合う値を渡します。3系と同じく「変換前の値を既定値として与え、変換を通す」挙動が欲しい場合は、新設の .prefault() を使う方法が適切です。z.string().transform((val) => val.length).prefault("tuna") と書けば、既定値の文字列が変換を通って数値になります。変換を挟まないスキーマでは両者に差が出ないため、移行で問題になるのは transform と組み合わせている箇所だけです。
サブパス読み込みで新旧2つの版を併存させる段階的な移行の進め方
一括移行が難しい規模では、サブパスを使って段階的に進められます。npmの zod パッケージは zod/v3 と zod/v4 のサブパスを公開しており、同一パッケージ内で新旧のAPIを併存させることが可能です。移行の順序としては、まず4系へ上げてから zod/v3 で既存コードを動かし、ファイル単位で zod/v4 へ切り替える進め方が取れます。注意点は、異なる版のスキーマを混ぜて渡すと型が合わないことです。境界となるモジュールを決め、そこを越えるデータは検証済みの素の値として受け渡す設計にしておきます。
Zod MiniとStandard Schema対応で見直すバンドル戦略の判断
フロントエンドでZodを使うとき、必ず出てくるのがバンドルサイズの話です。4系ではこれに対する答えとして、同じ機能をツリーシェイキング前提の関数型APIで提供する zod/mini が用意されました。もう1つ、検証ライブラリ間の共通インターフェースであるStandard Schemaへの対応も進んでいます。この2つは、どのライブラリを選ぶかという判断そのものを軽くします。
Zod Miniの関数型APIと通常のZodで異なる記述方法の対比
Zod Miniは import * as z from "zod/mini" で読み込みます。機能は通常のZodと同等ですが、メソッドチェーンではなく関数の入れ子で書く方式です。z.string().optional().nullable() は z.nullable(z.optional(z.string())) になります。チェーンを持たないぶん未使用のコードがバンドルから落ちる、という設計です。書き味は冗長になり、エディタの補完からメソッドを探す発見しやすさも下がります。公式ドキュメントもこの点を明記したうえで、開発体験を優先するなら通常のZodを使うよう案内しています。
公式が公開しているバンドルサイズの実測値と削減幅の読み取り方
公式が示す実測値は次のとおりです。単純なスキーマでZod Miniが2.12kB(gzip)、通常のZodが5.91kBで、削減率は64%。オブジェクト型を含むスキーマではZod Miniが4.0kB、通常のZodが13.1kBになります。
| スキーマの内容 | Zod Mini | 通常のZod |
|---|---|---|
| 単純な文字列スキーマ | 2.12kB | 5.91kB |
| オブジェクト型を含む | 4.0kB | 13.1kB |
差は約9kB(gzip)です。この数字をどう読むかが判断の分かれ目になります。JavaScriptのバンドル全体が200kBある画面で9kBを削る意味は小さく、逆に低速回線向けに総量を数十kBへ抑える設計なら無視できない比率になります。
Zod Miniを選ぶ場面と選ばない場面を分ける具体的な条件
判断を言い切ります。バックエンドではZod Miniを使いません。サーバー上でのバンドルサイズは実行性能にほぼ影響せず、公式もAWS Lambdaで17kBの増加が起動時間へ与える影響を約0.6ミリ秒と見積もっています。書き味を捨てる理由になりません。フロントエンドでも、社内向け業務システムや管理画面のように回線と端末が安定している環境では通常のZodで十分です。Zod Miniを選ぶのは、低速なモバイル回線を主な利用環境として想定し、バンドル総量に明確な上限を設けているプロダクトに限ります。迷う段階なら通常のZodで書き始め、計測して問題が出てから切り替えます。
Standard Schemaの共通インターフェースがもたらす移植性
Standard Schemaは、検証ライブラリが共通で実装する最小限のインターフェースの仕様です。スキーマオブジェクトが ~standard プロパティを持ち、そこから入力型・出力型の推論と検証の実行、標準化されたエラーの取得ができます。ZodとValibot、ArkTypeがいずれも対応しているため、スキーマを受け取る側のライブラリは特定の実装に縛られずに済みます。利用者側から見た効果は、フォームライブラリやAPIフレームワークを選ぶときに「Zodに対応しているか」を確認する必要が減ることです。
ライブラリ側がStandard Schemaを前提に設計するときの利点
自分でライブラリや社内の共通基盤を書く場合、引数の型をZod固有の z.ZodType ではなくStandard Schemaのインターフェースで受けると、利用側が使う検証ライブラリを限定せずに済みます。呼び出し側がZodでもValibotでも、同じコードで動作する設計です。社内に複数のプロダクトがあり、それぞれ別の検証ライブラリを使っている状況では、この設計が共通化の突破口になります。逆に、Zod固有の機能(.extend() による合成や .describe() のメタデータなど)に依存する処理を書くなら、インターフェースを絞る意味は薄くなります。
z.compileとvalidateで検証コストを下げる実装上の判断基準
2026年9月9日に公開されたZod 4.6で、検証の速度に関わる2つの機能が入りました。真偽値だけを返す validate() と、スキーマから検証関数を事前生成する z.compile() です。数字だけを見ると大きな改善に見えますが、効く場面は限られます。この章では、どこで使うと意味があるのかを条件付きで整理します。
validateが真偽値だけを返す仕組みと短絡による速度差の理由
validate() は、入力が有効かどうかだけを返します。z.validate(z.string(), "hi") は true、z.validate(z.string(), 42) は false です。速い理由は2つあります。エラーオブジェクトを組み立てない点と、最初の違反を見つけた時点で処理を打ち切る点です。safeParse().success は全フィールドを検証してissuesを集めてから成否を返すため、失敗するとわかっている入力でも最後まで走ります。TypeScript側では型述語として働くため、if (Player.validate(data)) の内側で data の型が絞り込まれます。エラーの中身を使わない判定なら、これで十分です。
z.compileで検証関数を事前生成する手順と対象となるスキーマ
z.compile() は、スキーマを解釈しながら検証する処理を、専用の検証関数として事前に組み立てます。スキーマ定義を読む工程が実行時から消えるため、同じスキーマを繰り返し使う経路で差が出ます。
import * as z from "zod";
const Player = z.object({
username: z.string(),
xp: z.number(),
});
// 起動時に1度だけコンパイルしておく
const compiled = z.compile(Player);
// リクエストごとの検証はコンパイル済みの関数を使う
if (compiled.validate(payload)) {
payload.username;
}
コンパイルはモジュールの初期化時に済ませ、リクエストごとに呼ばない形にします。毎回コンパイルすると、事前生成の利点が消えます。
公式ベンチマークが示す倍率と実務で効く場面についての見極め方
公式が公開したベンチマークでは、不正な入力に対する safeParse().success との比較で、5キーのオブジェクトが21ナノ秒で16.3倍、3つのオブジェクトのユニオンが28ナノ秒で34.9倍という数字が出ています。ここで見るべきは倍率ではなく絶対値です。1回あたり数百ナノ秒の処理が数十ナノ秒になっても、HTTPリクエスト1回の処理時間がミリ秒単位なら体感差は出ません。効くのは、1リクエストで数千回以上の検証が走る経路です。具体的には、大量のレコードを1件ずつ検証するバッチ処理、ストリーム処理のメッセージ検証、WebSocketで高頻度に届くイベントの検証が該当します。
非同期のrefineを含む場合にvalidateAsyncへ切り替える条件
validate() は同期処理のみを扱います。スキーマに非同期の refine(データベースへの重複確認や外部APIの照会など)が含まれる場合は validateAsync() を使います。ただし、非同期の検証を含むスキーマで支配的になるのは、その外部呼び出しの待ち時間です。検証処理そのものを数十ナノ秒に縮めても、データベースへの問い合わせが数ミリ秒かかるなら意味はありません。非同期の判定はスキーマの外へ出し、スキーマは形の検証に専念させる設計のほうが、速度でも見通しでも有利になります。
速度改善を狙う前に確認しておきたい検証処理の実行回数とその粒度
最後に順序を言い切ります。z.compile() に手を付ける前に、検証が何回走っているかを数えてください。多くの遅さは、検証が速くないことではなく、同じデータを何度も検証していることから来ます。ハンドラの入口で検証し、内側の関数でも念のため検証し、レスポンスを返す前にもう一度検証する。この構造なら、検証を速くするより回数を減らすほうが効きます。境界で1度だけ検証する設計に直し、それでも足りない経路にだけ z.compile() を当てる。この順序を逆にすると、設計の問題を性能改善で覆い隠すことになります。
よくある質問
Zodの導入と使い方について、検索でよく見かける質問に答えます。バージョンに依存する内容は2026年9月時点の4.6系を前提としています。
Zodのインストールコマンドは?(npm・yarn・pnpm)
npm install zod、yarn add zod、pnpm add zod のいずれか1行で完了します。追加の依存や設定ファイルは不要で、TypeScriptの型定義も本体に同梱されている構成です。版を固定したい場合は npm install zod@4.6.5 のように指定します。2026年9月時点のnpm最新版は4.6.5で、TypeScript 5.5以上が必要です。
Zodの読み方は?
「ゾッド」と読みます。TypeScript向けのスキーマ宣言・検証ライブラリで、スキーマ定義と静的型付けを1つの記述にまとめられる点が特徴です。ランタイムの依存パッケージを持たず、MITライセンスで公開されています。
z.inferとは何ですか?
定義済みスキーマからTypeScriptの静的型を取り出すユーティリティ型です。type User = z.infer<typeof UserSchema> と書けば、スキーマを変更しても型が追従し、型とスキーマのずれを防げます。.transform() を挟んで入力型と出力型が異なる場合は、z.input と z.output で必要なほうを選びます。
optional・nullable・nullishの違いは?
optional() は undefined を、nullable() は null を許容します。nullish() は両方を許容し、.optional().nullable() と同じ意味です。4系で追加された exactOptional() は、キーの欠落だけを許し、キーがあって値が undefined の状態は弾きます。部分更新のAPIで「未指定」と「明示的な空」を分けたいときに使います。
parseとsafeParseはどう使い分けますか?
parse() は検証失敗時にZodErrorをthrowし、safeParse() は例外を投げずに { success: false, error } を返します。失敗が想定内のユーザー入力では safeParse、失敗したら処理を止めるべき環境変数や設定の読み込みでは parse を使います。エラーの中身が要らない判定なら、4.6で追加された validate() が最も軽い選択肢です。エラー整形には z.treeifyError() と z.prettifyError() を使います。
関連記事
- ReactのZodフォームバリデーション実装|React Hook Form連携とZod 4移行の注意点:本記事のスキーマ定義を、実際のReactフォームへ組み込む手順を扱っています。
- ZodからOpenAPIを生成する方法|zod-to-openapiとZod 4ネイティブ変換:定義したスキーマをAPIドキュメントへ展開する経路をまとめました。
- Valibotとは?Zodとの違いとpipe構文の使い方をv1系の実コードで解説:バンドルサイズを優先する場合の比較対象です。
- ArkTypeとは?TypeScript実行時型検証の使い方とZodとの違い:型構文に寄せた記法を採るライブラリとの違いを整理しています。
- React Hook Formとは?使い方・バリデーション・v7の書き方を実例で解説:Zodスキーマを受け取る側のフォームライブラリの基礎です。