You've written the code carefully, tested it locally, and then at 11 PM before launch day you get JSONDecodeError: Expecting value: line 1 column 1. The model is returning something — you can see it in the logs — but json.loads() refuses to parse it.
This is more common than you'd think. Claude API JSON failures usually aren't bugs in your code; they're unexpected variations in how the model formats its output. Here are the five patterns I've run into repeatedly, along with fixes you can copy directly into your project.
Start with repr() — Always
Before debugging anything, print the raw response with repr():
import anthropic
client = anthropic.Anthropic()
message = client.messages.create(
model="claude-sonnet-4-6",
max_tokens=1024,
messages=[
{"role": "user", "content": "Return a JSON object with name and age fields"}
]
)
raw = message.content[0].text
print(repr(raw)) # Shows escape characters, newlines, everythingrepr() makes invisible characters visible — control characters, double-escaped backslashes, leading/trailing whitespace. Once you see the actual string, identifying which pattern you're dealing with takes seconds.
Root Cause 1: Markdown Code Block Wrapping
The most common issue. Ask for JSON and Claude often returns this:
```json
{"name": "Alice", "age": 30}
This is Claude being helpful for human readers — but it breaks machine parsing.
**Fix — strip it with regex:**
```python
import re
import json
def extract_json_text(text: str) -> str:
"""Remove code block wrapping and extract raw JSON string."""
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))
Better fix — improve the prompt:
content = (
"Return the following data as a JSON object only.\n"
"No code blocks, no explanation, no commentary.\n"
"The output must begin with '{' and end with '}'.\n\n"
f"Data: {input_data}"
)Fixing the prompt is more reliable than post-processing. Regex cleanup should be a fallback, not the primary strategy.
Root Cause 2: Truncated Output
When max_tokens is too small, responses get cut mid-JSON:
{"items": [{"id": 1, "name": "Widget A"}, {"id": 2, "name": "Widg
json.loads() will always fail on incomplete JSON. The fix is straightforward, but the key is actually detecting the truncation:
message = client.messages.create(
model="claude-sonnet-4-6",
max_tokens=2048, # Give yourself room
messages=messages
)
# Always check this — don't assume the response is complete
if message.stop_reason == "max_tokens":
raise ValueError(
f"Response was truncated at max_tokens={max_tokens}. "
f"Output used {message.usage.output_tokens} tokens. "
"Increase max_tokens and retry."
)
data = json.loads(message.content[0].text)For deeply nested structures or large arrays, estimate your output size before setting max_tokens. Err on the generous side — unused token budget doesn't cost you anything.
Root Cause 3: Commentary Injected Around the JSON
Depending on the prompt, Claude sometimes adds context before or after the JSON:
Here is the requested JSON object:
{"name": "Alice", "age": 30}
I've included both the name and age fields as requested.
Fix — prefill the assistant turn:
This is the most reliable technique I've found. Prefilling locks the response to start with {, which physically prevents any leading text from being inserted:
messages = [
{
"role": "user",
"content": "Convert this to a JSON object: name=Alice, age=30"
},
{
"role": "assistant",
"content": "{" # Forces response to begin here
}
]
message = client.messages.create(
model="claude-sonnet-4-6",
max_tokens=2048,
messages=messages
)
# Prepend the prefilled "{" before parsing
raw = "{" + message.content[0].text
data = json.loads(raw)For arrays, prefill with [ instead. This approach is cleaner than regex post-processing and works consistently across models.
Root Cause 4: Unicode Escape Issues
When processing Japanese or other non-ASCII text, you might see \u306f\u3058\u3081\u307e\u3057\u3066 in the response and assume something is broken. It's usually not:
# json.loads handles Unicode escapes correctly
raw = r'{"message": "\u306f\u3058\u3081\u307e\u3057\u3066"}'
data = json.loads(raw)
print(data["message"]) # → はじめまして (correctly decoded)The real problem is double-escaping — when \u appears as \\u in your raw string:
# Problem: double-escaped Unicode
raw = '{"message": "\\\\u306f\\\\u3058"}'
# json.loads treats this as the literal string "\u306f\u3058" — not decoded
# Diagnose with repr()
print(repr(raw)) # You'll see \\\\u if double-escaping is the issueIf repr() shows \\u, the problem is in how you're constructing or retrieving the string — look for places where you're doing json.dumps() on a string that's already JSON-encoded, or where you're building the string via f-string concatenation with escaped values.
Root Cause 5: Parsing Streaming Chunks Individually
Streaming chunks are incomplete JSON by definition. Trying to parse each chunk is always going to fail:
# This will throw JSONDecodeError on every chunk
with client.messages.stream(...) as stream:
for text_chunk in stream.text_stream:
data = json.loads(text_chunk) # Never worksFix — wait for the full response before parsing:
with client.messages.stream(
model="claude-sonnet-4-6",
max_tokens=2048,
messages=messages
) as stream:
full_text = stream.get_final_text() # Blocks until complete
data = json.loads(extract_json_text(full_text))If you need real-time UI updates and JSON processing, stream for display but process after completion:
chunks = []
with client.messages.stream(...) as stream:
for chunk in stream.text_stream:
chunks.append(chunk)
update_ui(chunk) # Real-time display
full_text = "".join(chunks)
data = json.loads(extract_json_text(full_text)) # Process complete responseCopy-Paste Utility: Reliable JSON from Claude API
Here's a single utility function that handles all five patterns:
import re
import json
import anthropic
def extract_json_text(text: str) -> str:
"""Strip code blocks and surrounding text, return clean JSON string."""
code_block = re.search(r"```(?:json)?\s*([\s\S]*?)```", text, re.DOTALL)
if code_block:
return code_block.group(1).strip()
json_obj = re.search(r"(\{[\s\S]*\})", text)
if json_obj:
return json_obj.group(1).strip()
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:
"""
Call Claude API and return parsed JSON.
Uses prefill to prevent code block wrapping and commentary injection.
"""
client = anthropic.Anthropic()
prefix = "[" if as_array else "{"
messages = [
{
"role": "user",
"content": (
f"Return the following as {prefix} — pure JSON only.\n"
"No code blocks, no explanation.\n\n"
f"{prompt}"
)
},
{"role": "assistant", "content": prefix}
]
message = client.messages.create(
model=model,
max_tokens=max_tokens,
messages=messages
)
if message.stop_reason == "max_tokens":
raise ValueError(
f"Response truncated (max_tokens={max_tokens}, "
f"output={message.usage.output_tokens} tokens). "
"Increase max_tokens."
)
raw = prefix + message.content[0].text
return json.loads(raw)
# Examples
user_data = call_with_json(
"Convert: name=Alice, age=28, role=engineer"
)
print(user_data)
# → {"name": "Alice", "age": 28, "role": "engineer"}
languages = call_with_json(
"List Python, JavaScript, Go with a one-word strength each. "
"Each item: {lang, strength}",
as_array=True
)
print(languages[0])
# → {"lang": "Python", "strength": "readability"}When you hit a JSON parsing error, repr() the response first. The root cause becomes obvious immediately, and every pattern here has a reliable fix. Most issues come down to output formatting (code blocks, commentary) or size constraints (truncation) — both are straightforward to address once identified.
For a deeper dive into structured output patterns with Claude API, see Claude API Structured Output Practical Mastery. For general API error handling, Claude API Error Complete Guide covers the full range of error types and recovery strategies.