Agent SDKの構造化出力入門 — JSON Schema・Zod・Pydanticで型安全に取得する
outputFormatにJSON Schemaを渡してエージェントの応答を検証済みJSONに固定する方法を、Zod・Pydanticでの型付けとエラー処理まで実例で解説します。
Agent SDKの構造化出力とは
Agent SDKの構造化出力は、エージェントが自由な文章ではなくJSON Schemaで定義した形どおりの検証済みJSONを返す機能です。エージェントはタスクの途中で好きなツールを使えます。形が決まるのは最後の応答だけです。
結果が返るまでの流れ
- 1
エージェントがツールを使って作業する
GrepやBashなど、タスクに必要なツールを自由に使います。
- 2
SDKが出力をスキーマに照らして検証する
一致すれば、結果メッセージの
structured_outputに検証済みのデータが入ります。 - 3
一致しなければClaudeに再度促す
SDKがClaudeに直させます。再試行の上限内で通れば、そのまま成功です。
- 4
上限を超えるとエラー結果になる
構造化データの代わりにエラーが返ります。見分け方は検証に失敗したときの見分け方の節にあります。
自由文のままだと、後段のアプリケーションはテキストを自前でパースして値を取り出す必要があります。レシピを検索してくるエージェントを例にすると、構造化出力なしでは「準備時間: 15分」のような文字列から数値を抜き出す処理が要ります。構造化出力ありなら、はじめから prep_time_minutes: 15 という数値フィールドとして届きます。型安全に使うにはZod(TypeScript)かPydantic(Python)でスキーマを書くのが実務上の近道で、どちらもJSON Schemaを自動生成してくれます。
outputFormatにJSON Schemaを渡す
使い方はシンプルです。まずJSON Schemaを定義します。それを query() の options.outputFormat(Pythonは output_format)に渡します。渡す値は { type: "json_schema", schema } の形です。
import { query } from "@anthropic-ai/claude-agent-sdk";
const schema = {
type: "object",
properties: {
company_name: { type: "string" },
founded_year: { type: "number" },
headquarters: { type: "string" }
},
required: ["company_name"]
};
for await (const message of query({
prompt: "Research Anthropic and provide key company information",
options: { outputFormat: { type: "json_schema", schema } }
})) {
if (message.type === "result" && message.subtype === "success" && message.structured_output) {
console.log(message.structured_output);
// { company_name: "Anthropic", founded_year: 2021, headquarters: "San Francisco, CA" }
}
}Python版も構造は同じで、ClaudeAgentOptions に output_format を渡します。
from claude_agent_sdk import query, ClaudeAgentOptions, ResultMessage
schema = {
"type": "object",
"properties": {
"company_name": {"type": "string"},
"founded_year": {"type": "number"},
"headquarters": {"type": "string"},
},
"required": ["company_name"],
}
async for message in query(
prompt="Research Anthropic and provide key company information",
options=ClaudeAgentOptions(output_format={"type": "json_schema", "schema": schema}),
):
if isinstance(message, ResultMessage) and message.structured_output:
print(message.structured_output)ZodとPydanticで型安全なスキーマを定義する
JSON Schemaを手書きする代わりに、ZodかPydanticでスキーマを定義すれば、JSON Schemaの生成とレスポンスの型付けを両方まかなえます。SDKが検証に使うのはJSON Schema draft-07です。新しい版を宣言したスキーマは拒否されるため、draft 2020-12を既定にするZodは変換時に target: "draft-7" を指定します。
import { z } from "zod";
import { query } from "@anthropic-ai/claude-agent-sdk";
const FeaturePlan = z.object({
feature_name: z.string(),
summary: z.string(),
steps: z.array(
z.object({
step_number: z.number(),
description: z.string(),
estimated_complexity: z.enum(["low", "medium", "high"])
})
),
risks: z.array(z.string())
});
type FeaturePlan = z.infer<typeof FeaturePlan>;
const schema = z.toJSONSchema(FeaturePlan, { target: "draft-7" });
for await (const message of query({
prompt: "Plan how to add dark mode support to a React app.",
options: { outputFormat: { type: "json_schema", schema } }
})) {
if (message.type === "result" && message.subtype === "success" && message.structured_output) {
const parsed = FeaturePlan.safeParse(message.structured_output);
if (parsed.success) {
console.log(parsed.data.feature_name);
}
}
}Pythonは BaseModel を継承したクラスを書き、.model_json_schema() でスキーマを取得します。応答が届いたら FeaturePlan.model_validate(message.structured_output) で型付きのオブジェクトに変換できます。Zod・Pydanticどちらの経路でも、以降のコードはIDEの補完と型チェックが効いた状態で plan.summary や plan.steps にアクセスできます。
outputFormatが受け付けるオプション
outputFormat / output_format が受け取るのは2つのキーだけです。
| キー | 内容 |
|---|---|
type | 内容構造化出力を使うなら "json_schema" を指定 |
schema | 内容出力の形を定義するJSON Schemaオブジェクト。Zodなら z.toJSONSchema(schema, { target: "draft-7" })、Pydanticなら .model_json_schema() で生成する |
SDKは基本型(オブジェクト・配列・文字列・数値・真偽値・null)をサポートします。enum / const / required / ネストしたオブジェクト / $ref を使った定義も使えます。format("format": "email" など)は注釈として受け付けられるだけで、SDKの検証には使われません。
JSON Schemaとして無効なスキーマは、実行開始時点でエラーになり問題の内容が示されます。v2.1.205より前は無効なスキーマが無視され、エージェントが構造化されないテキストを返す挙動でした。format を含むスキーマも、同じ版より前は無効扱いでした。
CLIの --json-schemaで検証エラーを実際に出す
SDKと同じ検証は、Claude Code CLIの --json-schema(printモード専用)にもあります。無効なスキーマを渡すと、claude は Error: --json-schema is not a valid JSON Schema で終了します。次の出力は、わざと壊したスキーマを渡したv2.1.287の手元実行の出力です。
$ claude -p --json-schema '{"type":"bogus"}' "hi"
Error: --json-schema is not a valid JSON Schema: data/type must be equal to one of the allowed values, data/type must be array, data/type must match a schema in anyOf
$ claude -p --json-schema 'not json' "hi"
Error: --json-schema is not valid JSON: JSON Parse error: Unexpected identifier "not"
$ claude -p --json-schema '{"$schema":"https://json-schema.org/draft/2020-12/schema","type":"object"}' "hi"
Error: --json-schema is not a valid JSON Schema: no schema with key or ref "https://json-schema.org/draft/2020-12/schema"エラー文は3種類に分かれます。JSONとして読めない場合、JSON Schemaとして無効な場合、そして宣言された $schema の版を検証側が解決できない場合です。3つ目は、新しい版を宣言したスキーマがSDKで拒否される話と同じ向きの失敗です。CLI側の対処はClaude Code --json-schemaエラーの直し方にあります。
例: TODO追跡エージェント
次の例は、コードベースからTODOコメントを探し(Grep)、各行のgit blame情報を調べます(Bash)。2種類のツールをエージェントが自律的に使い分け、結果を1つの構造化レスポンスにまとめます。
const todoSchema = {
type: "object",
properties: {
todos: {
type: "array",
items: {
type: "object",
properties: {
text: { type: "string" },
file: { type: "string" },
line: { type: "number" },
author: { type: "string" },
date: { type: "string" }
},
required: ["text", "file", "line"]
}
},
total_count: { type: "number" }
},
required: ["todos", "total_count"]
};
for await (const message of query({
prompt: "Find all TODO comments in this codebase and identify who added them",
options: { outputFormat: { type: "json_schema", schema: todoSchema } }
})) {
if (message.type === "result" && message.subtype === "success" && message.structured_output) {
const data = message.structured_output;
console.log(`Found ${data.total_count} TODOs`);
}
}author と date は必須にしていません。git blame情報が取れないファイルがあるためです。エージェントは分かる範囲を埋めて、分からない項目はフィールドごと省略します。常に存在するとは限らない情報を required から外しておくと、この省略が正しく機能します。
ネストしたオブジェクトと$refを使う
共通の型を複数箇所で使い回したいときは、$ref でスキーマ内の別定義を参照します。たとえば「担当者」のように同じ構造を複数のフィールドで使う場合は、トップレベルの $defs に定義を一度だけ置きます。使う側からは { "$ref": "#/$defs/person" } の形で参照します。
const schema = {
type: "object",
$defs: {
person: {
type: "object",
properties: {
name: { type: "string" },
role: { type: "string" }
},
required: ["name"]
}
},
properties: {
author: { $ref: "#/$defs/person" },
reviewer: { $ref: "#/$defs/person" }
},
required: ["author"]
};author と reviewer は同じ person 定義を参照しているので、フィールドの追加や必須項目の変更は $defs 側を1箇所直すだけで済みます。
SDKのページは、対応範囲の全体をAPI側のJSON Schema limitationsに委ねています。そこで未対応とされているのは、再帰スキーマと外部 $ref です。数値制約(minimum / maximum / multipleOf など)と文字列制約(minLength / maxLength など)も未対応です。配列の minItems は0と1だけが使えます。API側では、未対応の機能を使うと400エラーが返ります。
API側のページは、additionalProperties を false 以外にする指定を未対応とします。SDKのページの例はこのキーを指定せず、SDKの制限一覧はAPI側のページを参照先として示すだけです。API側の制限がSDKでも同じ形で働くかは、SDKのページだけでは決まりません。additionalProperties: true を使うスキーマは、実行して通るかを先に試すと安全です。
文字列の format は、API側が email や date-time などを対応形式として挙げ、SDKとの扱いが分かれます。
構造化出力が向く場面・向かない場面
構造化出力を使うかどうか
使う
- 抽出した値をUIやDBにそのまま渡す
- 複数ツールを使う調査の最終結果をまとめる
別の手段を選ぶ
- 人が読む説明文がメインの応答は、形を固定せず自由文のままにする
- 出力の質そのものを基準に採点したい場合は、Managed AgentsのRubricでアウトカムを定義する方法が向いています
検証に失敗したときの見分け方
構造化出力の生成は、スキーマに合うJSONをエージェントが作れなかったときに失敗します。典型的な原因は3つです。スキーマがタスクに対して複雑すぎる、タスク自体があいまい、再試行の上限に達した、のいずれかです。
結果が成功でない場合は、次の3つに切り分けます。
成功にならない3つのケース
検証が通らなかった
subtypeがerror_max_structured_output_retriesになります。複数回試しても有効な出力が残らなかった状態です。モデルフォールバックが出力を取り消した
完成済みの出力がストリーム途中で取り消され、再試行も代わりを用意できなかった場合です。検証エラーがなくても、同じ
subtypeで終わります。success なのに値が無い
subtypeはsuccessでもstructured_outputが空(PythonはNone、TypeScriptはundefined)の結果です。エージェントが構造化出力を作らずに完了した場合などに起きます。
メッセージの terminal_reason にもループが止まった理由が入ります。TypeScriptのリファレンスが挙げる値は19種類です。completed・max_turns・budget_exhausted などがあり、その1つが structured_output_retry_exhausted です。ログには subtype と terminal_reason を両方残しておくと、あとから原因を絞りやすくなります。
最初の2つは、結果メッセージの errors を見ればデバッグ前に切り分けられます。ここで言うモデルフォールバックは、安全性分類器がリクエストにフラグを立てたとき別モデルへ切り替わる仕組みです。対象はFable 5.1・Fable 5・Opus 5.5・Sonnet 5.5・Opus 5です。切り替え先はモデルと分野で決まり、Sonnet 5.5とOpus 5は生物学のフラグでは切り替わらず拒否で終わります。フラグが多いのはサイバーセキュリティと生物学の分野です。切り替わったあとのセッションは、切り替え先のモデルで続きます。
Python版では、検証失敗時にoutputなどのキーで結果がラップされることがあります。このバグの回避策を参照してください。
単発の query() 呼び出しでは、失敗はエラー結果を yield するだけでは終わりません。Pythonは raise で例外を送出し、TypeScript版も例外を投げます。try/catch(Pythonは try/except)で囲まずに呼ぶと、未処理の例外でプロセスが落ちます。バッチ処理やリクエストハンドラの中で呼ぶ場合は、1件の失敗が全体を止めないよう上位で受けます。
try {
for await (const message of query({
prompt: "Research Anthropic and provide key company information",
options: { outputFormat: { type: "json_schema", schema } }
})) {
if (message.type === "result" && message.subtype === "success" && message.structured_output) {
console.log(message.structured_output);
}
}
} catch (error) {
console.error("構造化出力の取得に失敗しました:", error);
}接続やプロセスの失敗では結果メッセージ自体が出ないため、catch の側でしか拾えません。エラー結果の場合は、catch に入る前に subtype の分岐が走ります。
スキーマが満たせないときの直し方
深くネストして必須フィールドが多いスキーマは、満たしにくくなります。情報が揃わない可能性のあるフィールドは任意にして、本当に必須な項目だけを required に残せば、情報の一部が欠けても検証を通せます。プロンプトには、取り出したい内容を明確に書きます。
どんな出力でも満たせないスキーマの例が、長さ制約が矛盾しているケースです。繰り返し失敗するなら、スキーマが充足可能かを確認し、通る形まで単純化してから制約を1つずつ戻します。
Claude JSONモード(Anthropic API)との違い
似た名前の機能に、Anthropic APIそのものが提供するstructured outputs(Claude JSONモード)があります。あちらはツール利用を伴わない単発リクエストの応答形式を固定する機能です。エージェントが自律的にツールを選んで複数ターン動くAgent SDKの構造化出力とは対象が異なります。API単体でJSON出力を固定したいだけなら、Claude JSONモードの使い方が対象です。
ストリーミングで構造化出力はどう見えるか
ストリーミングを有効にして構造化出力を受け取ろうとすると、期待とは違う形で流れます。部分メッセージを有効にした場合、JSONはツール呼び出しの未検証の input_json_delta チャンクとして流れます。検証済みの結果が届くのは、最終の ResultMessage.structured_output だけです。
途中のチャンクを画面に出す用途はあり得ますが、アプリケーションが使う値は最終結果から取ります。ストリーミング側の設定はAgent SDKのストリーミング出力を有効にするにあります。
まとめ
ツールを使う調査の最後に型付きデータが欲しい場面では、Agent SDKの構造化出力が向いています。呼び出し側は、結果メッセージの失敗と、例外として投げられる失敗の2経路を両方受ける前提で組みます。