M.

claude -p --json-schema はスキーマ違反を AI に差し戻す。ただし AI が諦めると exit 0 で中身が空になる

claude -p に --json-schema を付けると、Claude の出力を JSON スキーマどおりの形で受け取れます。AI の生成結果を930件自動採点した記事 でも、生成結果をこの方法で受け取っていました。

このとき、気になっていたことがあります。スキーマに合わない出力が来たら、どうなるのか。呼び出し側で検証し直す必要があるのか、それとも Claude Code が保証してくれるのか。わざと違反させて確かめました。

※ 環境は Windows、Claude Code 2.1.296、モデルは Haiku です。ほかのモデルでは、違反のしかたや諦めやすさが違うかもしれません。

結論

  • スキーマに書いた制約は Claude Code が検証する。違反すると、エラー文付きで AI に差し戻され、AI が直して出し直す(例外として format キーワードは検証されない)
  • 最終的な structured_output に入るのは、検証を通った出力だけ
  • ただし、AI が途中で諦めると、structured_output が無いまま exit 0・subtype: "success" で終わる。終了コードだけ見ていると、成功に見える
  • 試行回数には上限があり、既定は5回。上限に達すると exit 1 になる

実験のスキーマ

制約を一通り入れた、小さなスキーマを使いました。不具合の記録を想定しています。

{
  "type": "object",
  "properties": {
    "severity": { "type": "string", "enum": ["low", "medium", "high"] },
    "title":    { "type": "string", "minLength": 5, "maxLength": 20 },
    "count":    { "type": "integer", "minimum": 0 }
  },
  "required": ["severity", "title", "count"],
  "additionalProperties": false
}

プロンプトで「severity は critical にして」「count は書かないで」のように、わざと違反する値を指示します。途中のやりとりを見るために、--output-format stream-json --verbose で実行しました。

claude -p "<違反させるプロンプト>" --model haiku --output-format stream-json --verbose --setting-sources "" --json-schema '<上のスキーマ>'

最初は --setting-sources "" を付けずに実行していました。これだと私のグローバル CLAUDE.md(口調の設定など)が読み込まれ、AI の振る舞いに影響しているかもしれません。そこで、CLAUDE.md などを読まない設定で全ケースを2回ずつ取り直しました。後の「7種類の制約で違反させた結果」の表は、この3回分の結果です。

なお、取り直しでも --tools ""(後述)は付け忘れていました。ただ、StructuredOutput 以外のツールが呼ばれたのは、違反させないふつうの課題の1回だけでした。

仕組み:出力はツールの呼び出しとして作られる

stream-json を見ると、構造化出力の作られ方が分かります。AI は StructuredOutput という ツールを呼び出し、その引数として JSON を渡します。Claude Code はその引数をスキーマで検証し、合わなければツールのエラーとして返します。

Output does not match required schema: /severity: must be equal to one of the allowed values: ["low","medium","high"]

AI はこのエラーを読んで、値を直して呼び直します。ふつうのツールがエラーを返したときと同じ流れです。

7種類の制約で違反させた結果

結果は「通った」(直して、または最初から合う値で出力できた)、「諦めた」(structured_output なしで exit 0)、「上限」(exit 1)の3つに分けました。

違反させた制約指示差し戻しのエラーCLAUDE.md あり設定なし 1回目設定なし 2回目
enumseverity を critical に/severity: must be equal to one of the allowed values諦めた諦めた諦めた
enum同上。弾かれたら直してよい同上通った通った通った
maxLength27文字の title をそのまま/title: must NOT have more than 20 characters (got 27)通った諦めた諦めた
minLengthtitle は「不具合」の3文字だけ/title: must NOT have fewer than 5 characters (got 3)諦めた諦めた諦めた
typecount を文字列 “three” に/count: must be integer通った通った通った
requiredcount は書かないでroot: must have required property 'count'諦めた諦めた諦めた
additionalPropertiesowner という項目も入れてroot: must NOT have additional properties ('owner' is not allowed)通った通った通った
minimumcount を -1 に/count: must be >= 0諦めた通った諦めた
enumcritical のまま。2回失敗したら文章で答えてenum と同じ諦めた※諦めた※諦めた※

※ 最後の行は、諦め方をプロンプトで指示したケースです。指示どおりに諦めたので、AI が自分から諦めた数には入れていません。

どの制約も、違反はきちんと検出されました。エラー文には、どの項目がどう違反したかが JSON Pointer(/title など)付きで書かれています。

一方で、直すか諦めるかは、同じ指示でも回によって変わりました。CLAUDE.md を読ませなくても「諦めた」は毎回何件も出ているので、口調の設定のせいではなさそうです。

