Skip to content

Fix: Python json.decoder.JSONDecodeError: Expecting value

FixDevs ·

Quick Answer

How to fix Python JSONDecodeError Expecting value caused by empty responses, HTML error pages, invalid JSON, BOM characters, and API errors.

The Error

You parse JSON in Python and get:

json.decoder.JSONDecodeError: Expecting value: line 1 column 1 (char 0)

Or variations:

json.decoder.JSONDecodeError: Expecting property name enclosed in double quotes: line 1 column 2 (char 1)
json.decoder.JSONDecodeError: Extra data: line 2 column 1 (char 100)
json.decoder.JSONDecodeError: Unterminated string starting at: line 5 column 12 (char 89)
json.decoder.JSONDecodeError: Expecting ',' delimiter: line 3 column 15 (char 45)

Python’s json.loads() or json.load() cannot parse the input as valid JSON. The input is either empty, not JSON at all, or contains syntax errors.

Why This Happens

json.loads() expects a valid JSON string. When it encounters something that is not valid JSON, it raises JSONDecodeError with a message indicating where the parsing failed.

Common causes:

  • Empty string or response. json.loads("") fails immediately with “Expecting value.”
  • HTML error page instead of JSON. The server returned an HTML 404 or 500 page.
  • Single quotes instead of double quotes. JSON requires double quotes: {"key": "value"}, not {'key': 'value'}.
  • Trailing commas. {"a": 1, "b": 2,} is invalid JSON.
  • Comments in JSON. JSON does not support comments (// or /* */).
  • BOM (Byte Order Mark). A UTF-8 BOM at the start of the file.
  • Multiple JSON objects. Multiple JSON objects concatenated without being in an array.
  • Unquoted keys. {key: "value"} is not valid JSON.

Fix 1: Check the Response Before Parsing

The most common cause is parsing an empty or non-JSON response:

Broken:

import requests
import json

response = requests.get("https://api.example.com/data")
data = response.json()  # JSONDecodeError if response body is empty or not JSON!

Fixed — check status code and content first:

response = requests.get("https://api.example.com/data")

# Check if the request succeeded
if response.status_code != 200:
    print(f"Error: HTTP {response.status_code}")
    print(f"Response body: {response.text[:500]}")  # Print first 500 chars for debugging
    raise Exception(f"API returned {response.status_code}")

# Check content type
content_type = response.headers.get("Content-Type", "")
if "application/json" not in content_type:
    print(f"Expected JSON but got: {content_type}")
    print(f"Response: {response.text[:500]}")
    raise Exception(f"Non-JSON response: {content_type}")

# Check if body is not empty
if not response.text.strip():
    print("Empty response body")
    raise Exception("Empty response")

data = response.json()

Simplified with try-except:

try:
    data = response.json()
except json.JSONDecodeError as e:
    print(f"Failed to parse JSON: {e}")
    print(f"Response status: {response.status_code}")
    print(f"Response body: {response.text[:500]}")
    raise

Pro Tip: Always check response.status_code before calling .json(). A 500 error page is usually HTML, not JSON. Print response.text[:500] in your error handler to see what the server actually returned — it is almost always immediately obvious (HTML page, empty string, plain text error).

Fix 2: Fix the JSON Syntax

Common JSON syntax errors:

Single quotes (not valid JSON):

# Broken — single quotes
bad = "{'name': 'Alice', 'age': 30}"
json.loads(bad)  # JSONDecodeError!

# Fixed — double quotes
good = '{"name": "Alice", "age": 30}'
json.loads(good)

# If you have Python dict-like strings, use ast.literal_eval instead
import ast
data = ast.literal_eval("{'name': 'Alice', 'age': 30}")

Trailing commas:

# Broken — trailing comma after last element
bad = '{"a": 1, "b": 2,}'
json.loads(bad)  # JSONDecodeError!

# Fixed — remove trailing comma
good = '{"a": 1, "b": 2}'

Comments:

# Broken — JSON doesn't support comments
bad = '''
{
  // This is a comment
  "name": "Alice",
  /* This too */
  "age": 30
}
'''

# Fixed — remove comments before parsing
import re
cleaned = re.sub(r'//.*?$|/\*.*?\*/', '', bad, flags=re.MULTILINE | re.DOTALL)
json.loads(cleaned)

# Or use a library that supports JSON with comments
# pip install json5
import json5
data = json5.loads(bad)

Unquoted keys:

# Broken — keys must be quoted in JSON
bad = '{name: "Alice"}'

# Fixed
good = '{"name": "Alice"}'

Common Mistake: Assuming Python dict syntax and JSON are the same. They are not. JSON requires double quotes for strings, no trailing commas, no comments, no single quotes, and no Python literals like True/False/None (JSON uses true/false/null).

Fix 3: Fix BOM and Encoding Issues

A UTF-8 BOM (Byte Order Mark) at the start of a file causes “Expecting value”:

# The file starts with \xef\xbb\xbf (UTF-8 BOM)
with open("data.json", "r") as f:
    data = json.load(f)  # JSONDecodeError!

Fixed — use utf-8-sig encoding:

with open("data.json", "r", encoding="utf-8-sig") as f:
    data = json.load(f)  # utf-8-sig strips the BOM automatically

Fixed — strip BOM manually:

with open("data.json", "rb") as f:
    content = f.read()
    if content.startswith(b'\xef\xbb\xbf'):
        content = content[3:]  # Remove BOM
    data = json.loads(content.decode("utf-8"))

Check for encoding issues:

with open("data.json", "rb") as f:
    raw = f.read(10)
    print(raw)  # See the actual bytes
    # b'\xef\xbb\xbf{...' means BOM is present

Fix 4: Fix File Reading Issues

Reading a file incorrectly:

Broken — file path wrong or file is empty:

with open("config.json", "r") as f:
    data = json.load(f)  # JSONDecodeError if file is empty!

Fixed — check file contents:

import os
import json

filepath = "config.json"

if not os.path.exists(filepath):
    raise FileNotFoundError(f"{filepath} does not exist")

if os.path.getsize(filepath) == 0:
    raise ValueError(f"{filepath} is empty")

with open(filepath, "r", encoding="utf-8") as f:
    data = json.load(f)

Broken — reading the file twice (cursor at end):

with open("data.json", "r") as f:
    print(f.read())     # Read the entire file
    data = json.load(f)  # JSONDecodeError! File cursor is at the end

# Fixed — seek back to start
with open("data.json", "r") as f:
    print(f.read())
    f.seek(0)           # Reset cursor to beginning
    data = json.load(f)

Fix 5: Fix Multiple JSON Objects (JSONL / NDJSON)

A file with one JSON object per line is not valid JSON as a whole:

{"id": 1, "name": "Alice"}
{"id": 2, "name": "Bob"}
{"id": 3, "name": "Charlie"}

Broken:

with open("data.jsonl", "r") as f:
    data = json.load(f)  # JSONDecodeError: Extra data

Fixed — parse line by line:

records = []
with open("data.jsonl", "r") as f:
    for line in f:
        line = line.strip()
        if line:
            records.append(json.loads(line))

Fixed — use a list comprehension:

with open("data.jsonl", "r") as f:
    records = [json.loads(line) for line in f if line.strip()]

Fix 6: Fix API-Specific Issues

Empty responses on 204 No Content:

response = requests.delete(f"/api/items/{item_id}")

if response.status_code == 204:
    # 204 means success with no body — don't parse JSON
    return None

data = response.json()

Paginated APIs returning empty on last page:

def fetch_all_pages(base_url):
    all_items = []
    page = 1
    while True:
        response = requests.get(f"{base_url}?page={page}")
        if response.status_code != 200:
            break
        try:
            data = response.json()
        except json.JSONDecodeError:
            break
        if not data.get("items"):
            break
        all_items.extend(data["items"])
        page += 1
    return all_items

Rate-limited APIs returning HTML:

response = requests.get("https://api.example.com/data")
if response.status_code == 429:
    retry_after = int(response.headers.get("Retry-After", 60))
    time.sleep(retry_after)
    return fetch_data()  # Retry

Fix 7: Fix Python Literals vs JSON

Python literals and JSON look similar but are different:

# Python dict (not JSON!)
python_str = "{'key': True, 'value': None}"
json.loads(python_str)  # Fails! True/None are not valid JSON

# Convert Python literals to JSON
import ast
python_obj = ast.literal_eval(python_str)
json_str = json.dumps(python_obj)
# Now json_str is: '{"key": true, "value": null}'

Boolean and null differences:

PythonJSON
Truetrue
Falsefalse
Nonenull
'single'"double" only

Fix 8: Use a Robust JSON Parser

For JSON that is slightly malformed:

# pip install json5
import json5

# Handles comments, trailing commas, single quotes, unquoted keys
data = json5.loads("""
{
  // Configuration file
  name: 'My App',
  debug: true,
  ports: [8080, 8443,],  // trailing comma OK
}
""")

For very large JSON files, use streaming parsers:

# pip install ijson
import ijson

with open("huge.json", "rb") as f:
    for item in ijson.items(f, "items.item"):
        process(item)

Still Not Working?

Print the raw content to see what you are actually parsing:

print(repr(content))  # repr shows invisible characters like \r, \n, \xef
print(len(content))   # Check if it's empty
print(content[:100])  # Print first 100 characters

Check for compressed responses. Some APIs return gzip-compressed data:

response = requests.get(url, headers={"Accept-Encoding": "gzip"})
# requests automatically decompresses, but manual HTTP calls might not

Check for JSONP responses. Some older APIs wrap JSON in a callback function:

# JSONP: callback({"data": "value"})
text = response.text
if text.startswith("callback("):
    text = text[len("callback("):-1]  # Strip the wrapper
data = json.loads(text)

For Python type errors with None values, see Fix: Python TypeError: ‘NoneType’ object is not subscriptable. For file not found errors, see Fix: Python FileNotFoundError. For requests connection errors, see Fix: Python requests ConnectionError: Max retries exceeded.

F

FixDevs

Solo developer based in Japan. Every solution is cross-referenced with official documentation and tested before publishing.

Was this article helpful?

Related Articles