CLAUDE LABJP
FORK — Claude Code 2.1.212 changes what /fork does: it copies your conversation into a new background session with its own row in claude agents, so you can keep working. The old in-session subagent is now /subtaskLIMITS — WebSearch calls are now capped at 200 per session by default, and subagent spawns get the same 200 ceiling, so a runaway search or delegation loop stops on its ownMCPBG — MCP tool calls running past two minutes now move to the background automatically, keeping the session usable. Tune the threshold with CLAUDE_CODE_MCP_AUTO_BACKGROUND_MSPLANFIX — Fixed plan mode auto-running file-modifying Bash commands such as touch and rm without a permission prompt or an SDK canUseTool callbackSONNET5 — Claude Sonnet 5 is running on introductory pricing of $2 per million input tokens and $10 per million output. After August 31 it moves to $3 and $15IPO — Bankers are reportedly lining up investor meetings for Anthropic ahead of a possible public listing as soon as OctoberFORK — Claude Code 2.1.212 changes what /fork does: it copies your conversation into a new background session with its own row in claude agents, so you can keep working. The old in-session subagent is now /subtaskLIMITS — WebSearch calls are now capped at 200 per session by default, and subagent spawns get the same 200 ceiling, so a runaway search or delegation loop stops on its ownMCPBG — MCP tool calls running past two minutes now move to the background automatically, keeping the session usable. Tune the threshold with CLAUDE_CODE_MCP_AUTO_BACKGROUND_MSPLANFIX — Fixed plan mode auto-running file-modifying Bash commands such as touch and rm without a permission prompt or an SDK canUseTool callbackSONNET5 — Claude Sonnet 5 is running on introductory pricing of $2 per million input tokens and $10 per million output. After August 31 it moves to $3 and $15IPO — Bankers are reportedly lining up investor meetings for Anthropic ahead of a possible public listing as soon as October
Articles/API & SDK
API & SDK/2026-04-16Intermediate

Claude API JSON Output Fails: 5 Root Causes and Fixes

Fix Claude API JSON parsing errors with these 5 common root causes: markdown code block wrapping, truncated output, injected commentary, Unicode escaping, and streaming parse failures. Includes copy-paste ready Python utility code.

api38json3troubleshooting87python22structured-output5

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, everything

repr() 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 issue

If 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 works

Fix — 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 response

Copy-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.

Share

Thank You for Reading

Claude Lab is ad-free, supported entirely by members like you. We publish practical guides daily with implementation code, benchmarks, and production-ready patterns. If you've found it useful, we'd love to have you on board.

  • Copy-paste ready implementation code
  • New advanced guides published daily
  • $5/mo or $10 for lifetime access
View Membership →

If you found this article helpful, a small tip ($1.50) would mean a lot to us. Your support helps keep this site ad-free and covers server and hosting costs.

Related Articles

API & SDK2026-05-16
Debugging Claude API Tool Use Schema Errors: 3 Patterns I've Hit and How to Fix Them
A practical guide to diagnosing Claude API Tool Use errors—from schema definition mistakes to invalid_tool_use blocks and Claude ignoring your tools entirely. Based on real implementation experience.
API & SDK2026-04-26
Claude API Streaming Stops Mid-Response: Diagnosing and Fixing the 5 Root Causes
When Claude API streaming stops unexpectedly, there are exactly 5 root causes. Learn to diagnose which one you're hitting and apply the right fix — from timeout tuning to stop_reason logging.
API & SDK2026-06-27
When Claude API Streaming Stops Without an Error: Detecting Silent Stalls and Resuming Mid-Stream
How to catch the 'silent stall' where Claude API streaming stops with no exception at all, using a content-level watchdog that times the gap between tokens, plus a resume path that carries received text forward as an assistant prefill, and a four-layer timeout budget for long-running automation.
📚RECOMMENDED BOOKS
Build a Large Language Model (From Scratch)
Sebastian Raschka
LLM Dev
Prompt Engineering for LLMs
Berryman & Ziegler
Prompting
AI Engineering
Chip Huyen
AI Eng
* Contains affiliate links
See all →