Zod(ゾッド)は、TypeScriptの型とランタイムの入力検証を1つのスキーマで書けるnpmパッケージです。2026年10月25日時点の最新版は4.6.7(10月12日公開)で、npm install zodの1行で入ります。4.6.5以前はnpm auditで「high」と表示される脆弱性GHSA-mrhx-c7xc-m3w5の対象なので、既存のプロジェクトは版の確認から始めてください。この記事では、インストールと安全確認、z.inferによる型の取り出し、parse・safeParse・validateの使い分け、Zod 3から書き換えが必要なAPIまでを、Node.js 26とTypeScript 7.0.2で実際に動かしたコードで説明します。
まとめ:Zod 4.6.7で押さえる導入と検証の要点
- 導入:
npm install zod(pnpmはpnpm add zod、yarnはyarn add zod)。外部依存ゼロで型定義も同梱。tsconfig.jsonの"strict": trueが必須です。 - 版:4.6.6以上に上げます。4.6.5以前は、上限付きの配列でも、不正な要素を100万個渡すと要素ごとのエラーを組み立て、多量のメモリを消費します(掲載例の計測ではヒープが約320MB増加)。上限の無い配列は4.6.7でも直らないので、
.max()を他のチェックより前に書きます。 - 型:
type User = z.infer<typeof User>でスキーマから型を作ります。transformを挟むならz.inputとz.outputを分けます。 - 検証:想定外の失敗は
parse(例外)、ユーザー入力はsafeParse(結果オブジェクト)、真偽だけ欲しいならvalidate。ただしvalidateは変換後の値を返しません。 - Zod 3からの書き換え:
z.string().email()はz.email()へ、messageはerrorへ、.merge()は.extend()へ、error.format()はz.treeifyError()へ。
Zodの役割とnpmでの採用規模
TypeScriptの型注釈は、コンパイル後の実行時には何も保証しません。await req.json() as Userと書いても、ageに文字列が入っていればそのまま通ります。Zodはこの境界で値を検査し、合格した値だけに型を付けます。
npmの週間ダウンロード数は2026年10月16日〜22日で約2.2億回、9月27日〜10月3日は約3.9億回でした(npmのダウンロード統計APIの10月16日〜22日と9月27日〜10月3日の値)。GitHubのcolinhacks/zodはスター数44,191(2026年10月25日取得・MITライセンス)です。tRPCやReact Hook Formのリゾルバなど、多くのライブラリがZodのスキーマをそのまま受け取ります。型安全なAPI層との組み合わせはtRPCとは?REST・GraphQL・gRPCとの違いと型安全なAPI実装をv11の実コードで解説で扱っています。
npm・pnpm・yarnによるZodのインストール手順
npm install zod
pnpm add zod
yarn add zod
入る版はnpmのlatestタグで、2026年10月25日時点は4.6.7です。npm view zod dist-tagsを叩くとcanary(4.5.0-canary系)やbeta(4.1.13-beta.0)も並びますが、どちらもlatestより古い開発版のタグなので本番では使いません。入っている版はnpm ls zodで確かめます。他のライブラリが依存として古いZodを抱えている場合も、ここに木構造で出ます。npm自体の仕組みはnpmとは?CLIとレジストリの役割・package.jsonとv12の変更点を解説を参照してください。
公式の要件は2つです。TypeScriptはv5.5以降でテストされており、それより古い版は公式サポート外。そしてtsconfig.jsonで"strict": trueを有効にします。strictに含まれるstrictNullChecksが無効だと、nullableとoptionalの違いがz.inferの型に現れません。TypeScript側の設定の基本はTypeScriptとは?型・型推論・型注釈の書き分けとJavaScriptとの違い【2026年版】にまとめています。
読み込みは名前空間インポートが公式の書き方です。
import * as z from "zod";
const User = z.object({
name: z.string().min(1),
age: z.number().int().nonnegative(),
email: z.email(),
tags: z.array(z.string()).max(3),
});
int()は整数、nonnegative()は0以上(min(0)と同じ)、max(3)は配列の要素数の上限です。オブジェクトは既定で未知のキーを取り除いて通します。未知のキーをエラーにするならz.strictObject()、残して通すならz.looseObject()を使います(Zod 3の.strict()・.passthrough()は非推奨)。
TypeScript 7.1でZod 4.6.5以前が出すTS5115
2026年10月20日に公開されたTypeScript 7.1.0-betaでは、Zod 4.6.5以前の型定義がerror TS5115: Instantiations of the following types appear infinitely circularを出します。手元の再現では、skipLibCheckがfalseならimportしただけでZod側のschemas.d.ctsに、trueでもz.record(z.string(), z.json())の行に出ました。TypeScript 7.0.2では出ず、Zod 4.6.7なら7.1.0-betaでも出ません。修正は4.6.6に入ったPR #6645です。TypeScriptを7.1へ上げる前に、Zodを4.6.6以上にしておきます。
Zod 4.6.6の脆弱性修正とnpm installの安全確認
4.6.5以前が入ったプロジェクトでnpm auditを実行すると、次の警告が出ます。
# npm audit report
zod <4.6.6
Severity: high
Zod can exhaust memory while validating oversized constrained collections - https://github.com/advisories/GHSA-mrhx-c7xc-m3w5
GHSA-mrhx-c7xc-m3w5(CVE-2023-54404)はCVSS 3.1で7.5、対象は4.6.6未満、修正版は4.6.6です。アドバイザリの公開は2026年10月1日、修正版の公開は10月11日でした。--save-exactで版を固定しているとnpm audit fixだけでは上がらず、--forceを求められます。その場合はnpm install zod@latestで明示的に上げてください。
上限付きの配列・Set・Mapで起きていたメモリ枯渇
原因は、.max()・.length()・.size()の判定が全要素の検証後に走っていたことです。上限100を書いていても、各要素が検証に失敗する配列を100万要素渡すと、要素ごとのissueが100万件作られ、上限超過のissueも加わります。4.6.6のPR #6652で、上限の判定が要素の検証より前に移りました。次のコードで、4.6.5と4.6.7の違いを確かめられます。
import * as z from "zod";
const input = Array(1_000_000).fill(0); // 文字列の配列に数値を100万個
const schemas = {
max: z.array(z.string()).max(100),
unbounded: z.array(z.string()),
refineThenMax: z.array(z.string()).refine(() => true).max(100),
maxThenRefine: z.array(z.string()).max(100).refine(() => true),
};
for (const [name, s] of Object.entries(schemas)) {
const t0 = performance.now();
const r = s.safeParse(input);
console.log(name, r.error?.issues.length, r.error?.issues[0].code, (performance.now() - t0).toFixed(1));
}
ケース間の影響を抑えるため、Node.js 26.5.0の別プロセスで各ケースを1回ずつ計測した参考値です。実行時間とヒープ増分は、実行環境やGCのタイミングによって変動します。
| 書き方 | 4.6.5のissue数 | 4.6.5の時間 | 4.6.7のissue数 | 4.6.7の時間 |
|---|---|---|---|---|
.max(100) |
1,000,001 | 768ms | 1(too_big) | 1.6ms |
.max(100).refine() |
1,000,001 | 678ms | 1(too_big) | 1.6ms |
.refine().max(100) |
1,000,001 | 857ms | 1,000,001 | 723ms |
| 上限なし | 1,000,000 | 835ms | 1,000,000 | 648ms |
issueが100万件規模になったケースは、どれもヒープが約320MB増えました。
4.6.7に上げても直らない2つの書き方
表の下2行が、版を上げても残る穴です。PR #6652は対象を「他のどのチェックよりも前にある上限」に限っており、.refine()の後ろに書いた.max()は従来どおり要素の検証後に判定されます。上限を書いていない配列は、そもそも早く打ち切る根拠がないので変わりません。アドバイザリの説明文は上限の無い配列での攻撃を書いていますが、4.6.6で直ったのは上限付きの側です。同じCVEを扱うVulnCheckのアドバイザリは対象を「zod <= 4.6.7」とし、上限の無い既定の書き方を直した版はまだ無いと書いています。npm auditはGitHubのアドバイザリ(4.6.6未満)を参照するので、4.6.7に上げれば警告は消えますが、上限の無い配列はそのまま残ります。
外部から配列を受け取るスキーマには、z.array(...).max(n)を他のチェックより先に書きます。あわせてHTTPの入口でリクエストボディのサイズ上限を設け、検証ライブラリに届く前に量を止めます。
9月21日の不正コミットと配布物の確かめ方
4.6.6のリリースノートによると、漏えいしたGitの認証情報で9月21日にmainへ6件の不正コミットが押し込まれ、同日中にPR #6620で取り消されました。npmへは1度も公開されていません。海外の一部ニュースは「4.6.6にpreinstallスクリプトが入った」と報じましたが、npmから4.6.5〜4.6.7のtarballを取得してpackage.jsonを確認すると、preinstall・install・postinstallはどの版にもなく、依存パッケージもゼロでした。4.6.7と4.6.6の差は版番号の文字列だけです。
9月24日のPR #6622以降、CIはnpmへ直接公開できなくなりました。新しい版はnpmの承認待ちの列に置かれ、メンテナーが2FAで承認するまでインストールできません。手元で確かめられるのは署名と来歴(provenance)です。
npm audit signatures
// 1 package has a verified registry signature
// 1 package has a verified attestation
zod 4.6.7の来歴は、colinhacks/zodのmainブランチの.github/workflows/release.ymlからGitHubホストのランナーでビルドされたことを示します(npmのattestations APIで取得)。来歴は「どこでビルドされたか」の保証で、コードの安全性の保証ではありません。不正コミットがmainに残ったままビルドされても来歴は正しく付きます。依存の更新を自動で取り込む運用なら、公開から数日置いて入れる設定と、ソフトウェアサプライチェーンセキュリティとは?開発工程に組み込む防御と2026年規制対応の実装判断で扱っている確認工程を組み合わせてください。
z.inferによる型の取り出しとz.input・z.outputの違い
z.inferはスキーマから静的な型を取り出す型関数です。上のUserなら次の1行で型ができ、スキーマを直せば型も追随します。
type User = z.infer<typeof User>;
// { name: string; age: number; email: string; tags: string[] }
注意が要るのは.transform()を挟んだときです。入力と出力の型が変わるため、z.infer(=z.output)だけでは足りません。次の例は型の変化を示すためのもので、数値に変換できない文字列はNaNになります。実際の年齢入力では変換後に.pipe(z.number().int().nonnegative())などの検証を足します。
const Age = z.string().transform((s) => Number(s));
type AgeIn = z.input<typeof Age>; // string
type AgeOut = z.output<typeof Age>; // number
フォームの入力値の型にはz.input、検証後に処理する側の型にはz.outputを当てます。
parse・safeParse・validateの使い分け
3つとも同じスキーマで検証しますが、失敗の返し方と戻り値が違います。
| メソッド | 成功時 | 失敗時 | 向く場面 |
|---|---|---|---|
parse |
検証済みの値 | ZodErrorをthrow | 設定ファイル・内部データ |
safeParse |
{ success: true, data } |
{ success: false, error } |
フォーム・APIリクエスト |
validate(4.6〜) |
true |
false |
件数の多い振り分け |
失敗が起こり得る入力にはsafeParseを使い、try/catchなしでsuccessで分岐します(trueの枝ではdataに型が付きます)。parseは「ここで失敗したらバグ」という場所に置き、例外で処理を止めます。非同期のrefineを含むスキーマでは、それぞれparseAsync・safeParseAsync・validateAsyncを使います。
validateが返さない変換後の値と既定値
4.6.0(2026年9月9日)で加わったvalidateは真偽値だけを返し、エラーを組み立てない分だけ失敗時が速くなります。見落としやすいのは、型ガードが効くのは入力の型に対してで、変換後の値や既定値は手元に返ってこない点です。transformのコールバック自体は検証中に1回実行されますが(4.6.7で実測)、その結果で元の変数が置き換わることはありません。
const Age = z.string().transform((s) => Number(s));
const raw: unknown = "42";
if (Age.validate(raw)) {
console.log(typeof raw);
}
// string(numberにはならない)
const Cfg = z.object({ port: z.number().default(3000) });
const c: unknown = {};
console.log(Cfg.validate(c), c, Cfg.parse({}));
// true {} { port: 3000 }
transform・default・coerceを含むスキーマは、validateではなくsafeParseのdataを使ってください。
ZodErrorのissuesと表示用の整形
失敗の中身はerror.issuesの配列で、各要素がcode・path・messageを持ちます。冒頭のUserに不正な値を渡した結果です。
const r = User.safeParse({ name: "", age: -1.5, email: "x", tags: [] });
if (!r.success) {
console.log(r.error.issues.map((i) => [i.code, i.path]));
console.log(z.prettifyError(r.error));
}
// [ [ 'too_small', [ 'name' ] ], [ 'invalid_type', [ 'age' ] ], [ 'invalid_format', [ 'email' ] ] ]
// ✖ Too small: expected string to have >=1 characters
// → at name
// ✖ Invalid input: expected int, received number
// → at age
// ✖ Invalid email address
// → at email
浅いオブジェクトをフィールド単位で返すならz.flattenError()、入れ子を保って返すならz.treeifyError()、ログやCLIならz.prettifyError()です。Zod 3のerror.format()・error.flatten()は非推奨になりました。1行の文字列にしたい場合は外部パッケージのzod-validation-error(5.0.0、Zod 3.25以降と4系に対応)のfromError(err).toString()が使えます。
optional・nullable・nullish・exactOptionalの違い
「値が無い」の表し方は4通りあり、通す値が違います。
| 書き方 | 推論される型 | キー欠落 | undefined | null |
|---|---|---|---|---|
.optional() |
a?: string | undefined |
通す | 通す | 弾く |
.nullable() |
b: string | null |
弾く | 弾く | 通す |
.nullish() |
c?: string | null | undefined |
通す | 通す | 通す |
.exactOptional() |
d?: string |
通す | 弾く | 弾く |
DBのNULL許容カラムをそのまま返すAPIならnullable、未入力をundefinedまたはキー欠落として扱うフォームならoptional(空文字列は別途処理)、両方が混ざる外部APIならnullishです。exactOptionalはPATCHリクエストのように「キーが無い=変更しない」「undefinedを明示=不正」と区別したいときに使います。4.6.7でも、キーを省いた入力は合格し、undefinedを明示した入力は不合格でした。静的な型でもこの区別を適用するには、tsconfig.jsonで"exactOptionalPropertyTypes": trueを有効にします。
refine・superRefineによるカスタム検証とエラー文面の日本語化
型だけで表せない条件はrefineで足します。複数フィールドにまたがる条件はオブジェクト側に付け、pathでエラーを出す欄を指定します。
const Signup = z
.object({
password: z.string().min(8, { error: "8文字以上で入力してください" }),
confirm: z.string(),
})
.refine((v) => v.password === v.confirm, {
error: "パスワードが一致しません",
path: ["confirm"],
});
1つの条件で複数のissueを出し分けたいときはsuperRefineを使い、ctx.addIssue()を必要な回数呼びます。エラー文面の指定はZod 4でerrorに統一されました。messageはまだ動きますが非推奨で、Zod 3のinvalid_type_error・required_error・errorMapは廃止され、errorに関数を渡す形に置き換わっています。
既定の英語メッセージは、同梱のロケールで日本語に切り替えられます。
import * as z from "zod";
import { ja } from "zod/locales";
z.config(ja());
z.string().safeParse(1).error?.issues[0].message;
// 無効な入力: stringが期待されましたが、数値が入力されました
z.config()はグローバル設定なので、ユーザーごとに言語が変わるサーバーでは切り替えに使わず、issue.codeとissue.pathから自前のi18nキーを組み立てるほうが安全です。
スキーマの合成:extend・intersection・union・再帰
merge非推奨とextend・intersectionの違い
Zod 3で使われていた.merge()はZod 4で非推奨になり、A.extend(B.shape)が推奨形です。z.intersection(A, B)でも合成できますが、結果はZodObjectではないため.pick()・.omit()・.shapeが使えません(実測でundefined)。さらに同じキーの型が食い違うと、どんな値も通らないスキーマができます。
const A = z.object({ id: z.number() });
const B = z.object({ name: z.string() });
const AB = A.extend(B.shape);
const X = z.intersection(z.object({ v: z.string() }), z.object({ v: z.number() }));
X.safeParse({ v: "a" }).success;
// false(stringでもnumberでも通らない)
オブジェクトの合成はextend、intersectionはextendで表せない組み合わせ(オブジェクトとz.record()など)に限ります。
discriminatedUnionと再帰スキーマ
型フィールドで分岐するデータはz.union()よりz.discriminatedUnion("type", [...])が向きます。判別キーを先に見て該当するスキーマだけを検証するため、エラーが「全候補で失敗」ではなく該当候補の中身で返ります。型の考え方はDiscriminated Unionとは?判別可能なユニオン型の書き方とneverでの網羅チェックで解説しています。
ツリー構造のような再帰は、Zod 4ではゲッターで書けます。z.lazy()も引き続き使えますが、ゲッターならz.inferが型注釈なしで再帰型を推論します。
const Category = z.object({
name: z.string(),
get children() {
return z.array(Category);
},
});
type Category = z.infer<typeof Category>;
Zod 3からZod 4系への書き換え表
Zod 4の安定版は2025年5月19日に[email protected]のサブパスzod/v4として公開され、2025年7月の[email protected]でパッケージ本体がZod 4になりました。現在もzod/v3から旧APIを読み込めるので、ファイル単位で段階的に移行できます。全項目は公式の移行ガイド(Migration guide)にあります。
| Zod 3 | Zod 4 | 状態 |
|---|---|---|
z.string().email() |
z.email() |
非推奨(次のメジャーで削除) |
z.string().uuid() |
z.uuid()/z.guid() |
RFC準拠に厳格化 |
{ message: "..." } |
{ error: "..." } |
非推奨 |
invalid_type_error等 |
errorに関数を渡す |
廃止 |
A.merge(B) |
A.extend(B.shape) |
非推奨 |
.strict()/.passthrough() |
z.strictObject()/z.looseObject() |
非推奨 |
error.format()/.flatten() |
z.treeifyError() |
非推奨 |
z.record(値) |
z.record(キー, 値) |
1引数は廃止 |
.deepPartial() |
z.deepPartial(schema)(4.5以降) |
旧メソッドは削除 |
「zod email 非推奨」の警告は1行目で、z.string().email()は4.6.7でも動きますが型定義の@deprecatedでエディタに打ち消し線が出ます。挙動が変わって事故になりやすいのは2つです。z.uuid()はRFC 9562/4122に沿ってバリアント桁まで検査し、8-4-4-4-12の形だけ合う文字列を弾きます(実測で00000000-0000-0000-0000-000000000001はz.uuid()で不合格、z.guid()で合格)。z.record(z.enum(["a", "b"]), ...)は値スキーマが必須なら列挙した全キーを要求し、一部のキーだけを許すならz.partialRecord()を使います。React Hook Formと組み合わせている場合の移行手順はReactのZodフォームバリデーション実装|React Hook Form連携とZod 4移行の注意点にまとめています。
z.compileとZod Miniの採用条件
4.5で入ったz.compile()は、スキーマの構造に合わせた検証関数をnew Functionで生成し、有効な入力をその高速経路で通します。npmパッケージに同梱されているREADMEのAOT compilationの節では、55スキーマのベンチマークで中央値2.4倍、20キーのオブジェクトやオブジェクトの大きな配列で約9倍、素のz.string()では効果なしとされています。倍率はスキーマと計測条件で変わるので、公式ドキュメントの数値も目安として読みます。効く範囲は限られます。
- 無効な入力はほぼ速くならない:高速経路で失敗すると通常のパーサーでやり直してエラーを作るためです。失敗の判定を速くしたいなら
validateと組み合わせます(4.6のリリースノートでは無効な入力に対してsafeParse().success比で最大35倍)。 - 非同期の
refine・transformを含むとコンパイルされない:エラーにはならず、元のスキーマがそのまま返ります(4.6.7でもz.compile(asyncSchema) === asyncSchemaはtrue)。確かめるならz.compile(schema, { strict: true })でZodCompileAsyncErrorを投げさせます。 new Functionを禁じるCSP環境では外す:z.config({ jitless: true })でimport "zod/compile"による全体のコンパイルは止まりますが、z.compile()を直接呼んだ箇所は自分で外します。
小さなスキーマをリクエストごとに数回検証する程度なら、先に同じデータを何度も検証していないかを見直して境界の1回に減らします。z.compile()は、数万件単位のバッチやストリームの検証で詰まる経路にだけ当てます。
バンドルサイズを削る手段はzod/miniです。z.string().check(z.minLength(1))のように関数を組み合わせて書き、ツリーシェイキングが効きます。公式の計測ではz.boolean().parse(true)をバンドルした場合、Zod Miniが2.12kB、通常のZodが5.91kB(gzip)です。すでにZodの書き方で大量のスキーマを抱える画面では、書き換えのコストに見合いません。最初から関数型APIで小さく作るならValibotとは?Zodとの違いとpipe構文の使い方をv1系の実コードで解説、型構文に近い書き方ならArkTypeとは?TypeScript実行時型検証の使い方とZodとの違いが比較先です。複数のライブラリのスキーマを共通の型で受け取る仕組みはStandard Schemaとは?Zod・Valibot・ArkTypeを共通で受け取る仕組みと実装例で扱っています。型をコードでなくJSON Schemaとして他言語やOpenAPIと共有したい場合の選択肢はAjvとは:JSON SchemaをコンパイルするNode.jsの検証ライブラリの使い方とエラー整形にまとめています。
よくある質問
Zodのインストールコマンドは?
npm install zod、pnpm add zod、yarn add zodのいずれかです。2026年10月25日時点の最新版は4.6.7で、TypeScript 5.5以降とtsconfig.jsonの"strict": trueが要件です。
Zod 4.6.6・4.6.7の修正範囲と、更新後に必要な対策は?
4.6.6以上を使います。4.6.6はGHSA-mrhx-c7xc-m3w5の修正版で、9月21日の不正コミットはnpmに公開されておらず、4.6.6と4.6.7のtarballにはインストール時に動くスクリプトがありません。4.6.7は4.6.6と版番号以外同じ中身です。更新後も、配列の上限を他のチェックより前に指定し、HTTPの入口でリクエストボディのサイズを制限してください。
Zodの読み方と、何をするライブラリですか?
「ゾッド」と読みます。1つのスキーマからTypeScriptの型(z.infer)と実行時の検証(parse・safeParse)の両方が得られる、JavaScript・TypeScript向けのnpmパッケージです。
z.inferとは何ですか?
Zodスキーマから静的な型を取り出す型関数です。type User = z.infer<typeof User>と書くと、スキーマと同じ構造の型ができます。transformを含むスキーマでは出力側の型になるので、入力側が必要ならz.inputを使います。
parseとsafeParseはどう使い分けますか?
失敗が想定内のフォームやAPIの入力はsafeParseでsuccessを見て分岐し、失敗したらバグとみなす内部データはparseで例外を投げます。真偽値だけでよく、変換後の値を使わない場面に限ってvalidateも選べます。