StructuredOutputがoutputキーに包まれるバグと回避策
claude-agent-sdk-pythonのStructuredOutputツールが応答をoutputキーで包み、スキーマ検証が失敗する既知のバグの実態と回避策を解説します。
StructuredOutputツールがoutputキーで包まれる問題とは
claude-agent-sdk(Python)で ClaudeAgentOptions(output_format=...) を使うと、内部で呼ばれる StructuredOutput というツールが、指定したスキーマに合うデータをそのまま返さず {"output": {...}} の形で包んでしまうことがあります。スキーマ側は additionalProperties: false で余分なキーを禁止しているため、この包みが付いた瞬間に検証が失敗し、ResultMessage.structured_output は None になります。
この挙動はGitHub Issue #502として2026年1月21日に報告され、8月19日にも解決を求めるコメントが付いています。同じプロンプトを実行しても包まれるときと包まれないときがある非決定的な挙動である点が、原因の特定と再現テストを難しくしています。
いつ起きるか — 報告されているラップキーと再現性の低さ
起きるタイミングは一定していません。報告者が挙げているツールのエラーメッセージは次の内容です。
Output does not match required schema: root: must have required property 'actions', root: must NOT have additional propertiesactions フィールドを持つスキーマに対し、エージェントが {"actions": [...]} を返せば検証は通ります。ところが同じ呼び出しで {"output": {"actions": [...]}} が返ると、actions はトップレベルに無いので「requiredプロパティが無い」というエラーと、output というキー自体が additionalProperties: false に反する「余分なプロパティがある」というエラーが同時に出ます。
コメント欄では、包みに使われるキーが output だけでないことも報告されています。
| ラップキー | 報告者 |
|---|---|
output | 報告者報告者本人(初報) |
response | 報告者AgentWrapper |
json | 報告者AgentWrapper(2件目のコメント) |
3種類のキーが別々の環境から報告されている以上、キー名を1つずつ許容リストに追加する対症療法では追いつきません。実際にコメント欄のyarjorも、output と result は個別に処理していたところに json という新しいキーが出て対応が崩れたと述べています。
報告に含まれるもう1つの実害は、検証エラーを見たエージェント自身の挙動です。47件の要素を含む配列を返すはずのタスクで、スキーマエラーを受け取ったエージェントが出力を1件にまで削って「修正」しようとした例が記録されています。エラーメッセージが output という余分なキーの存在を指摘する一方で actions という必須キーの不在も指摘するため、エージェント側はどちらを直せば良いか判断できず、データ量を減らす方向に倒れたと見られます。
ラップはどの層で起きているか — CLIのStructuredOutputツールと再試行ループ
Python SDKの ClaudeAgentOptions(output_format=...) は、SDK自身がJSONを直接検証しているわけではなさそうです。公式のPython SDKリファレンスは、SDKがローカルのCLIバイナリをサブプロセスとして起動する構成であることを説明しています。一方、output_format の検証・再試行という挙動そのものは別ページの公式ドキュメント(構造化出力)に記載があり、CLIの変更履歴には StructuredOutput という名前の内部ツールへの修正が繰り返し記録されています。これらを重ねると、検証と再試行を担っているのはCLI側の StructuredOutput ツールだと考えられますが、公式ドキュメントが「output_formatの内容をCLIのStructuredOutputツールが検証する」と名指しで説明しているわけではありません。
だとすれば、CLIの変更履歴に並ぶ StructuredOutput 関連の修正は、Python SDKにもTypeScript SDKにも及ぶ可能性があります。Issue内でjmehnleが指摘しているとおり、Claude APIが単体で提供する client.messages.parse(output_format=...)(旧output_format、現行のoutput_config.format)はスキーマに沿うトークンしか生成させないスキーマ制約サンプリングを使うのに対し、Agent SDK側の output_format は生成後にスキーマと突き合わせ、合わなければ再度促して直させる検証・再試行ループという別方式だとjmehnleは述べています。ただし、output などのキーで包む処理がこの再試行ループの実装自体で起きているのか、モデルがツール入力として生成するJSON側で起きているのかは、Issue内でも特定されていません。
再試行ループ自体は現行の公式ドキュメントにも明記されています。結果メッセージの subtype が error_max_structured_output_retries になるのは、複数回の試行後も有効な出力が残らなかった場合です。公式ドキュメントはこれとは別に、モデルフォールバックが完成済みの出力をストリーム途中で取り消し、再試行が代わりを用意できないまま終わるケースも同じsubtypeになりうると説明しています。ただし、このエラー一覧には「途中でツールの出力が余分なキーに包まれる」という個別の原因は挙げられておらず、公式のエラー分類は成功か再試行上限切れかの二択にとどまっています。
隣接する構造化出力の修正は続くのに、この包みだけ残る
Claude Code CLIの変更履歴を追うと、StructuredOutput 周りの修正は継続的に入っています。
| バージョン | 公開日 | 内容 |
|---|---|---|
| v2.1.81 | 公開日2026年3月20日 | 内容CLAUDE_CODE_DISABLE_EXPERIMENTAL_BETASが構造化出力のbetaヘッダーを抑制しない不具合を修正 |
| v2.1.89 | 公開日2026年4月1日 | 内容複数スキーマを使うと約50%の確率で失敗するStructuredOutputのスキーマキャッシュのバグを修正 |
| v2.1.186〜187 | 公開日2026年6月22〜23日 | 内容スキーマ検証失敗後にStructuredOutputを無限に呼び直すループと、バックグラウンドジョブが完了しないまま止まる問題を修正 |
| v2.1.196 | 公開日2026年6月29日 | 内容スキーマ拒否されたStructuredOutputの再試行表示が二重になる不具合を修正 |
| v2.1.205 | 公開日2026年7月8日 | 内容無効なスキーマが黙って無視される挙動と、formatキーワードを含むスキーマが無効扱いになる不具合を修正 |
| v2.1.260 | 公開日2026年9月3日 | 内容満たせないJSON Schemaを事前に拒否し、再試行上限エラーに直近の検証失敗内容を含めるよう改善 |
この一覧が示すのは、無限ループ・キャッシュ・無効スキーマの扱いといったStructuredOutputツールの信頼性向上は継続しているという事実です。一方で、Issue #502が指摘する「成功した出力自体が output / response / json のいずれかで包まれる」という不具合は、この一覧のどこにも修正として現れていません。claude-agent-sdkのPyPI最新版は0.2.152(2026年9月2日公開)で、そのCHANGELOGにもこの包み処理の修正は記載がなく、Issueは2026年8月19日の督促コメントを最後にopenのままです。
報告時点の環境はclaude-agent-sdk==0.1.19でした。同じCHANGELOGを数えると、0.1.19から最新の0.2.152までに133件のバージョンが記録されており、そのどれにも output / response / json キーの包みを解消したという記述はありません。ツールが「呼び出しに成功したかどうか」を扱う経路の改善と、「成功した呼び出しの中身をどう返すか」を扱う経路の改善は、別の修正対象として扱われてきたことがうかがえます。
実務でできる回避策
根本修正がない以上、アプリ側でラップを解く処理を挟むのが現実的な対処です。コメント欄で複数の利用者が採用しているのは、PreToolUse フックで StructuredOutput ツールの呼び出しを捕まえ、渡される tool_input が既知のラップキー1つだけを持つオブジェクトになっていないかを確認して、包みを外してから検証に進ませる方法です。Python SDKの PreToolUseHookSpecificOutput には updatedInput フィールドがあり、フックがここに値を返すとツールへ渡される入力そのものが書き換わります。
from claude_agent_sdk import ClaudeAgentOptions, HookMatcher
# output/response/jsonはIssueで報告された3種のラップキー。resultは
# yarjorが自身の実装で既知キーとして扱っていた語で、この3種の報告には含まれない。
KNOWN_WRAPPER_KEYS = {"output", "response", "json", "result"}
async def unwrap_structured_output(input_data, tool_use_id, context):
"""StructuredOutputツールに渡される直前のtool_inputが
既知のキー1つだけで包まれていれば中身を取り出す。"""
if input_data["tool_name"] != "StructuredOutput":
return {}
tool_input = input_data["tool_input"]
if len(tool_input) == 1:
(key, value) = next(iter(tool_input.items()))
if key in KNOWN_WRAPPER_KEYS and isinstance(value, dict):
return {
"hookSpecificOutput": {
"hookEventName": "PreToolUse",
"updatedInput": value,
}
}
return {}
options = ClaudeAgentOptions(
hooks={
"PreToolUse": [
HookMatcher(matcher="StructuredOutput", hooks=[unwrap_structured_output]),
],
}
)この方式には限界があります。コメント欄のyarjorが報告しているとおり、output と result を許容リストに入れていても、新しいキー(jsonなど)が出た時点で対応漏れが発生します。KNOWN_WRAPPER_KEYS は一度書いたら終わりではなく、想定外のトップレベルキー1つだけを持つオブジェクトが返ってきた場合に警告ログを出すなど、未知のバリエーションを検知できる形にしておくと被害を抑えやすくなります。
検証エラーの原因を切り分けるときは、まず unwrap_structured_output のようなフックで包みを外し、それでも検証が通らない場合にだけスキーマの required 定義を疑う、という順序にすると原因追跡が早くなります。
もう1つの方向性として、スキーマ自体に複数の形を受理させる書き方も考えられます。JSON Schemaの anyOf で「素のオブジェクト」と「output キーで包まれたオブジェクト」の両方を定義するアプローチです。ただし、Agent SDK側のスキーマ検証がトップレベルの anyOf をどう扱うか(v2.1.260で入った不成立スキーマの事前拒否の対象になるかどうかを含め)は一次ソースで確認できていません。また下記の例は output キーの包みにしか対応しておらず、本記事で報告されている response / json / result の包みには効きません。採用する場合は、実際に手元の環境で検証が通るか確かめてからにするのが安全です。
schema_tolerant = {
"type": "object",
"anyOf": [
{"type": "object", "required": ["actions"], "properties": {"actions": {"type": "array"}}},
{
"type": "object",
"required": ["output"],
"properties": {
"output": {
"type": "object",
"required": ["actions"],
"properties": {"actions": {"type": "array"}},
}
},
},
],
}Issue内では、output_format のこの不安定さを理由に、エージェントには自然文で回答させたうえで別のモデルに構造化を任せる、という運用に切り替えたという報告もあります。根本原因を回避する手段ではなく、StructuredOutput ツール自体への依存を減らす選択です。どの回避策も一長一短があるため、実際の失敗頻度とタスクの重要度を見ながら選ぶことになります。
| 回避策 | 効果 | 弱点 |
|---|---|---|
PreToolUseフックで既知キーを解く | 効果実装が単純ですぐ試せる | 弱点未知のラップキーには対応できない |
スキーマにanyOfで複数形状を定義 | 効果outputキーの包みには対応できる可能性がある(要検証) | 弱点response/json/resultには効かず、中身を取り出す後処理も別途必要 |
| 自然文で受けて別処理で構造化する | 効果StructuredOutputツールへの依存自体を無くせる | 弱点元のエージェントのツール利用結果を活かしにくい |
影響の出方は利用形態で変わる
同じバグでも、SDKの使い方によって実害の大きさは変わります。
| 利用形態 | 影響 |
|---|---|
単発のquery()で1回だけ構造化出力を取る | 影響非決定的なため毎回失敗するわけではないが、失敗時はstructured_outputがNoneのまま処理が止まる |
本番のリクエストハンドラでoutput_formatを使う | 影響同じ入力でも成功・失敗が入れ替わるため、再現性のあるテストが書きにくい |
| 47件のような大きな配列を1回のスキーマで受け取る | 影響エージェントがエラーメッセージを誤読し、データを大幅に削って「修正」する可能性がある(報告例) |
PreToolUseフックで独自にラップを解いている | 影響既知のキーには対処できるが、未知のキーが出た時点で同じ失敗が再発する |
structured_outputがNoneになる他の原因との切り分け
structured_output が None になる原因は、このラップだけではありません。必須フィールドが多すぎて情報が埋めきれない場合や、エージェントが構造化出力を一切生成せずに完了した場合も同じ状態になります。これらの一般的な原因の見分け方はAgent SDKのエラー集にまとめているので、まずそちらで典型的な原因を除外してから、本記事のラップキーを疑う順序が効率的です。output_format そのものの基本的な使い方やZod・Pydanticでのスキーマ定義はAgent SDKの構造化出力入門を参照してください。
スキーマの additionalProperties: false が絡む検証の仕組みや、SDKが対応するJSON Schemaの範囲についてはStructured outputsのJSON Schema制限をSDKが自動変換する仕組みで詳しく扱っています。
よくある質問
TypeScript版のAgent SDKでも同じ問題は起きますか
Issue #502の報告はPython SDK(claude-agent-sdk)に対するものです。ただしPython SDKはローカルのCLIバイナリをサブプロセスとして起動する構成で、TypeScript SDKも同じCLIを起動します。検証・再試行の実装がCLI側にあると見られる以上、TypeScript側が無関係だと言い切れる根拠はありませんが、TypeScript SDK固有の同種の報告はこのIssueには含まれていません。
スキーマの書き方が悪くて起きているのではないですか
報告内容を見る限り、スキーマ自体は妥当でも発生しています。同じプロンプト・同じスキーマで包まれる回と包まれない回があるという非決定性が、スキーマ設計のミスでは説明が付かない根拠です。まずは本記事のunwrap_structured_outputのようなフックで切り分け、それでも解決しない場合にスキーマのrequired過多を疑う順序が無駄がありません。
まとめ
claude-agent-sdk-python の StructuredOutput ツールは、成功した検証済みの出力を output / response / json などのキーで非決定的に包んでしまうことがあり、スキーマの additionalProperties: false と衝突して検証が失敗します。GitHub Issue #502は2026年1月の報告以降もopenのままで、隣接する StructuredOutput の不具合修正がCLIの変更履歴に並ぶ一方、この包み自体の修正は記載されていません。根本修正を待つ間は、PreToolUse フックで既知のラップキーを外してから検証に進ませる方法が実務上の回避策になりますが、キーの種類が増える限り対症療法である点は踏まえておく必要があります。