json.decoder.JSONDecodeError: Expecting value: line 1 column 1 (char 0)
The string you parsed is not JSON at all — often an LLM reply wrapped in markdown fences, or an error page where JSON was expected. Log the raw string, then use structured output instead of hoping.
Updated
The error
json.decoder.JSONDecodeError: Expecting value: line 1 column 1 (char 0)
What it means
json.loads looked at position 0 of the string and did not find the beginning of any JSON value. Line 1, column 1 means the very first character is already wrong — the string starts with something other than {, [, a quote, a number or a literal. In practice that means the string is markdown, prose, HTML, or empty.
Why it happens
For AI developers there are two big sources.
LLM output. You asked the model to "respond with JSON", and it responded with almost-JSON: wrapped in ``json ` fences, prefixed with "Here is the JSON you requested:", or truncated mid-object by a token limit. Models follow instructions probabilistically; parsers do not.
HTTP responses. You called .json() on a response that was actually an HTML error page, an empty body, or a plain-text proxy message. The status code was never checked, so the failure surfaced at the parser instead of the request.
How to fix it
1. Look at the actual string before anything else.
print(repr(raw[:200]))'```json\n{"city": "Pune", "score": 8}\n```'repr exposes fences, whitespace and emptiness instantly. Diagnosis done.
2. For LLM output, stop parsing prose — use the structured output features. OpenAI's JSON schema mode and Anthropic's tool calling make the model return machine-validated JSON:
resp = client.messages.create(
model=MODEL, max_tokens=1024,
tools=[{"name": "record_score", "input_schema": schema}],
tool_choice={"type": "tool", "name": "record_score"},
messages=messages,
)
data = next(b.input for b in resp.content if b.type == "tool_use")The provider enforces the shape; the fence problem disappears by construction.
3. If you must parse free text, strip fences defensively and validate.
import json, re
def parse_llm_json(raw: str) -> dict:
m = re.search(r"\{.*\}", raw, re.DOTALL)
if not m:
raise ValueError(f"no JSON object found in: {raw[:200]!r}")
return json.loads(m.group())Pair it with a Pydantic model so wrong-but-parseable output also fails loudly, and retry the LLM call once on failure.
4. Check for truncation. JSON cut off mid-object means the reply hit the output-token cap — raise max_tokens and check the stop reason rather than patching the string.
5. For HTTP responses, gate on status and content type first.
r = requests.get(url, timeout=30)
r.raise_for_status()
data = r.json()raise_for_status() converts the hidden 502-with-HTML-body into a visible HTTP error at the right layer.
How to prevent it
Never feed a parser something you have not logged. For every LLM-to-JSON boundary in a real system, choose schema-enforced output over prompt-and-pray; keep the regex fallback only for models and modes without structured support.
Related errors
- Invalid schema for function — the setup for schema-enforced output
- tool_use ids were found without tool_result blocks
- This model's maximum context length is N tokens — one cause of truncated JSON
- RateLimitError 429 — error bodies that are not the JSON you expected