Zod Schema 実行時検証
責務
Zod schema は OpenClaw 設定 (config) の実行時契約 (contract) です:openclaw.json が読み込まれるとすぐに OpenClawSchema.safeParse が走り,すべてのフィールド形状、列挙値、provider 必須項目、クロスフィールド制約がここで守門します。schema を通らなければ,throwInvalidConfig が直接 INVALID_CONFIG をスローし,ゲートウェイは起動を拒否します——黙って緩いデフォルト値にフォールバックしません。
schema モジュールはデフォルト値注入と形态正規化の一部も担当します:.default()、.transform()、.overwrite()、.superRefine() の 4 種の hook が safeParse 1 回の呼び出しで全部走り,そのまま使える OpenClawConfig オブジェクトを産出します。
設計動機
なぜ JSON Schema や手書き if-else ではなく Zod か?3 つの理由:
- TypeScript 同源:schema 定義から直接
OpenClawConfig型が導出され,schema を変えることは型を変えること,コンパイル時に不一致を発見できます。 - 組合可能:provider、channel、approvals、cron、skills 各々が schema を持ち,
OpenClawSchemaトップレベルで組み立て,プラグインはさらにvalidateConfigObjectWithPluginsで自身の schema 断片を注入できます——これは JSON Schema にはできない動的組合せです。 - エラーメッセージ可読:
safeParseが返す issues はパスを持ち,mapZodIssueToConfigIssueが Zod の内部 issue をpath + messageの平坦リストに翻訳し,CLI がユーザに表示するときmodels.providers.openai.models[0].idのような深いパスに正確に位置づけできます。
代償は schema ファイル自体が巨大(zod-schema.core.ts は千行以上)で,Zod のランタイムオーバーヘッドが大きい設定では無視できないこと——しかし「設定エラーでランタイムが落ちる」コストと比べれば,このオーバーヘッドは許容されます。
主要ファイル
OpenClawSchema トップレベル:495-560— ルート schema,models / channels / agents / approvals / session / cron / skills / memory / mcp / hooks / gateway / proxy のすべてのサブ schema を集約。ModelsConfigSchema:551-558—modelsブロック,mode: merge|replaceとprovidersを含む。ModelProvidersSchema:518-540— provider 登録表,superRefineでカスタム provider にbaseUrlとmodels[]を強制,bundled overlay のみ豁免。zod-schema.providers-core.ts— provider 主体フィールド(baseUrl / api / auth / apiKey / headers / models[]),channels にも再利用される。ChannelsSchema:52— チャネル設定,動的 record,各チャネル id が channel schema に対応,channel 自身の schema は plugin が提供。InternalHooksSchema:97-113— 内部 hooks 設定,トリガーとアクションのマッピング。zod-schema.session.ts— セッションポリシー、メッセージ保持、送信ポリシー。zod-schema.approvals.ts— ツール呼び出し承認ポリシー(native exec / file write 等)。zod-schema.agents.ts— 複数 agent 設定(agent list / audio / bindings / broadcast)。cron schema:849-860—enabled/store/maxConcurrentRuns/retry。skills schema:1260-1310—allowBundled/load/install/limitsの 4 グループのサブフィールド。validateConfigObjectRaw:1041-1105— Zod 検証エントリ,safeParseが通らなければ issues にマップ。validateConfigObject:1107-1124— raw の上にmaterializeRuntimeConfigを重ね,そのまま使える runtime config を産出。validateConfigObjectWithPlugins:1149-1183— plugin schema 検証を重ね,plugin がvalidateConfigObjectWithPluginsBaseで自身のフィールド制約を注入。io.invalid-config.ts—throwInvalidConfig,issues をエラーメッセージにフォーマットしINVALID_CONFIGcode を付ける。defaults.ts:109-529— schema 外の「ソフトデフォルト値」:applyMessageDefaults/applyAgentDefaults/applyCronDefaults/applyCompactionDefaults等をmaterializeRuntimeConfig段で呼び出し,ディスクに書き戻さない。
データフロー
検証エントリは validateConfigObjectRaw(validateConfigObjectRaw:1041)です。核心は safeParse 一行:
export function validateConfigObjectRaw(
raw: unknown,
opts?: {
sourceRaw?: unknown;
touchedPaths?: ReadonlyArray<ReadonlyArray<string>>;
validateBundledChannels?: boolean;
preservedLegacyRootKeys?: readonly string[];
},
): { ok: true; config: OpenClawConfig } | { ok: false; issues: ConfigValidationIssue[] } {
const normalizedRaw = stripPreservedLegacyRootKeysForValidation(
stripDeprecatedValidationKeys(raw),
opts?.preservedLegacyRootKeys,
);
const policyIssues = collectUnsupportedSecretRefPolicyIssues(normalizedRaw);
const validated = OpenClawSchema.safeParse(normalizedRaw);
if (!validated.success) {
const schemaIssues = validated.error.issues.map((issue) => mapZodIssueToConfigIssue(issue));
return {
ok: false,
issues: mergeUnsupportedMutableSecretRefIssues(policyIssues, schemaIssues),
};
}
const validatedConfig = materializeBundledModelProviderOverlays(validated.data as OpenClawConfig);3 つの詳細に注意:
stripDeprecatedValidationKeysは検証前に廃止フィールドを抹消し,schema が.strict()で未知フィールドを拒否しても旧設定を壊さないようにします;policyIssuesは schema 外のポリシーチェック(secret ref が可変か等)で,Zod issues とマージして返します;materializeBundledModelProviderOverlaysは検証通過後にビルトイン provider overlay を物化し,ユーザ設定にopenaiid と書くだけで bundled デフォルト model リストを継承できます。
ModelsConfigSchema 自体(ModelsConfigSchema:551)は小さい:
export const ModelsConfigSchema = z
.object({
mode: z.union([z.literal("merge"), z.literal("replace")]).optional(),
providers: ModelProvidersSchema.optional(),
pricing: ModelPricingConfigSchema,
})
.strict()
.optional();.strict() は models ブロックに未定義フィールド(例:apiKey を providers.openai 下ではなく models 下に誤って書く)が出現すると拒否します。mode: "merge" はデフォルト挙動——bundled provider とユーザ宣言の同 id provider フィールドをマージ;mode: "replace" は bundled 定義を完全に置換し,カスタム OpenAI 互換エンドポイントに使います。
ModelProvidersSchema は .superRefine() でクロスフィールド検証(ModelProvidersSchema:518-540)を行います:
const ModelProvidersSchema = z
.record(z.string(), ModelProviderSchema)
.superRefine((providers, ctx) => {
for (const [providerId, provider] of Object.entries(providers)) {
if (isBuiltInModelProviderOverlayId(providerId)) {
continue;
}
if (!provider.baseUrl) {
ctx.addIssue({
code: "custom",
path: [providerId, "baseUrl"],
message:
"custom model providers must declare baseUrl; provider overlays without baseUrl are only supported for bundled providers",
});
}isBuiltInModelProviderOverlayId が openai / anthropic / google などの bundled id かを判定し,該当すれば一部フィールドだけの上書きを許可します。カスタム id(例:my-local-llm)は baseUrl と models[] を明示的に与えなければならず,さもなくば拒否されます。これで「provider を設定したが baseUrl がなく,ランタイムで初めて発見」の尴尬を回避します。
検証チェーン全体:
境界と失敗
- fail closed:
validateConfigObjectRawが通らないとき,loadConfigLocalは直接throwInvalidConfig(src/config/io.ts:1752-1757)に走り,エラーはINVALID_CONFIGcode を持ち,loadConfigLocalの catch はこの code を明示的に判定してフォールバックしません——壊れた設定でゲートウェイが立ち上がらないことを保証します。 .strict()スペル訂正:トップレベル schema とModelsConfigSchemaはどちらも.strict()を使い,providrsのようなスペルミスフィールドを直接拒否し,「キー書き間違い後 黙って無視」を回避します。- plugin schema 注入:サードパーティプラグインは
validateConfigObjectWithPluginsで自身の schema 断片を注入し,失敗時の issue パスには plugin プレフィックスが付きます。skipPluginValidation: trueはプラグイン schema にバグがあることが分かっている緊急修正パスに使われます。 - default と optional の区別:多くのフィールドは
.default("allowlist").optional()パターン——.optional()は作成者態での省略を許可,.default()は runtime が常に値を取得することを保証します。この区別で「ユーザが書いていない」と「ユーザが null を書いた」のセマンティクスが異なり,doctor もこれに基づいて埋め戻し要否を判断します。 .overwrite()正規化:VisibleRepliesSchemaは"automatic" | "message_tool"またはbooleanを受け入れ,後者は.overwrite()で対応列挙に変換(VisibleRepliesSchema:563-573)します。この正規化で旧設定のtrueを移行不要にします。.transform()型強制:DiscordIdSchemaはstring | numberを受け入れますが,Discord snowflake はNumber.MAX_SAFE_INTEGERを超えると精度が失われるため,transform 段階で string に強制し非負を検証(DiscordIdSchema)します。- future version guard:
meta.lastTouchedVersionはshouldWarnOnTouchedVersionでチェックし,新バージョンの設定を旧バージョンが読んでも warn だけで拒否しません。しかし schema 自身のlastTouchedAtはstring | numberを受け入れ,number は ISO 文字列に transform され,agent スクリプトがDate.now()をファイルに書いた後に解析失敗するのを回避します。 - channel schema 遅延バインド:channel は動的 record で,各 channel id に対応する schema は channel plugin が読み込み時に登録するため,
validateConfigObjectRawは channel 共通フィールドだけを検証します。channel 内部フィールド検証は plugin 読み込み後にcollectRawBundledChannelConfigIssuesで追加分を走ります。 - preserved legacy root keys:doctor が歴史的遺留キーを修復するとき,
preservedLegacyRootKeysでこれらのキーを検証スキップしつつファイルに保持し,schema が厳格化された後に旧設定が一括して全エラーになるのを避けます。
まとめ
Zod schema は OpenClaw 設定の守門人で,静的形状を定義するだけでなく,クロスフィールドポリシーと正規化も担当します。safeParse 1 回の呼び出しですべてのチェックを走り,失敗すれば fail closed します。provider / channel / agent 各々に独立 schema ファイルを持ち,OpenClawSchema トップレベルで組み立て,plugin はさらに動的注入できます。openclaw.json 設定エントリ の読み取りパスと組み合わせ,読み-検証-物化のチェーン全体で壊れた設定が runtime に入るのを保証しません。channel 側の具体フィールドは チャネル:設定 を参照。
公式資料:Configuration Schema · README。