なお、ふつうの課題(「本番のデータベースが全件消えた障害の重大度を判定して記録して」)では、AI は3回とも high を選んで通りました。差し戻しが起きるのは、指示とスキーマがぶつかったときです。

罠:諦めると exit 0 のまま中身が無い

「直してよい」と言われていれば、AI は3回とも直しました。許可がないときは、制約によって分かれました。

  • 3回とも直した:count の “three” を 3 にする(type)、owner を外す(additionalProperties)
  • 3回とも諦めた:「critical に」(enum)、「3文字だけ」(minLength)、「書かないで」(required)
  • 回によって違った:「そのまま、省略しない」と念を押した長い title(maxLength)、-1 を指定した count(minimum)

諦めるときは、AI は指示を守ったまま1〜2回試し、「スキーマで拒否されたので出力できません」と文章で答えて終わりました。どこで分かれるのか、はっきりした規則は今回の結果からは読み取れません。

このときの最終結果がこれです。

項目値
終了コード0
subtype"success"
is_errorfalse
structured_outputキー自体が無い
result「出力できません」という説明の文章

失敗しているのに、成功として終わります。スクリプトから呼び出して終了コードだけ見ていると、ここを見逃します。

これは仕様どおりの挙動で、Agent SDK の構造化出力のドキュメント にも「subtype が success でも structured_output が無いことがある。それも失敗として扱う」と書かれています。私はドキュメントを読む前に、実験で出会いました。

上限まで粘ると exit 1 になる

「エラーが返っても critical のまま、少なくとも5回呼び続けて」と指示すると、CLAUDE.md ありの実行では結果が変わりました。

項目値
StructuredOutput の呼び出し5回(すべて差し戻し)
終了コード1
subtype"error_max_structured_output_retries"
is_errortrue

試行回数には上限があります。環境変数の公式ドキュメント によると、MAX_STRUCTURED_OUTPUT_RETRIES で変えられ、既定は5回(最初の1回+再試行4回)です。MAX_STRUCTURED_OUTPUT_RETRIES=2 にして同じ指示で試すと、2回で exit 1 になりました。

ただ、設定なしの2回では、同じ指示でも AI は5回まで粘らず、1回目は2回で諦め(exit 0)、2回目は high に直して通りました。11ケース×3回、計33回の実行で、exit 1 になったのはこの1回だけです(上限を2にした確認の実行は除きます)。AI が諦めるほうがずっと多い ので、exit 1 だけを失敗と見なすと、ほとんどの失敗を見逃します。

もう一つ、ドキュメントによると error_max_structured_output_retries は、検証の失敗がなくても起きます。モデルのフォールバックで、出来上がった出力が取り消された場合です。どちらが原因かは、結果の errors を見て見分けます。

呼び出し側で確かめること

終了コードではなく、subtype が success で、structured_output に値があるか で判定します。--output-format json のときの例です。

import json, shutil, subprocess

claude = shutil.which("claude")
if claude is None:
    raise RuntimeError("claude が PATH にありません")
p = subprocess.run(
    [claude, "-p", "--output-format", "json",
     "--setting-sources", "", "--tools", "", "--json-schema", schema],
    input=prompt, capture_output=True, text=True, encoding="utf-8",
)
try:
    out = json.loads(p.stdout)
except json.JSONDecodeError:
    # 不正なスキーマやフラグなど、実行前のエラーは stdout が空で、理由は stderr に出る
    raise RuntimeError(f"exit {p.returncode}: {p.stderr.strip()[:200]}") from None
data = out.get("structured_output")
if out.get("subtype") != "success" or data is None:
    # 上限やフォールバックの理由は errors、AI が諦めた理由は result に入る
    reason = out.get("errors") or (out.get("result") or "")[:200]
    raise RuntimeError(f"{out.get('subtype')}: {reason}")
# ここに来た data はスキーマの検証を通っている
  • プロンプトは引数ではなく標準入力で渡しています。長いプロンプトや改行を含むプロンプトでも崩れません
  • Windows では shutil.which("claude") で claude.exe のフルパスを取っています。npm で入れた版は実体が claude.cmd で、cmd.exe を経由するため、スキーマに ^ & | などが入ると引数が崩れることがあります。Windows でバッチに使うなら、claude.exe になるネイティブ版のインストーラーで入れるほうが安全です

逆に言えば、structured_output に値が入っていれば、それはスキーマの検証を通った値です。型や enum を呼び出し側で検証し直す必要はありません。

ただし、検証されるのは スキーマに書いた制約だけ です。

  • プロンプトで「title は短く」と書いても、maxLength が無ければ何文字でも通ります。守ってほしい形は、プロンプトではなくスキーマに書きます
  • format キーワード("format": "email" や "date-time" など)は、ドキュメントによると注釈として受け付けるだけで、検証されません。メールアドレスや日時の形式を保証したいなら、pattern で書くか、呼び出し側で検証します

