Skip to content
datahacks.dev
JSON

Python JSONDecodeError Expecting Value Line 1 Column 1

Why json.loads or response.json() raises Expecting value at line 1 column 1 in Python, how to see what the text really is, and a checklist for each cause.

7 min read
Summarize in AI
Flat illustration of a tree of nested objects beside the document it came from, with the Python logo.

The traceback ends with json.decoder.JSONDecodeError: Expecting value: line 1 column 1 (char 0) and the code was perfectly sensible a minute ago. This is the most common JSON error in Python, and it nearly never means that your JSON is malformed. It means the parser was given something that is not JSON at all: nothing, a web page, a prefix, a Python object that was converted to a string. This article is a short procedure for finding which, with the specific messages each cause produces, the three habits that prevent the error from being confusing, and the cases where the parser is not at fault.

What the message says

Expecting value is raised when the parser needs the start of a value (an object, array, string, number, true, false or null) and finds something else. line 1 column 1 (char 0) is the position where that happened: the first character of the text. So the document does not begin with anything JSON can begin with.

The first step is to look at the text itself, not the traceback. Put this just above the call that fails:

Python
print(repr(text[:200]), len(text))

repr matters. It shows an empty string as '', a newline as \n, a byte order mark as , and keeps invisible characters visible. Whatever it prints, you now know what the parser saw, and the rest of this article is a lookup by that output.

Causes, from most to least common

An empty string. json.loads("") fails with Expecting value: line 1 column 1 (char 0), and so does a string of whitespace (for " \n" the position moved to line 2 column 1, char 3, the end of the text). The repr is '' with a length of 0. This is what you get from an empty file, an API call that returned no body, an environment variable that was not set (os.environ.get("CONFIG", "")) or a message queue payload that was blank.

HTML where JSON was expected. The repr starts '<!doctype html>...' or '<html>'. Python reports the same position as for an empty string, because < cannot start a value. A server returned an error page, a login page, a captive portal, a proxy's block page or a 404 from the wrong URL. The payload is human-readable text that tells you what happened, so read it.

A file that was already read. Passing a file object to json.load after something else consumed it fails the same way, because the read starts at the end. In my test, f.read() followed by json.load(f) raised Expecting value: line 1 column 1 (char 0) even though the file was fine. The fix is f.seek(0) or to read once and keep the text. A common version of this bug is printing the file for debugging with print(f.read()) and then loading it.

The wrong text, converted to a string. json.loads(str(obj)) on a dict gives single quotes (a different message, at char 1; see JSON parse error with single quotes instead of double quotes). json.loads(str(None)) gives Expecting value because the text is None, not null. json.loads("undefined") does the same. The repr starts with a Python or JavaScript word.

A prefix in front of the document. Some APIs prepend )]}', to guard against script inclusion, a log line, or callback( in JSONP. The repr shows the prefix. If a )]}' prefix is documented, strip exactly that and then parse; for JSONP, strip the function wrapper. Expecting value at char 0 in that case is expected, because the first character really is not a JSON start.

A value that Python does not accept. {"a": undefined} and {"a": +1} fail at the value, not at char 0, with Expecting value: line 1 column 7 (char 6). Position 0 stays reserved for text that is wrong from the first character.

The same problem with requests

Most readers meet this error through a web call:

Python
import requests
r = requests.get("https://example.test/api/items")
data = r.json()

r.json() parses the response body, and in the version of requests I used (2.34.2) its requests.exceptions.JSONDecodeError subclasses the standard one and prints the same message. Against a local test server, a 200 page of HTML and a 204 with no body both failed with Expecting value: line 1 column 1 (char 0). When it fails, the response is the evidence:

Python
print(r.status_code, r.headers.get("Content-Type"), len(r.content))
print(repr(r.text[:200]))

Three results explain most failures. A status of 204 with a length of 0 means the server answered correctly and sent no body, so there is nothing to parse. A status of 404, 500 or 503 with text/html means you are reading an error page, so the real fault is the request. A **status of 200 with text/html** and a page full of forms means a redirect to a login or consent page, often because the cookie or token expired, or the URL points to the website and not to the API.

Handle those cases before parsing. A small wrapper that names the problem saves the next person an hour:

fetch_json.pyPython
import requests

def fetch_json(url, **kw):
    r = requests.get(url, timeout=10, **kw)
    ctype = r.headers.get("Content-Type", "")
    if r.status_code == 204 or not r.content:
        return None
    if "json" not in ctype:
        raise ValueError(f"{url} returned {r.status_code} {ctype!r}: {r.text[:120]!r}")
    r.raise_for_status()
    return r.json()

The check on Content-Type is a heuristic. A badly configured server can send JSON as text/plain, and then the wrapper rejects a good response, but the error message includes the first 120 characters, so the situation is plain. I prefer a loud mismatch to a silent guess. The same pattern for error responses from APIs is in reading JSON from an API with requests and handling errors.

When the position is not zero

If the message says line 1 column 1 the text is not JSON from the start. When it names a later position, the cause is something else, and the neighboring articles apply:

The same condition in other languages reads differently: Node says Unexpected token '<', "<html>" is not valid JSON for the HTML case and Unexpected end of JSON input for an empty string, which is described in unexpected token in JSON at position 0 and unexpected end of JSON input.

Three habits that make the error short

  1. Log the evidence at the boundary. The code that receives external text should log status, content type and the first 200 characters when parsing fails. The traceback alone cannot tell you any of that.

  2. Parse once, from a variable. Reading a response or a file into a variable, then parsing the variable, avoids the consumed-handle bug and gives you something to print.

  3. Fail with context. raise ValueError(f"bad JSON from {source}: {text[:100]!r}") from e replaces a library message with one that tells the on-call engineer what to look at.

When the JSON is fine and the call is wrong

Sometimes the parser is right and the real defect is upstream, and no change to the JSON handling helps.

The service is returning errors. A wrapper that retries on JSONDecodeError treats an outage as bad data. Check the status first and retry on the status, not on the parse failure.

The request is for the wrong thing. A URL that points at a documentation page returns HTML with a 200. If the status is fine and the content is a page, the URL or the Accept header is wrong. Many APIs choose a representation from Accept: application/json, and the default of requests does not set it.

The data is not JSON on purpose. A CSV endpoint, a stream of JSON Lines or an XML feed is not parseable by json.loads, and a .json name does not change that. A file that contains one object per line fails with Extra data on line 2; use a line-by-line reader, as in reading and writing JSON files in Python.

The text is truncated. If the response was cut off, the failure is not at char 0, but a proxy that returns an empty 200 on timeout produces Expecting value at char 0 from a call that "worked". Check Content-Length against len(r.content) when the empty responses are intermittent.

For the exception class and its attributes, see the Python json module reference, and for what a media type of application/json promises, see RFC 8259.

Share

Keep reading

Get new articles by email

We send one email when a new article is published. No spam.

Unsubscribe in one click. No spam, ever.