Skip to content

Zod Schema 実行時検証

源码版本v2026.6.11

責務

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 つの理由:

  1. TypeScript 同源:schema 定義から直接 OpenClawConfig 型が導出され,schema を変えることは型を変えること,コンパイル時に不一致を発見できます。
  2. 組合可能:provider、channel、approvals、cron、skills 各々が schema を持ち,OpenClawSchema トップレベルで組み立て,プラグインはさらに validateConfigObjectWithPlugins で自身の schema 断片を注入できます——これは JSON Schema にはできない動的組合せです。
  3. エラーメッセージ可読: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-558models ブロック,mode: merge|replaceproviders を含む。
  • ModelProvidersSchema:518-540 — provider 登録表,superRefine でカスタム provider に baseUrlmodels[] を強制,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-860enabled / store / maxConcurrentRuns / retry
  • skills schema:1260-1310allowBundled / 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.tsthrowInvalidConfig,issues をエラーメッセージにフォーマットし INVALID_CONFIG code を付ける。
  • defaults.ts:109-529 — schema 外の「ソフトデフォルト値」:applyMessageDefaults / applyAgentDefaults / applyCronDefaults / applyCompactionDefaults 等を materializeRuntimeConfig 段で呼び出し,ディスクに書き戻さない。

データフロー

検証エントリは validateConfigObjectRaw(validateConfigObjectRaw:1041)です。核心は safeParse 一行:

typescript
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 を物化し,ユーザ設定に openai id と書くだけで bundled デフォルト model リストを継承できます。

ModelsConfigSchema 自体(ModelsConfigSchema:551)は小さい:

typescript
export const ModelsConfigSchema = z
  .object({
    mode: z.union([z.literal("merge"), z.literal("replace")]).optional(),
    providers: ModelProvidersSchema.optional(),
    pricing: ModelPricingConfigSchema,
  })
  .strict()
  .optional();

.strict()models ブロックに未定義フィールド(例:apiKeyproviders.openai 下ではなく models 下に誤って書く)が出現すると拒否します。mode: "merge" はデフォルト挙動——bundled provider とユーザ宣言の同 id provider フィールドをマージ;mode: "replace" は bundled 定義を完全に置換し,カスタム OpenAI 互換エンドポイントに使います。

ModelProvidersSchema.superRefine() でクロスフィールド検証(ModelProvidersSchema:518-540)を行います:

typescript
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",
        });
      }

isBuiltInModelProviderOverlayIdopenai / anthropic / google などの bundled id かを判定し,該当すれば一部フィールドだけの上書きを許可します。カスタム id(例:my-local-llm)は baseUrlmodels[] を明示的に与えなければならず,さもなくば拒否されます。これで「provider を設定したが baseUrl がなく,ランタイムで初めて発見」の尴尬を回避します。

検証チェーン全体:

境界と失敗

  • fail closed:validateConfigObjectRaw が通らないとき,loadConfigLocal は直接 throwInvalidConfig(src/config/io.ts:1752-1757)に走り,エラーは INVALID_CONFIG code を持ち,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() 型強制:DiscordIdSchemastring | number を受け入れますが,Discord snowflake は Number.MAX_SAFE_INTEGER を超えると精度が失われるため,transform 段階で string に強制し非負を検証(DiscordIdSchema)します。
  • future version guard:meta.lastTouchedVersionshouldWarnOnTouchedVersion でチェックし,新バージョンの設定を旧バージョンが読んでも warn だけで拒否しません。しかし schema 自身の lastTouchedAtstring | 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