-p で CLAUDE.md やツールを使わせない

最初の実行で、AI が諦めたときの result の文章は「そ、そんなはずありませんわ!?」で始まっていました。私のグローバル CLAUDE.md に書いている口調の設定です。claude -p でも、何も指定しなければ CLAUDE.md は読み込まれます。

さらに、設定なしで取り直したときには、上のふつうの課題で、AI が StructuredOutput の前に Bash や Write を呼び、作業フォルダに記録用の Markdown ファイルを書いていました。「記録して」を、ファイルに残すことだと受け取ったようです。

バッチ処理で使うなら、次のオプションで余計なものを切っておくのが安全です。

オプション効果
--setting-sources ""~/.claude やプロジェクトの設定(CLAUDE.md・hooks など)を読み込まない
--bareCLAUDE.md の自動読み込み、hooks、プラグインの同期、auto memory などをまとめて止める最小モード。ただし認証は ANTHROPIC_API_KEY か apiKeyHelper だけで、サブスクリプションのログイン(OAuth)は使えない
--tools ""ツールを使わせない。出力を作るだけの用途なら、ファイル操作やコマンド実行は要らない

930件の記事でも、--tools "" と --setting-sources "" を付けていました。今回の実験では付け忘れていて、その結果が上の2つです。

まとめ

  • --json-schema の出力は、StructuredOutput ツールの呼び出しとして作られ、Claude Code がスキーマで検証する
  • enum・maxLength・minLength・type・required・additionalProperties・minimum の違反は、どれも検出されて AI に差し戻された。format は検証されない
  • structured_output に入るのは検証を通った値だけ
  • AI が諦めると、structured_output が無いまま exit 0・success で終わる。試行回数の上限(既定5回、MAX_STRUCTURED_OUTPUT_RETRIES)まで粘って exit 1 になるのはまれだった
  • 呼び出し側は、終了コードではなく subtype と structured_output の値 で成否を判定する
  • 守らせたい形はプロンプトではなくスキーマに書く。バッチでは --setting-sources "" と --tools "" を付ける(API キーで動かすなら --bare でもよい)

「スキーマで検証される」と聞くと、成功すれば必ず形の合った出力が手に入る、と考えがちです。実際には「出力が無い成功」がありました。スキーマが守ってくれるのは中身の形までで、中身があるかどうかは呼び出し側で確かめる必要があります。

※ 公開当日に次の点を直しました。

  • CLAUDE.md を読み込まない条件で全ケースを取り直し、結果の表を3回分にして、諦め方を指示したケースの行を加えた
  • 指示とスキーマがぶつかったときの説明を、制約ごとの結果に合わせて書き直した
  • format が検証されないこと、モデルのフォールバックでも上限のエラーになることを追記した
  • --tools "" を付けないとファイル操作が実行されたこと、--bare では OAuth のログインが使えないことを追記した
  • 判定用のコード例と、Windows の npm 版での注意を直した

この記事をシェア

関連する記事

Claude Code の /code-review は、強度を省略すると前回の値を使う。xhigh と medium で時間は4倍、読んだ量は8倍違った

/code-review を強度なしで打ったら、1か月前に別のプロジェクトで指定した xhigh で動いていました。前回の値は ~/.claude.json に全プロジェクト共通で残ります。同じ記事のレビューを xhigh と medium で3回動かし、時間・トークン・ツールの使い方・指摘を比べました。

「〜のはず」は外れる。Claude Code と作業して、推測が外れて実測で分かったこと8件

「npm 版だから遅いはず」「auto mode によく止められている」「通信料はかかっていないはず」。Claude Code と一緒に個人開発をしていて、思い込みや推測が外れ、数えたり測ったりして初めて分かったことを8件集めました。自分の推測、確かめたつもり、AI の推測の3種類に分けて、どう確かめたかをまとめます。

Alexa-hosted スキルのビルドが jest で落ちた件と、ログを直接読めない環境でエラーを集計する方法

Alexa スキル「安眠ノイズ」に Jest のテストを足したら、Alexa-hosted のビルドが失敗するようになりました。原因の切り分けと、テストを lambda/ の外に出した構成、さらに CloudWatch Logs に直接アクセスできない Alexa-hosted で、エラーを Logs Insights で集計できる1行ログにした話をまとめます。

コード進行アプリの楽譜表示を自作した(大譜表・コード名どおりの綴り・転調で調号を変える)

コード進行生成アプリ Chord Eureka の楽譜表示を、html-midi-player(Magenta)の表示から、SVG を直接組み立てる自作の大譜表に置き換えました。E♭m がシャープで書かれる、進行によって音部記号が変わるといった問題をどう直したかと、置き換えるかどうかの判断をまとめます。

テーマから探す