Agent SDKの構造化出力入門 — JSON Schema・Zod・Pydanticで型安全に取得する
outputFormatにJSON Schemaを渡してエージェントの応答を検証済みJSONに固定する方法を、Zod・Pydanticでの型付けとエラー処理まで実例で解説します。
Agent SDKの構造化出力とは
Agent SDKの構造化出力は、エージェントが自由な文章ではなくJSON Schemaで定義した形どおりの検証済みJSONを返す機能です。エージェント自身はタスク完了に必要などんなツールを使ってもよく、最後にSDKがその出力をスキーマに照らして検証し、一致しなければ再度Claudeに促して直させます。再試行の上限内で検証が通らなければ、構造化データの代わりにエラーが返ります。
自由文のままだと、後段のアプリケーションはテキストを自前でパースして値を取り出す必要があります。レシピを検索してくるエージェントを例にすると、構造化出力なしでは「準備時間: 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 } の形で渡します。エージェントが完了すると、結果メッセージの structured_output フィールドに検証済みのデータが入ります。
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 を使った定義をサポートします。JSON Schemaとして無効なスキーマは実行開始時点でエラーになり、問題の内容が示されます。v2.1.205より前は無効なスキーマが無視され、エージェントが構造化されないテキストを返す挙動でした。同様に format("format": "email" など)キーワードは注釈として受け付けられるだけでSDKの検証には使われず、v2.1.205より前はこのキーワードを含むスキーマ自体が無効扱いになっていました。
例: 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 でスキーマ内の別定義を参照する形にすると、同じプロパティ定義を繰り返し書かずに済みます。前節で挙げた対応範囲(基本型、enum / const / required / ネストしたオブジェクト / $ref)は代表例で、網羅リストは公式がAPI側のJSON Schema limitationsに委ねています。そこに未対応として挙がっているキーワード(minLength / maxLength、minimum / maximum / multipleOf、再帰スキーマ、外部 $ref など)を使うと400エラーで返ります。失敗の出方は3通りに分かれます。JSON Schemaとして無効な形なら実行開始時点で落ち、未対応キーワードならAPIから400が返り、format だけは例外で注釈として受け付けられ検証に使われません。
たとえば「担当者」のように複数のフィールドで同じ構造を使う場合、トップレベルの $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箇所直すだけで両方に反映されます。他のJSON Schemaベースのツール向けに書いたスキーマをそのまま流用する場合も、参照先の定義自体が対応範囲内の型・キーワードだけで構成されているかを確認しておくと、移植時のエラーを防げます。
構造化出力が向く場面・向かない場面
| 用途 | 構造化出力 | 理由 |
|---|---|---|
| 抽出した値をUIやDBにそのまま渡す | 構造化出力使う | 理由パース処理を書かずに型安全な値を受け取れる |
| 複数ツールを使う調査タスクの最終結果をまとめる | 構造化出力使う | 理由途中のツール利用は自由なまま、最後の形だけ固定できる |
| チャットで人が読む説明文がメインの応答 | 構造化出力使わない | 理由スキーマ制約が自然な文章の生成を妨げることがある |
| 単発リクエストでツールを使わない単純なJSON化 | 構造化出力Claude JSONモードを検討 | 理由エージェントのツールループを介さない分シンプルになる |
エラー処理 — 検証に失敗したときの見分け方
構造化出力の生成は、スキーマに合うJSONをエージェントが作れなかったときに失敗します。原因はタスクに対してスキーマが複雑すぎる、タスク自体があいまい、再試行の上限に達した、のいずれかが典型です。検証エラーとは別に、モデルフォールバックが完成済みの出力をストリーム途中で取り消し、再試行が代わりを用意できないまま終わるケースもあります。ここで言うモデルフォールバックは、Fable 5やOpus 5が安全性分類器でリクエストにフラグを立てたときに別モデルへ自動的に切り替わる仕組みで、切り替えのタイミング次第では構造化出力の生成中に発生することがあります。結果メッセージの errors を見れば、どちらの原因かをデバッグ前に切り分けられます。
単発の query() 呼び出しでは、この失敗はエラー結果を yield するだけでは終わりません。Pythonは raise で例外を送出し、TypeScript版も例外を投げるため、try/catch(Pythonは try/except)で囲まずに呼び出すと未処理の例外でプロセスが落ちます。CLIのバッチ処理やWebアプリのリクエストハンドラの中で query() を呼ぶ場合は特に、この例外を握りつぶさずに上位でハンドリングしないと、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);
}| subtype | 意味 |
|---|---|
success | 意味出力が生成され、検証も通った |
error_max_structured_output_retries | 意味複数回試行しても有効な出力が残らなかった(検証失敗、またはモデルフォールバックによる取り消しで再試行が成功しなかった) |
subtype が success でも structured_output が無い結果もあり得ます。エージェントが構造化出力を一切生成せずに完了したケースのほか、required を多く付けすぎてタスクの情報量では埋めきれないスキーマを渡した場合も、この状態で終わることがあります。原因に心当たりがないのにこの状態を繰り返すなら、必須フィールドを減らして充足可能かを確認し、1つずつ戻して切り分けます。subtype === "success" かつ structured_output が存在する場合だけを成功として扱い、それ以外はすべて失敗として処理するのが安全な書き方です。
失敗時は結果メッセージの terminal_reason フィールドにも同じ原因が記録されます。構造化出力の再試行上限切れでは terminal_reason が structured_output_retry_exhausted になり、subtype の error_max_structured_output_retries と同じ失敗を指します。subtype は結果全体の成否を分岐させるための値、terminal_reason はループが止まった理由をログや監視ダッシュボードに残すための値と役割が分かれているので、両方を出力に含めておくとデバッグ時に原因を絞り込みやすくなります。
スキーマ設計でつまずきやすいパターン
構造化出力のエラーは、突き詰めると「スキーマが複雑すぎてタスクに対して満たしにくい」という同じ根に行き着きます。
必須フィールドを詰め込みすぎるとうまくいきません。情報の一部が欠けているだけでエージェントが検証を通せなくなるため、本当に必須な項目だけを required に残し、揃わない可能性があるフィールドは外しておきます。
性質の異なるデータを1つの巨大なオブジェクトにまとめるのも失敗しやすいパターンです。「会社情報」と「財務情報」のように目的が違うデータは、1つのスキーマに寄せず呼び出しを分けるほうが安定します。
enum の選択肢を狭くしすぎるのも要注意です。実際のデータに現れうる値が含まれていないと、エージェントは正しい値を持っていても出力できません。あいまいなプロンプトも同様で、エージェントがどんな出力を作ればよいか判断しづらくなります。
エラーが出たら、まずスキーマを一段階シンプルにしてから原因を切り分けるのが早道です。error_max_structured_output_retries が出た直後にスキーマをいきなり作り直すのではなく、required を1つずつ外して再実行し、どのフィールドがエージェントにとって満たしにくいのかを特定してから直すほうが、修正の手戻りが少なく済みます。
Claude JSONモード(Anthropic API)との違い
似た名前の機能に、Anthropic APIそのものが提供するstructured outputs(Claude JSONモード)があります。あちらはツール利用を伴わない単発リクエストの応答形式を固定する機能で、エージェントが自律的にツールを選んで複数ターン動くAgent SDKの構造化出力とは対象が異なります。API単体でJSON出力を固定したいだけならClaude JSONモードの使い方、エージェントに複数ツールを使わせた上で最後に型付きデータが欲しいなら本記事のAgent SDKが対象です。
Claude Code CLIの --json-schema フラグにも似た検証の仕組みがあり、エラーの出方や format キーワードの扱いはこちらのAgent SDKと共通する部分があります。CLI側での具体的なエラーメッセージと対処はClaude Code --json-schemaエラーの直し方にまとめています。
よくあるつまずき
- ストリーミングと組み合わせて構造化出力もリアルタイムに受け取ろうとする: 構造化出力はストリーミングデルタとしては流れず、最終の
ResultMessage.structured_outputにまとまってから届きます。詳細はAgent SDKのストリーミング出力を有効にするを参照してください subtypeを確認せずstructured_outputの有無だけで判定する:successかつ値が存在する場合だけを成功として扱わないと、失敗ケースを見落とします- Zodのターゲットバージョンを指定し忘れる: 既定のdraft 2020-12のままだとSDKの検証(draft-07)と合わず、スキーマエラーになります
よくある質問
構造化出力とツール呼び出しの結果は別物ですか
別物です。ツール呼び出しの結果はタスク実行の途中経過で、構造化出力はタスクが完了した時点の最終レスポンスの形を指定するものです。
まとめ
Agent SDKの構造化出力は、outputFormat にJSON Schemaを渡すだけでエージェントの最終応答を型で固定できる機能です。ZodやPydanticを使えば、スキーマ定義とレスポンスの型安全性を両方手に入れられます。エラーは subtype で原因を切り分けられるので、success かつ structured_output が存在する場合だけを成功として扱う書き方を徹底すれば、想定外の失敗を取りこぼしません。