/개발/JSONDecodeError Expecting value: line 1 column 1 (char 0) — Causes and Fixes
DevelopmentJSONDecodeError파이썬 에러 해결

JSONDecodeError Expecting value: line 1 column 1 (char 0) — Causes and Fixes

Diagnose the five causes of json.decoder.JSONDecodeError: Expecting value: line 1 column 1 (char 0) with a table, then fix them with copy-paste code. A Python guide covering empty requests responses, HTML bodies, BOM, and double parsing.

JSONDecodeError Expecting value: line 1 column 1 (char 0) — Causes and Fixes

JSONDecodeError Expecting value: line 1 column 1 (char 0) — A Fix Guide

You called a JSON API, and the console prints this:

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

"I got JSON back — so why does it blow up at the first character (char 0)?" This error is a five-minute fix. Bottom line: that message almost always means the parser did not actually receive JSON. Empty string, HTML error page, or an object that was already parsed. This is part 4 of the Python Development Guide series: diagnosis table first, then copy-paste code, straight through to a working fix.

5-second self-diagnosis table

Pin down your situation in under a minute. The key is printing the actual value right before you parse.

SymptomLikely causeOne-line checkFix
response.text is ''Empty body (204, timeout, rate limit)print(repr(r.text))''① Empty-string guard
Body starts with <!DOCTYPE html>HTML / error page (404/500/login)r.text[:50] contains <html② raise_for_status + content-type
Leading \ufeffEncoding with BOMr.text[:1] == '\ufeff'③ Strip BOM
Already a dict, parsing againDouble encoding / calling loads twiceCheck type(data)④ Double-parse trap
Content-Type: text/htmlWrong content-typer.headers['Content-Type']② Check content-type

The #1 diagnostic line: print(response.status_code, repr(response.text[:200])) That one line tells you which of the five cases you're in. Use repr so empty strings, whitespace, and BOM actually show up.

Repro code and copy-paste fixes by cause

① Empty response — empty-string guard

This is the most common one. The server returns 204 No Content, or the body is empty because of a 429 (rate limit) or a timeout.

Python
# ❌ Bad
import requests
r = requests.get("https://api.example.com/data")
data = r.json()   # blows up at char 0 if the body is ''
Python
# ✅ Fixed
r = requests.get("https://api.example.com/data", timeout=10)
print(r.status_code, repr(r.text[:200]))  # inspect first

if not r.text.strip():           # empty-string / whitespace guard
    data = {}                    # safe default
else:
    data = r.json()

② HTML / error page / wrong content-type

A 404 returns a pretty error HTML page, or an expired session sends you a login page. The body starts with <!DOCTYPE html>, so the JSON parser dies on the first character <.

Python
# ✅ Validate status code and content-type first
r = requests.get(url, timeout=10)
r.raise_for_status()             # raises on 4xx/5xx

ctype = r.headers.get("Content-Type", "")
if "application/json" not in ctype:
    raise ValueError(f"Not JSON: {ctype} / {r.text[:200]!r}")

data = r.json()

③ BOM in the payload

Some public-sector and finance OpenAPIs send a UTF-8 BOM (\ufeff). Decode with utf-8-sig instead of the default json path.

Python
# ✅ requests usually handles this, but if you're dealing with raw bytes
import json
raw = r.content                  # bytes
data = json.loads(raw.decode("utf-8-sig"))

If the encoding itself is the issue (UnicodeDecodeError, UTF-8 decoding), see part 3 of this series. Here we only cover the BOM one-liner.

④ The double-parse trap

Parsing an already-parsed object from r.json() or json.loads() will blow up. Passing a dict to loads raises TypeError, but passing an already-parsed string again — or None — produces the same JSONDecodeError.

Python
# ❌ Parsing twice
data = r.json()              # already a dict
data = json.loads(data)      # parse again → error

# ✅ Once is enough
data = r.json()              # done. get in the habit of checking type(data)

json.loads vs json.load

Mix these two up and you get the same error. Memorize this:

FunctionInputUse
json.loads(s)str / bytesParse API response text
json.load(f)file objectParse from an open file
Python
# ❌ Common mistake: passing a file path string to loads
json.loads("data.json")          # parses the path as JSON → char 0 error

# ✅ Files go through load
with open("data.json", encoding="utf-8") as f:
    data = json.load(f)

Practical requests debugging procedure

When a 404 returns HTML or an auth redirect dumps you on a login page, trace it in this order:

  1. print(r.status_code) — if it's not 200, the body is almost certainly an error page.
  2. print(r.url) — check whether a redirect bounced you to a login URL.
  3. print(r.headers.get("Content-Type"))text/html means it is definitely not JSON.
  4. print(repr(r.text[:300])) — look for clues like <title>Login</title>.
  5. If the token expired, refresh the header; if you hit a rate limit (429), back off and retry.

A note from production: This question spiked once people started wiring up LLMs, OpenAI, and Korean AI APIs. The three greatest hits: a 429 arriving as HTML instead of JSON, calling json() on an entire streaming response, and an expired-token redirect that returns login HTML. I always log status_code + text[:200] on every external API call — that one habit cut my debugging time in half.

Wrap-up: a safe try/except pattern

Last step: stop it from coming back. As of 2026, requests.exceptions.JSONDecodeError is the standard in requests 2.x (it subclasses the stdlib json.JSONDecodeError). Catching both is the safe play.

Python
import logging
import requests
from json import JSONDecodeError

def safe_get_json(url, **kwargs):
    r = requests.get(url, timeout=10, **kwargs)
    try:
        r.raise_for_status()
        if not r.text.strip():
            logging.warning("Empty response: %s", url)
            return None
        return r.json()
    except (JSONDecodeError, requests.exceptions.JSONDecodeError):
        # keep a snippet of the raw body so you can diagnose later
        logging.error(
            "JSON parse failed | status=%s ctype=%s body=%r",
            r.status_code,
            r.headers.get("Content-Type"),
            r.text[:200],
        )
        return None

Final checklist

  • Right before parsing, print(status_code, repr(text[:200])) and inspect the real value
  • Filter 4xx/5xx with raise_for_status()
  • Empty-string guard: if not text.strip()
  • Confirm content-type is application/json
  • Don't loads an already-parsed object
  • try/except with a fallback and raw-body logging

FAQ

Q. status_code is 200 — why am I still getting JSONDecodeError? A. A 200 can still have an empty body or HTML. 200 does not mean JSON. Always inspect the actual body with repr(r.text[:200]).

Q. Should I catch requests.exceptions.JSONDecodeError or json.JSONDecodeError? A. In requests 2.x the former subclasses the latter, so catching both is safe. If you only deal with requests responses, requests.exceptions.JSONDecodeError alone is enough.

Q. I get the same error when reading a file. A. You probably passed a file path string to json.loads(). Open the file and use json.load(f).


Next up: debugging KeyError and TypeError: 'NoneType' object is not subscriptable after a successful parse.

확인 정보
✦ ✦ ✦
편집 검토 · Editorial Review

Nodelog는 모든 콘텐츠의 내용과 출처를 공개 전에 검토합니다. 환경(OS·버전)에 따라 결과가 달라질 수 있는 기술 정보는 공식 문서와 함께 확인하며, 검토 기준과 정정 원칙은 편집 정책에서 안내합니다. 오류를 발견하시면 이메일로 제보해 주세요 — 확인 후 신속히 정정합니다.

편집 책임 · Nodelog 기술 편집팀·발행 · ·업데이트 ·
관련 공식 문서Python 공식 문서

Comments

Be the first to comment.