本番デプロイ前日の深夜、JSONDecodeError: Expecting value: line 1 column 1 というエラーが出て冷や汗をかいた——そんな経験をした開発者は意外と多いのではないでしょうか。私自身も、Claude API で JSON を返させるコードをちゃんと書いたはずなのに、特定の入力でだけパースが失敗する、という問題に数時間費やした記憶があります。
原因を調べてみると、問題はコードではなくモデルの返し方にあることが多いです。Claude API の JSON 出力でよく起きる失敗パターン5つと、それぞれに対する即効 Fix を順番にご紹介します。
まず症状を「生テキスト」で確認する
パースエラーが出たら、最初にやるべきことは repr() でレスポンスの中身を確認することです。
import anthropic
client = anthropic.Anthropic()
message = client.messages.create(
model="claude-sonnet-4-6",
max_tokens=1024,
messages=[
{"role": "user", "content": "name と age を含む JSON を返してください"}
]
)
raw = message.content[0].text
print(repr(raw)) # 改行・制御文字・エスケープをそのまま確認できるrepr() を使うと、肉眼では分からない制御文字や二重エスケープもすべて見えます。以下の5パターンのうちどれに当てはまるかは、ここで一目瞭然になるはずです。
原因1: マークダウンのコードブロックに囲まれる
最も遭遇頻度が高いパターンです。「JSON で返してください」と書くだけでは、Claude がこう返してくることがあります:
```json
{"name": "田中", "age": 30}
人間向けのフォーマットとして丁寧な対応なのですが、機械処理には邪魔です。
**Fix — 正規表現で除去する**
```python
import re
import json
def extract_json_text(text: str) -> str:
"""コードブロックが混入していても JSON 文字列を取り出す"""
# ```json ... ``` または ``` ... ``` を除去
code_block = re.search(r"```(?:json)?\s*([\s\S]*?)```", text, re.DOTALL)
if code_block:
return code_block.group(1).strip()
return text.strip()
raw = message.content[0].text
data = json.loads(extract_json_text(raw))
ただし、これはあくまで後処理での対策です。根本的にはプロンプトを改善するほうが確実です。
# プロンプトに明示的な制約を加える
content = (
"以下のデータを JSON オブジェクトのみで返してください。\n"
"コードブロック・説明・コメントは一切不要です。\n"
"出力の最初の文字は '{' でなければなりません。\n\n"
f"データ: {input_data}"
)原因2: レスポンスが途中で切れる
max_tokens が小さすぎると、JSON の途中でレスポンスが終了します:
{"items": [{"id": 1, "name": "商品A"}, {"id": 2, "name": "商品
これは明確に不完全な JSON なので json.loads() は必ず失敗します。
Fix — stop_reason を必ず確認する
message = client.messages.create(
model="claude-sonnet-4-6",
max_tokens=2048, # 出力サイズを見積もって余裕を持たせる
messages=messages
)
# ← これを毎回確認する習慣をつける
if message.stop_reason == "max_tokens":
raise ValueError(
f"レスポンスが max_tokens で打ち切られました。"
f"現在の設定: max_tokens={message.usage.output_tokens}"
)
data = json.loads(message.content[0].text)配列や深くネストされた構造を生成させる場合は特に要注意です。生成するデータ量を事前に見積もって、出力トークン数に余裕を持たせてください。
原因3: モデルが補足コメントを混入させる
プロンプトの書き方によっては、Claude が JSON に前置きや後置きのテキストを添えることがあります:
以下が JSON です:
{"name": "田中", "age": 30}
name と age を含むオブジェクトを生成しました。
前後のテキストが邪魔でパースできません。
Fix — prefill(アシスタントの先頭を固定)
私が一番信頼している方法です。アシスタントの返答の先頭を { で固定すると、物理的に前置きテキストが挿入できなくなります。
messages = [
{
"role": "user",
"content": "以下の情報を JSON オブジェクトに変換してください: 名前=田中, 年齢=30"
},
{
"role": "assistant",
"content": "{" # ← JSON の開始文字を先頭に固定
}
]
message = client.messages.create(
model="claude-sonnet-4-6",
max_tokens=2048,
messages=messages
)
# prefill した "{" を先頭に戻してからパース
raw = "{" + message.content[0].text
data = json.loads(raw)この prefill テクニックは、後処理に頼らず確実に純粋な JSON を取得したい場合に特に有効です。配列を返させたい場合は [ を prefill してください。
原因4: Unicode エスケープの二重処理
日本語を含むデータで \u306f\u3058\u3081\u307e\u3057\u3066 のような Unicode エスケープが混入しているように見えることがあります。
実は json.loads() は Unicode エスケープを正しく処理できるので、これ自体は問題ではありません:
# これは問題なく動作する
raw = r'{"message": "\u306f\u3058\u3081\u307e\u3057\u3066"}'
data = json.loads(raw)
print(data["message"]) # → はじめまして(正しく展開される)問題が起きるのは、エスケープが二重になっているときです:
# NG: \\u のように二重エスケープされている場合
raw = '{"message": "\\\\u306f\\\\u3058\\\\u3081\\\\u307e"}'
# json.loads は "\u306f" という文字列リテラルとして解釈してしまう
# repr() で確認するとすぐ分かる
print(repr(raw)) # → '{"message": "\\\\u306f\\\\u3058..."}'repr() で \\u が含まれていたら二重エスケープが原因です。文字列の構築方法(f-string の使い方、API からの取得方法)を見直してください。
原因5: ストリーミング中に逐次パースしようとする
ストリーミングで受け取ったチャンクを1つずつ json.loads() しようとすると、当然ながら途中の断片は不完全な JSON です:
# NG: ストリーミング中に毎チャンクパースするとエラー
with client.messages.stream(...) as stream:
for text_chunk in stream.text_stream:
data = json.loads(text_chunk) # ← 毎回 JSONDecodeErrorFix — 完了後にまとめてパースする
# OK: 全テキストを結合してからパース
with client.messages.stream(
model="claude-sonnet-4-6",
max_tokens=2048,
messages=messages
) as stream:
full_text = stream.get_final_text() # ← 完了まで待ってから取得
data = json.loads(extract_json_text(full_text))UI のリアルタイム更新とバックエンドの JSON 処理を両立したい場合は、表示用にはストリーミングを使いつつ、処理用には完了後のテキストを使う設計にするのがおすすめです。
コピペで使えるユーティリティ関数
これまでの対策をすべて組み込んだ汎用関数です。新しいプロジェクトではここから始めてみてください:
import re
import json
import anthropic
def extract_json_text(text: str) -> str:
"""コードブロック・前後のテキストを除去して JSON 文字列を返す"""
# コードブロックがある場合はその中身を取得
code_block = re.search(r"```(?:json)?\s*([\s\S]*?)```", text, re.DOTALL)
if code_block:
return code_block.group(1).strip()
# JSON 部分({ })を抽出
json_obj = re.search(r"(\{[\s\S]*\})", text)
if json_obj:
return json_obj.group(1).strip()
# JSON 配列([ ])を抽出
json_arr = re.search(r"(\[[\s\S]*\])", text)
if json_arr:
return json_arr.group(1).strip()
return text.strip()
def call_with_json(
prompt: str,
model: str = "claude-sonnet-4-6",
max_tokens: int = 4096,
as_array: bool = False
) -> dict | list:
"""JSON レスポンスを確実に取得するラッパー関数"""
client = anthropic.Anthropic()
prefix = "[" if as_array else "{"
messages = [
{
"role": "user",
"content": (
f"以下の指示に対して {prefix} で始まる JSON のみで回答してください。\n"
"コードブロック・説明・コメントは一切不要です。\n\n"
f"{prompt}"
)
},
{"role": "assistant", "content": prefix} # prefill
]
message = client.messages.create(
model=model,
max_tokens=max_tokens,
messages=messages
)
# 途中切れチェック
if message.stop_reason == "max_tokens":
raise ValueError(
f"レスポンスが途中で切れました(max_tokens={max_tokens})。"
"max_tokens を増やして再試行してください。"
)
raw = prefix + message.content[0].text
return json.loads(raw)
# 使用例
result = call_with_json(
"名前: 佐藤, 年齢: 28, 職業: デザイナー をオブジェクトに変換して"
)
print(result)
# → {"name": "佐藤", "age": 28, "occupation": "デザイナー"}
items = call_with_json(
"Python, JavaScript, Go の各言語の特徴を配列で返して。各要素は {lang, strength} を持つこと",
as_array=True
)
print(items[0])
# → {"lang": "Python", "strength": "データ処理・機械学習"}パースエラーに悩んでいる方は、まず repr() でレスポンスを確認して原因を特定する、というシンプルな一歩から始めてみてください。原因さえ分かれば、対策は上記のどれかで必ず解決できます。
Claude API の構造化出力についてより体系的に学びたい方には、Claude API 構造化出力 完全マスターガイドが参考になります。API エラーハンドリング全般については Claude API エラー完全ガイド もあわせてご覧ください。