はじめに / 対象と前提
Claude API で「JSON で返して」とプロンプトに書いたのに、返答の先頭に「はい、以下が抽出結果です:」が付いて json.loads() が落ちる——この定番のワナは、現在は Structured Outputs で仕組みごと潰せる。
この記事の対象と前提:
- 想定読者:Claude API を Python から叩いていて、LLM の応答を後段のプログラムでパースしている人
-
環境:Python 3.13 / anthropic SDK 1.x / モデルは
claude-opus-5(Claude 4.6 以降なら同じ書き方) - 自分は自律エージェントのツール応答パースで実際にハマったので、その回避策込みで書く
TL;DR
- 応答スキーマは
output_config: {"format": {...}}で指定する。ネット上に多い旧output_formatトップレベル引数+beta ヘッダーの記事はもう古い - Pydantic モデルを渡せる
client.messages.parse()が最短。response.parsed_outputが検証済みインスタンスで返る - スキーマには
additionalProperties: falseとrequired全列挙が必須。忘れると 400
手順 / 動かし方
1. Pydantic モデルで受ける(推奨)
from pydantic import BaseModel
import anthropic
class ContactInfo(BaseModel):
name: str
email: str
plan: str
demo_requested: bool
client = anthropic.Anthropic()
response = client.messages.parse(
model="claude-opus-5",
max_tokens=16000,
messages=[{
"role": "user",
"content": "抽出して: 田中さん (tanaka@example.com) は Enterprise プラン希望、デモ申込あり",
}],
output_format=ContactInfo, # parse() には Pydantic モデルをそのまま渡す
)
contact = response.parsed_output # 検証済み ContactInfo インスタンス
print(contact.name, contact.demo_requested)
実行結果:
田中 True
json.loads() も try/except も書かない。パースと検証は SDK 側で終わっている。
2. 生の JSON Schema で受ける
Pydantic を入れたくない場合は messages.create() に output_config を渡す。
response = client.messages.create(
model="claude-opus-5",
max_tokens=16000,
messages=[{"role": "user", "content": "..."}],
output_config={
"format": {
"type": "json_schema",
"schema": {
"type": "object",
"properties": {
"name": {"type": "string"},
"demo_requested": {"type": "boolean"},
},
"required": ["name", "demo_requested"],
"additionalProperties": False,
},
}
},
)
この場合、最初の text ブロックがスキーマに適合した JSON であることが保証されるので、json.loads() 一発で受けられる。
3. ツール引数側も固めるなら strict: true
応答だけでなく tool use の input も検証したいときは、ツール定義のトップレベルに strict を置く。
tools=[{
"name": "book_flight",
"description": "フライトを予約する",
"strict": True, # tool_choice 側ではなくここ
"input_schema": {
"type": "object",
"properties": {
"destination": {"type": "string"},
"passengers": {"type": "integer"},
},
"required": ["destination", "passengers"],
"additionalProperties": False,
},
}]
これで tool_use.input がスキーマ通りであることが保証され、エージェントループ内の防御的パースがほぼ消える。
ハマりどころ
1. 旧記事の output_format + beta ヘッダーを写すと 400
初出時は anthropic-beta: structured-outputs-2025-11-13 ヘッダー+トップレベル output_format パラメータのベータ機能だったため、検索上位の記事は今もその書き方が多い。現在は GA で beta ヘッダー不要、パラメータは output_config.format に移動済み。旧形式を新 SDK に写経すると Extra inputs are not permitted 系の 400 で落ちる。エラーメッセージ内のパラメータ名を見たら、まず記事の鮮度(2025年末以前か)を疑うのが早い。
2. additionalProperties: false と required を忘れて 400
Structured Outputs のスキーマは JSON Schema のサブセットで、object には additionalProperties: false と全プロパティの required 列挙が要求される。Pydantic 経由(parse())なら SDK が変換してくれるので、生スキーマを書くときだけ注意。オプショナルにしたいフィールドは "type": ["string", "null"] のように null 許容にして required には残す。
3. max_tokens 切れの JSON は保証対象外
スキーマ保証は「完走した応答」に対するもの。stop_reason が max_tokens だと JSON が途中で切れて壊れたまま返る。自分はここを見落として、深夜バッチが週1回だけ落ちる不具合を仕込んだ。
if response.stop_reason == "max_tokens":
raise RuntimeError("truncated: max_tokens を増やして再試行")
max_tokens はケチらず 16000 程度を既定にしておくのが安全。
背景・補足
- 以前の定番だった「assistant メッセージに
{を prefill して JSON を強制」は、Claude 4.6 以降のモデルでは prefill 自体が 400 で拒否されるため使えない。Structured Outputs がその公式後継 - 新しいスキーマの初回リクエストは、サーバー側のスキーマ処理(文法へのコンパイル)が入るぶん少し遅くなることがある。結果はキャッシュされるので2回目以降は通常速度。レイテンシ計測は初回を捨てて測る
- ドキュメントの
citations機能とは併用不可(400)なので、引用付き RAG の応答には使えない
まとめ
- JSON 強制は
output_config.format(またはmessages.parse()+ Pydantic)が現行の正解。beta ヘッダー時代の記事は形式が違うので写さない - スキーマは
additionalProperties: false+required全列挙がお約束 - 保証されるのは完走した応答だけ。
stop_reason == "max_tokens"チェックは必ず入れる - prefill による JSON 強制は新モデルでは廃止済み。移行先も Structured Outputs