Sửa lỗi 'JSONDecodeError: Expecting value: line 1 column 1 (char 0)' trong Python

intermediate🐍 Python2026-05-03| Python 3.x, mọi hệ điều hành (Linux, macOS, Windows), phổ biến trong ứng dụng Flask/FastAPI, script gọi REST API hoặc đọc file JSON

Error Message

json.decoder.JSONDecodeError: Expecting value: line 1 column 1 (char 0)
#python#json#jsondecodeerror#parsing

Tình Huống Thực Tế

2 giờ sáng. Cron job vừa chết. Log hiển thị:

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

char 0 nghĩa là Python chưa vượt qua được ký tự đầu tiên. Chuỗi nó cố parse có thể hoàn toàn rỗng, hoặc là thứ gì đó hoàn toàn không mong đợi — một trang HTML lỗi, một ký tự xuống dòng đơn thuần, một ký tự BOM, hoặc response 500 từ một API mà bạn tưởng là đáng tin cậy.

Chuyện Gì Đang Xảy Ra

Cả json.loads()json.load() đều ném ra JSONDecodeError ngay khi gặp bất cứ thứ gì không phải JSON hợp lệ. Variant line 1 column 1 (char 0) cụ thể nghĩa là input thuộc một trong các trường hợp sau:

  • Chuỗi rỗng ("" hoặc b"")
  • Chỉ có khoảng trắng (" ")
  • Một trang HTML — response 404 hoặc 503 từ API
  • Thông báo lỗi dạng plain-text từ server
  • File bị cắt ngắn giữa chừng khi ghi, hoặc chưa được ghi lần nào
  • Response có UTF-8 BOM (\xef\xbb\xbf) ở đầu

Cách sửa phụ thuộc vào trường hợp bạn đang gặp. Hãy bắt đầu bằng cách in ra chính xác thứ bạn đang cố parse.

Chẩn Đoán Trước Khi Sửa

Thêm một dòng này trước lệnh gọi json.loads() của bạn:

print(repr(response_text))  # or repr(raw_string)
json.loads(response_text)

repr() loại bỏ mọi ảo giác. Bạn sẽ thấy ngay chuỗi có rỗng không, có bắt đầu bằng <!DOCTYPE không, hoặc có BOM ẩn ở vị trí zero không.

Sửa Nhanh Theo Từng Nguyên Nhân

1. Chuỗi Rỗng từ API Response

Đây là nguyên nhân phổ biến nhất trên môi trường production. API trả về 204 No Content, timeout, hoặc trả về trang lỗi 5xx thay vì JSON.

import requests
import json

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

# Sai — crash khi body rỗng
data = json.loads(response.text)

# Đúng — kiểm tra trước khi parse
if response.status_code == 200 and response.text.strip():
    data = response.json()  # requests xử lý việc decode
else:
    print(f"Unexpected response: {response.status_code} — {repr(response.text[:200])}")    

Ưu tiên dùng response.json() từ thư viện requests khi có thể. Nó ném ra requests.exceptions.JSONDecodeError với nhiều ngữ cảnh hơn so với phiên bản stdlib thuần túy.

2. Đọc File Rỗng Hoặc Bị Cắt Ngắn

import json
import os

path = "data.json"

if not os.path.exists(path) or os.path.getsize(path) == 0:
    raise FileNotFoundError(f"JSON file missing or empty: {path}")

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

Ghi và đọc file từ các process khác nhau? Bên ghi có thể chưa flush xong. Gọi f.flush() rồi os.fsync(f.fileno()) sau khi ghi để đảm bảo dữ liệu đã được lưu xuống đĩa.

3. BOM ở Đầu File

Các file được lưu bởi công cụ Windows hoặc Excel thường lén thêm vào UTF-8 BOM (\xef\xbb\xbf). Module json của Python coi đó là ký tự không phải JSON và dừng lại ngay lập tức.

