3. ParsePayload・ハイブリッド同期/非同期実行と Checks エンジン
ランタイムにおいて、Zod v4.6.7 は packages/zod/src/v4/core/parse.ts、packages/zod/src/v4/core/schemas.ts、および packages/zod/src/v4/core/checks.ts が統括する可変ペイロード・ステートマシン(ParsePayload<T>)を通じてデータの検証、変換、およびエンコードを実行します。ネストされたプロパティごとに新しいコンテキストオブジェクトを割り当てたり、同期的な値を Promise.resolve() でラップしたりする代わりに、Zod v4 は非同期の refinement や transform が Promise を返さない限り 100% 同期的に動作するハイブリッド実行パイプラインを採用し、さらに Codec や Pipe のための双方向実行(forward (decode) および backward (encode))と、safeParse 失敗時の遅延 ZodError 生成 getter を備えています。
Source Code
from drawlib.canvas import save, setup
from drawlib.icons import phosphor
from drawlib.lines import line
from drawlib.smartarts import CardList, ChevronProcess, Panel
from drawlib.styles import Styles
from drawlib.text import text
setup(width=176, height=112)
text(
(88, 107.5),
"ParsePayload ステートマシンとハイブリッド同期/非同期実行エンジン",
style=Styles.DarkBold.patch(text_size=13.5),
)
text(
(88, 103.5),
"parse.ts と schemas.ts による forward/backward 双方向ルーティング、Promise ハンドオフ、および遅延エラー getter",
style=Styles.Muted.patch(text_size=10.0),
)
# ==============================================================================
# TOP PIPELINE: 5-Stage Parse Execution Lifecycle (x=5..171, y=85..98)
# ==============================================================================
pipe = ChevronProcess(
style=Styles.PrimaryNeutral,
text_style=Styles.DarkBold.patch(text_size=9.5),
description_style=Styles.Dark.patch(text_size=9.5),
corner_angle=60.0,
spacing=1.2,
flat_left_end=True,
)
pipe.add(
"1. ランナー入口",
description="_parse / _safeParse / _encode",
style=Styles.PrimaryFlat,
text_style=Styles.WhiteBold.patch(text_size=9.5),
description_style=Styles.White.patch(text_size=9.5),
)
pipe.add(
"2. Payload 初期化",
description="{ value, issues: [] } + ctx",
)
pipe.add(
"3. _zod.run 実行",
description="forward / backward カナリア",
)
pipe.add(
"4. 同期 / 非同期判定",
description="Promise ハンドオフ判定",
)
pipe.add(
"5. 結果 / 遅延エラー",
description="{ success } / throw Error",
style=Styles.AccentFlat,
text_style=Styles.WhiteBold.patch(text_size=9.5),
description_style=Styles.White.patch(text_size=9.5),
)
pipe.draw(xy=(5, 85), width=166, height=13)
# ==============================================================================
# BOTTOM LEFT: 1. Bidirectional _zod.run Execution (x=5..86, y=5..78)
# ==============================================================================
p_run = Panel(
"1. 双方向 _zod.run パイプライン (schemas.ts)",
style=Styles.PrimaryNeutral,
header_style=Styles.Primary,
text_style=Styles.WhiteBold.patch(text_size=10.5),
icon=phosphor.arrows_left_right,
icon_style=Styles.White,
header_height=6.0,
)
p_run.draw(xy=(5, 5), width=81, height=73)
run_cards = CardList(
style=Styles.White,
text_style=Styles.DarkBold.patch(text_size=9.5),
description_style=Styles.Muted.patch(text_size=9.5),
badge_shape="none",
direction="vertical",
spacing=1.5,
)
run_cards.add(
"チェック無し高速パス (checks.length === 0)",
description="deferred フックが _zod.run = _zod.parse を直接設定",
style=Styles.PrimaryFlat,
text_style=Styles.WhiteBold.patch(text_size=9.5),
description_style=Styles.White.patch(text_size=9.5),
)
run_cards.add(
"Forward モード (direction: 'forward' — parse / decode)",
description="1) _zod.parse(payload, ctx) --> 2) runChecks(result, checks)",
style=Styles.PrimaryNeutral,
)
run_cards.add(
"Backward モード Step 1: カナリアパス (direction: 'backward')",
description="_zod.parse({ value, issues: [] }, { skipChecks: true }) で型検証",
)
run_cards.add(
"Backward モード Step 2: チェック先行実行 -> 逆方向パース",
description="カナリア通過後に runChecks を先行実行してから _zod.parse へ",
)
run_cards.add(
"チェックループと継続可能 / 継続不可 Issue の制御",
description="チェックを順次実行し explicitlyAborted が true なら早期中断",
)
run_cards.draw(xy=(7.5, 7.5), width=76, height=62.5)
# ==============================================================================
# BOTTOM RIGHT: 2. Abort Semantics, Async Handoff & Lazy Errors (x=90..171, y=5..78)
# ==============================================================================
p_state = Panel(
"2. Abort 判定、非同期ハンドオフ、遅延 failure() Getter",
style=Styles.Neutral,
header_style=Styles.Secondary,
text_style=Styles.WhiteBold.patch(text_size=10.5),
icon=phosphor.lightning,
icon_style=Styles.White,
header_height=6.0,
)
p_state.draw(xy=(90, 5), width=81, height=73)
state_cards = CardList(
style=Styles.White,
text_style=Styles.DarkBold.patch(text_size=9.5),
description_style=Styles.Muted.patch(text_size=9.5),
badge_shape="none",
direction="vertical",
spacing=1.5,
)
state_cards.add(
"ParsePayload<T> & ParseContextInternal (schemas.ts)",
description="Payload: { value, issues, aborted? }; Ctx: { async, direction }",
style=Styles.PrimaryNeutral,
)
state_cards.add(
"util.aborted(x, start) vs util.explicitlyAborted(x, start)",
description="aborted: continue !== true; explicitlyAborted: continue === false",
)
state_cards.add(
"ハイブリッド同期 -> 非同期 Promise エスカレーション",
description="パーサやチェックが Promise を返すまで Promise 生成コストゼロ",
style=Styles.AccentFlat,
text_style=Styles.WhiteBold.patch(text_size=9.5),
description_style=Styles.White.patch(text_size=9.5),
)
state_cards.add(
"同期ガード: ctx.async === false 時は $ZodAsyncError をスロー",
description="非同期チェックを含むスキーマで同期 parse() を呼ぶと即座に例外",
)
state_cards.add(
"遅延 failure(Err, issues, ctx) Getter (parse.ts:L83-103)",
description="safeParse は .error プロパティ参照時のみ ZodError を実体化!",
style=Styles.PrimaryNeutral,
)
state_cards.draw(xy=(92.5, 7.5), width=76, height=62.5)
# Arrows from pipeline to bottom panels
line((45, 85), (45, 78), arrow_head="->", style=Styles.PrimaryBold)
line((130, 85), (130, 78), arrow_head="->", style=Styles.SecondaryBold)
save()
1. 実行エントリポイント (packages/zod/src/v4/core/parse.ts)
v4/classic の schema.parse(data)、v4/mini の z.parse(schema, data)、および Standard Schema の schema["~standard"].validate(data) を含むすべてのバリデーション呼び出しは、packages/zod/src/v4/core/parse.ts 内の高階ランナーファクトリへと集約されます。
1.1 parse.ts における10個のコアランナーファクトリ
ランナーファクトリ (v4/core/parse.ts) |
実行方向 (direction) |
非同期モード | 失敗時の戻り値・挙動 | 備考 |
|---|---|---|---|---|
_parse(Err) / parse |
"forward" |
async: false |
throw new Err(finalIssues) |
_zod.run が Promise を返した場合は $ZodAsyncError をスロー。 |
_parseAsync(Err) / parseAsync |
"forward" |
async: true |
new Err(finalIssues) で Reject |
非同期チェックが無い場合も結果を Promise.resolve でラップ。 |
_safeParse(Err) / safeParse |
"forward" |
async: false |
failure(Err, issues, ctx) |
{ success: false } 上で遅延 get error() getter を使用。 |
_safeParseAsync(Err) / safeParseAsync |
"forward" |
async: true |
failure(Err, issues, ctx) を Resolve |
_safeParse の非同期版。 |
_encode(Err) / encode |
"backward" |
async: false |
throw new Err(finalIssues) |
$ZodPipe および $ZodCodec を逆方向(out -> in)に実行。 |
_decode(Err) / decode |
"forward" |
async: false |
throw new Err(finalIssues) |
Codec 用の明示的な順方向デコード関数。 |
_encodeAsync / _decodeAsync |
"backward" / "forward" |
async: true |
new Err(finalIssues) で Reject |
非同期 Codec エンコード/デコードランナー。 |
_safeEncode / _safeDecode (+ Async) |
"backward" / "forward" |
false / true |
failure(Err, issues, ctx) |
Codec 用の例外を投げない結果オブジェクト返却版。 |
1.2 failure() における遅延エラー確定処理 (parse.ts:L83-103)
高スループットなアプリケーションでは、const res = schema.safeParse(input) を呼び出して if (!res.success) return 400; のように分岐するだけで、res.error の中身を参照しないケースが多々あります。完全な ZodError インスタンスを構築するには、すべての生 Issue に対して util.finalizeIssue(iss, ctx, config()) を実行し、ロケールのエラーマップを解決してエラーメッセージを整形する必要があります。
res.error が参照されない場合の safeParse 失敗コストをほぼゼロにするため、parse.ts の failure() は遅延 getter を返します。
// packages/zod/src/v4/core/parse.ts:L83-103
function failure(
_Err: errors.$ZodErrorClass,
issues: errors.$ZodRawIssue[],
ctx: schemas.ParseContextInternal
): util.SafeParseError<any> {
let error: errors.$ZodError | undefined;
let _issues: errors.$ZodRawIssue[] | undefined = issues;
let _ctx: schemas.ParseContextInternal | undefined = ctx;
return {
success: false,
get error() {
if (!error) {
const finalIssues = _issues!.map((iss) => util.finalizeIssue(iss, _ctx!, config()));
error = new _Err(finalIssues);
// クロージャがパース対象の入力値をメモリ上に保持し続けないよう参照を解放
_issues = undefined;
_ctx = undefined;
}
return error;
},
};
}
error が実体化された直後に _issues = undefined および _ctx = undefined を代入している点に注目してください。生 Issue($ZodRawIssue)は iss.input や iss.inst への参照を保持しているため、これらをクリアすることで、長寿命な SafeParseError オブジェクトが巨大な入力データをヒープメモリ上にピン留めしてしまうのを防いでいます。
2. ParsePayload と双方向 _zod.run (v4/core/schemas.ts)
2.1 ParsePayload<T> と ParseContextInternal (schemas.ts:L29-52)
// packages/zod/src/v4/core/schemas.ts
export interface ParseContextInternal extends ParseContext {
async?: boolean | undefined;
direction?: "forward" | "backward" | undefined;
skipChecks?: boolean | undefined;
abortEarly?: boolean | undefined;
}
export interface ParsePayload<T = unknown> {
value: T;
issues: errors.$ZodRawIssue[];
/**Flag toabort validation early Check execution stops if true. */
aborted?: boolean;
/**True if payload value has already been processed by a memoizer. */
memo?: boolean;
}
Zod v3 のように再帰呼び出しの各階層で { status: "valid" | "dirty" | "aborted", value } というラッパーオブジェクトを生成して返すのではなく、Zod v4 は可変の ParsePayload オブジェクト { value, issues } を schema._zod.run(payload, ctx) に渡します。
- インプレースな値の更新: Coercion(型強制)、デフォルト値、Transform、および
$ZodCheckOverwriteチェックはpayload.valueを直接書き換えてpayloadを返します。 - Issue の蓄積: 検証エラーが発生すると、軽量な
$ZodRawIssueオブジェクトがpayload.issuesに追加されます。
2.2 aborted() と explicitlyAborted() の違い (util.ts:L904-922)
Zod v4 は $ZodRawIssue 上の continue フラグを用いて、2種類の中断(Abort)条件を厳密に区別しています。
// packages/zod/src/v4/core/util.ts:L904-922
export function aborted(x: schemas.ParsePayload, startIndex = 0): boolean {
if (x.aborted === true) return true;
for (let i = startIndex; i < x.issues.length; i++) {
if (x.issues[i]?.continue !== true) return true;
}
return false;
}
export function explicitlyAborted(x: schemas.ParsePayload, startIndex = 0): boolean {
if (x.aborted === true) return true;
for (let i = startIndex; i < x.issues.length; i++) {
if (x.issues[i]?.continue === false) return true;
}
return false;
}
- 基本型パースの失敗 (
continue: undefined):$ZodStringがtypeof input !== "string"により失敗した場合、continue: trueを付けずに{ code: "invalid_type", expected: "string", input }を追加します。このときutil.aborted(payload)はtrueを返し、後続の文字列チェック(.min(3)や.email())が非文字列データに対して実行されてクラッシュするのを防ぎます。 - Refinement チェックの失敗 (
continue: !def.abort):checks.tsの標準チェック群はcontinue: !def.abort(既定でtrue)付きの Issue を追加します。そのため、チェックに{ abort: true }(continue: falseとなりexplicitlyAbortedを発火させる)が指定されていない限り、同一文字列やオブジェクト上の複数のチェックがすべて実行され、一度のパースですべてのエラーをまとめて報告できます。
2.3 $ZodType における Forward / Backward (encode) の配線 (schemas.ts:L218-291)
スキーマ生成時、$ZodType は deferred コールバックを登録して inst._zod.run を配線します。
- チェック無し高速パス:
!checks || checks.length === 0の場合、inst._zod.run = inst._zod.run ?? inst._zod.parseが直接代入されます。_zod.run(payload, ctx)の呼び出しはラッパー関数を介さず直接_zod.parse(payload, ctx)を実行します。 - Forward モード (
direction === "forward", 既定):result = inst._zod.parse(payload, ctx)を実行します。resultが解決された後(同期または.then()経由)、runChecks(result, checks, ctx)を実行します。
- Backward モード (
direction === "backward",z.encode()で使用):- なぜ Backward モードでは単純に
_zod.parseの前にchecksを実行できないのでしょうか?それは、スキーマに付与されたチェック(文字列の.min(3)やオブジェクトの.refine(...))が「入力値はすでにスキーマの出力型を満たしている」ことを前提としているためです。 - そこで
$ZodTypeはまず**カナリアパス(Canary Pass)**を実行します。const canary = inst._zod.parse({ value: payload.value, issues: [] }, { ...ctx, skipChecks: true }); - カナリアパスが失敗(
util.aborted(canary))した場合、_zod.runは即座に本体のinst._zod.parse(payload, ctx)を実行して型エラーを記録し、checksをスキップします。 - カナリアパスが成功した場合、
_zod.runは先にrunChecks(payload, checks, ctx)を実行し、チェック通過後のペイロードをinst._zod.parse(checkResult, ctx)に渡します(これにより Pipe、Transform、Default、Codec が逆方向に実行され、入力型へとエンコードされます)。
- なぜ Backward モードでは単純に
3. $ZodObject の高速正規化、判別共用体、および $ZodCheck
Source Code
from drawlib.canvas import save, setup
from drawlib.icons import phosphor
from drawlib.smartarts import CardList, Panel
from drawlib.styles import Styles
from drawlib.text import text
setup(width=176, height=104)
text(
(88, 99.5),
"$ZodObject の Shape 正規化、判別共用体、および Check エンジン",
style=Styles.DarkBold.patch(text_size=13.5),
)
text(
(88, 95.5),
"schemas.ts と checks.ts によるオブジェクト検証ループ最適化、O(1) ユニオン分岐、および _zod.bag メタデータ収集",
style=Styles.Muted.patch(text_size=10.0),
)
# ==============================================================================
# PANEL 1 (LEFT): $ZodObject Normalization & Security (x=5..58, y=6..91)
# ==============================================================================
p_obj = Panel(
"1. $ZodObject 正規化と防御",
style=Styles.PrimaryNeutral,
header_style=Styles.Primary,
text_style=Styles.WhiteBold.patch(text_size=10.5),
icon=phosphor.cube,
icon_style=Styles.White,
header_height=6.0,
)
p_obj.draw(xy=(5, 6), width=53, height=85)
obj_cards = CardList(
style=Styles.White,
text_style=Styles.DarkBold.patch(text_size=9.5),
description_style=Styles.Muted.patch(text_size=9.5),
badge_shape="none",
direction="vertical",
spacing=1.5,
)
obj_cards.add(
"ShapeGetter.raw: def.shape の凍結",
description="初回参照時に { ...def.shape } を freeze",
)
obj_cards.add(
"normalizeDef(def) (util.cached)",
description="allKeys, symbolKeys, optionalKeys を計算",
style=Styles.PrimaryFlat,
text_style=Styles.WhiteBold.patch(text_size=9.5),
description_style=Styles.White.patch(text_size=9.5),
)
obj_cards.add(
"optin / optout オプショナル性の伝播",
description="入力時・出力時の optional 状態を追跡",
style=Styles.PrimaryNeutral,
)
obj_cards.add(
"__proto__ プロトタイプ汚染の除去",
description="shape 未宣言の '__proto__' キーを delete",
style=Styles.AccentFlat,
text_style=Styles.WhiteBold.patch(text_size=9.5),
description_style=Styles.White.patch(text_size=9.5),
)
obj_cards.add(
"handleCatchall: strip / strict",
description="余剰キーの無視・エラー・catchall 検証",
)
obj_cards.draw(xy=(7.5, 8.5), width=48, height=74.5)
# ==============================================================================
# PANEL 2 (CENTER): Discriminated Unions & Codecs (x=61.5..114.5, y=6..91)
# ==============================================================================
p_union = Panel(
"2. O(1) 判別共用体と双方向 Codec",
style=Styles.Neutral,
header_style=Styles.Secondary,
text_style=Styles.WhiteBold.patch(text_size=10.5),
icon=phosphor.git_branch,
icon_style=Styles.White,
header_height=6.0,
)
p_union.draw(xy=(61.5, 6), width=53, height=85)
union_cards = CardList(
style=Styles.White,
text_style=Styles.DarkBold.patch(text_size=9.5),
description_style=Styles.Muted.patch(text_size=9.5),
badge_shape="none",
direction="vertical",
spacing=1.5,
)
union_cards.add(
"遅延 _zod.propValues の伝播",
description="literal, enum, union が許容値集合を公開",
style=Styles.PrimaryNeutral,
)
union_cards.add(
"DiscriminatedUnion O(1) Map 検索",
description="判別キー値 -> 分岐スキーマを O(1) 検索",
style=Styles.PrimaryFlat,
text_style=Styles.WhiteBold.patch(text_size=9.5),
description_style=Styles.White.patch(text_size=9.5),
)
union_cards.add(
"Backward 時の Union フォールバック",
description="encode 時は順次ユニオン走査へ自動移行",
)
union_cards.add(
"$ZodPipe & ZodCodec ルーティング",
description="Forward: in -> out; Backward: out -> in",
style=Styles.PrimaryNeutral,
)
union_cards.add(
"Pipe 内の unrecognized_keys 中断",
description="unrecognized_keys 検出時は後続変換中断",
)
union_cards.draw(xy=(64.0, 8.5), width=48, height=74.5)
# ==============================================================================
# PANEL 3 (RIGHT): $ZodCheck Lifecycle & Bag (x=118..171, y=6..91)
# ==============================================================================
p_chk = Panel(
"3. $ZodCheck エンジンと Bag",
style=Styles.PrimaryNeutral,
header_style=Styles.PrimaryFlat,
text_style=Styles.WhiteBold.patch(text_size=10.5),
icon=phosphor.check_circle,
icon_style=Styles.White,
header_height=6.0,
)
p_chk.draw(xy=(118, 6), width=53, height=85)
chk_cards = CardList(
style=Styles.White,
text_style=Styles.DarkBold.patch(text_size=9.5),
description_style=Styles.Muted.patch(text_size=9.5),
badge_shape="none",
direction="vertical",
spacing=1.5,
)
chk_cards.add(
"フェーズ 1: _zod.onattach の実行",
description="アタッチ時に schema._zod.bag を構築",
style=Styles.PrimaryFlat,
text_style=Styles.WhiteBold.patch(text_size=9.5),
description_style=Styles.White.patch(text_size=9.5),
)
chk_cards.add(
"メタデータ Bag (minimum, pattern)",
description="toJSONSchema() や正規表現統合で活用",
style=Styles.PrimaryNeutral,
)
chk_cards.add(
"フェーズ 2: def.when(payload) ゲート",
description="前提条件が false の場合はチェックを省略",
)
chk_cards.add(
"フェーズ 3: _zod.check(payload)",
description="Issue の追加または payload.value の変換",
)
chk_cards.add(
"attachSchema(inst, path, schema)",
description="ネストされたプロパティ検証を委譲",
)
chk_cards.draw(xy=(120.5, 8.5), width=48, height=74.5)
save()
4. $ZodObject、$ZodDiscriminatedUnion、および $ZodCheck の詳細解説
4.1 $ZodObject の Shape 正規化とセキュリティ (schemas.ts:L2098-2269)
$ZodObject は Zod において最もパフォーマンスが重視される複合スキーマです。パース呼び出しのたびに Object.keys(def.shape) を実行するのを避けるため、$ZodObject は2段階のキャッシュ層を備えています。
- 不変化された Shape ディスクリプタ (
ShapeGetter.raw): 初回読み取り時に、ShapeGetter.rawがdef.shapeを浅くコピーして凍結(Object.freeze({ ...inst._zod.def.shape }))し、すべてのプロパティが$ZodTypeのインスタンスであることを検証します。 - キャッシュされた
normalizeDef(def)(schemas.ts:L2098-2114):allKeys(Object.keys(shape)による文字列キー配列)、symbolKeys(Object.getOwnPropertySymbols(shape))、keySet(new Set(allKeys))、およびoptionalKeys(子スキーマのprop._zod.optin === "optional"が成り立つキー群)を事前計算してメモ化します。 - プロトタイプ汚染(Prototype Pollution)の防御 (
schemas.ts:L2218-2222):
信頼できない JSON ペイロード(例:// shape 内に明示的に宣言されていない限り、入力オブジェクト自身の __proto__ を常に除去 if (!keySet.has("__proto__") && Object.prototype.hasOwnProperty.call(input, "__proto__")) { delete (input as any)["__proto__"]; }JSON.parse('{"__proto__": {"isAdmin": true}}'))をパースする際、inputは自身のプロパティとして"__proto__"を持っています。出力オブジェクトresult = {}を構築する前であっても、$ZodObjectはshapeで明示的に宣言されていない"__proto__"キーを除去し、検証済みプロパティをutil.assignProp(result, key, value)(key === "__proto__"の場合はObject.definePropertyを使用)によって安全に書き込みます。 - ハイブリッド同期/非同期オブジェクト継続処理 (
processObjectResults):allKeysのループ中、$ZodObjectはlet isAsync = falseを追跡します。すべての子プロパティが同期的に返る場合、Promiseを一切割り当てずにその場でresultが構築されます。いずれかの子プロパティがPromiseを返した瞬間にisAsync = trueに切り替わり、ループ終了後にPromise.all(promises).then(() => processObjectResults(...))によって非同期プロパティのみを待機してからhandleCatchallを実行します。
4.2 $ZodDiscriminatedUnion における O(1) ルックアップ (schemas.ts:L2695-2801)
$ZodDiscriminatedUnion は、なぜユーザーが手動でユニオンをフラット化しなくても、ネストされた判別共用体や z.literal()、z.enum()、z.optional()、z.default()、z.pipe() を含むオブジェクト分岐を扱えるのでしょうか?
- 有限個のプリミティブ値を制約するすべてのスキーマは
_zod.values(Set<Primitive>)を定義し、すべての$ZodObjectおよび$ZodUnionは子スキーマの_zod.valuesをdefineLazyInternal経由で遅延_zod.propValuesマップ(Record<string, Set<Primitive>>)へと集約します。 $ZodDiscriminatedUnionは初期化時に各ユニオン選択肢からopt._zod.propValues?.[def.discriminator]を読み取り、Map<Primitive, $ZodType>(discriminatorMap)を構築します。- パース実行時(
direction === "forward")、$ZodDiscriminatedUnionはconst discVal = payload.value[def.discriminator]を読み取り、O(1)時間でconst match = discriminatorMap.get(discVal)を検索します。一致するスキーマが見つかれば、直接match._zod.run(payload, ctx)に処理を委譲します。 ctx.direction === "backward"の場合(Pipe によって判別キーの値が変換される可能性がある場合)や、静的に判別値を決定できない分岐のためにdef.unionFallbackが有効化されている場合は、自動的に$ZodUnionの順次評価(inst._zod._super.parse(payload, ctx))へとフォールバックします。
4.3 2フェーズ $ZodCheck アーキテクチャ (packages/zod/src/v4/core/checks.ts)
Zod v3 では、.min(5) や .email() といった制約は ZodString._parse の内部にハードコードされていました。Zod v4 では、すべての制約が packages/zod/src/v4/core/checks.ts で定義された独立した $ZodCheck トレイトとして実装されています。
// packages/zod/src/v4/core/checks.ts:L55-76
export const $ZodCheck = /*@__PURE__*/ $constructor("$ZodCheck", (inst, def) => {
inst._zod ??= {} as any;
inst._zod.def = def;
inst._zod.onattach ??= [];
});
各 $ZodCheck サブクラスは以下の2つのフェーズで動作します。
- アタッチメント・フェーズ (
inst._zod.onattach): チェックがスキーマに付与される際($ZodTypeの初期化時)、check._zod.onattach内のすべてのコールバックがホストのschemaを引数として呼び出されます。$ZodCheckMinLengthはbag.minimum = def.minimum(またはbag.minLength)を書き込みます。$ZodCheckRegex/$ZodCheckStringFormatは正規表現をbag.patternsに追加します。- 数値範囲チェック(
$ZodCheckGreaterThan、$ZodCheckLessThan)はbag.minimum/bag.maximum/bag.exclusiveMinimum/bag.exclusiveMaximumを更新します。 これによりschema._zod.bagが事前に構築されるため、toJSONSchema()やイントロスペクションツールはチェッククロージャを実行・逆コンパイルすることなくO(1)で制約メタデータを読み取ることができます。
- 実行フェーズ (
inst._zod.check(payload)):runChecks(payload, checks, ctx)の実行中:- まず
check._zod.def.whenが定義されている場合、Zod はcheck._zod.def.when(payload)を呼び出し(例えば$ZodCheckMinLengthはwhen: (payload) => payload.value.length !== undefinedを設定)、falseが返された場合はチェック本体をスキップします。 - 次に
check._zod.check(payload)が実行されます。検証チェックは生 Issue を追加(payload.issues.push({ code: ..., input: payload.value, inst: check, continue: !def.abort }))し、$ZodCheckOverwrite(z.overwrite()、.trim()、.toLowerCase())のような変換チェックはpayload.value = def.tx(payload.value)をインプレースで書き換えます。
- まず