# Sửa: utf-8-sig tự động bỏ BOM trước khi parse
with open("data.json", "r", encoding="utf-8-sig") as f:
    data = json.load(f)

4. API Trả Về HTML Thay Vì JSON

Rate limiting, token xác thực hết hạn, và proxy cấu hình sai đều hay trả về trang HTML lỗi. Kiểm tra header Content-Type — nó không bao giờ nói dối:

import requests

response = requests.get(url, headers={"Accept": "application/json"})

content_type = response.headers.get("Content-Type", "")
if "application/json" not in content_type:
    raise ValueError(f"Expected JSON, got {content_type}: {response.text[:300]}")

data = response.json()

5. Biến Môi Trường Rỗng

Biến môi trường luôn là chuỗi. Nếu một biến chưa được set hoặc để trống, bạn đang truyền thẳng chuỗi rỗng vào json.loads():

import json
import os

raw = os.environ.get("MY_CONFIG", "")

if not raw:
    raise EnvironmentError("MY_CONFIG environment variable is not set")

config = json.loads(raw)

Một Helper Hữu Dụng Nên Giữ Lại

Sau khi tìm ra nguyên nhân gốc rễ, hãy bọc việc parse JSON vào một helper. Traceback khó hiểu lúc 2 giờ sáng chẳng vui chút nào — cái này cho bạn thông tin có thể xử lý được ngay:

import json
from typing import Any

def safe_parse_json(raw: str, source: str = "unknown") -> Any:
    """Parse JSON với thông báo lỗi hữu ích khi thất bại."""
    if not isinstance(raw, str):
        raw = raw.decode("utf-8", errors="replace")  # xử lý bytes
    stripped = raw.strip()
    if not stripped:
        raise ValueError(f"Empty JSON input from {source}")
    try:
        return json.loads(stripped)
    except json.JSONDecodeError as e:
        preview = repr(stripped[:200])
        raise ValueError(f"Invalid JSON from {source}: {e} — got: {preview}") from e

Gọi như sau:

data = safe_parse_json(response.text, source=f"GET {url}")

Lần sau khi lỗi này xảy ra lúc 2 giờ sáng, log của bạn sẽ hiển thị chính xác input xấu đến từ đâu và 200 ký tự đầu tiên trông như thế nào.

Kiểm Tra Sau Khi Sửa

Chạy kiểm tra nhanh này trước khi deploy:

import json

test_cases = [
    ("", "empty string"),
    ("   ", "whitespace only"),
    ('{"key": "value"}', "valid JSON"),
    ("null", "JSON null"),
]

for raw, label in test_cases:
    try:
        result = json.loads(raw) if raw.strip() else None
        print(f"{label}: OK → {result}")
    except json.JSONDecodeError as e:
        print(f"{label}: FAIL → {e}")

Kết quả mong đợi:

empty string: OK → None
whitespace only: OK → None
valid JSON: OK → {'key': 'value'}
JSON null: OK → None

Mẹo Phòng Ngừa

Đang nhìn chằm chằm vào một response blob 3.000 ký tự mà không biết cú pháp bị lỗi ở đâu? Paste vào JSON Formatter & Validator tại ToolCraft. Nó highlight chính xác token nào không hợp lệ — không có gì được upload lên, tất cả chạy ngay trên trình duyệt của bạn.

Về lâu dài:

  • Luôn kiểm tra status code của API response trước khi đụng vào body.
  • Log raw response bất cứ khi nào parse JSON thất bại — bạn sẽ cần nó để debug sau này.
  • Nếu bạn sở hữu API, hãy trả về Content-Type: application/json nhất quán, kể cả cho các response lỗi.
  • Với các pipeline dựa trên file, hãy ghi theo cách atomic: output ra file .tmp trước, rồi dùng os.rename() để đưa vào vị trí cuối cùng. Điều này ngăn chặn hoàn toàn việc đọc file chưa ghi xong.

Related Error Notes