Python json.decoder.JSONDecodeError: Expecting value: line 1 column 1 (char 0) の原因と解決方法【空レスポンスの罠】

json.decoder.JSONDecodeError: Expecting value: line 1 column 1 (char 0)とは?ざっくりと3行で
  • json.decoder.JSONDecodeError: Expecting value は、渡された文字列が空、または先頭がJSONとして解釈できないときに発生するエラーだ。
  • 実務で圧倒的に多いのは、APIが404や500を返してボディが空だったり、ログイン画面のHTMLが戻ってきていたりするケース。そもそもJSONを受け取れていないのが本当の原因だ。
  • 直すときは、パースの前に status_codeContent-Type、そしてレスポンス本文の先頭を目で確認する。この3点を検証するだけで大半は防げる。
Python JSONDecodeError: Expecting valueで焦る開発者が、レスポンスが404で空だったと気づき、statusとContent-Typeの検証を追加して解決する4コマ漫画
①API連携スクリプトを実行するとJSONDecodeErrorが赤く表示される。②レスポンス本文を見ると404で中身が空だと判明する。③パースの前にstatusを検証する修正を入れてエラーが消える。④翌朝、後輩にJSONが来ていない疑いを先に持てと助言する。

この4コマで描かれているのは、外部APIと連携するバッチ処理で最も頻繁に起きる事故です。スクリプト側のロジックには一切の誤りがなく、壊れていたのは受け取ったデータのほうでした。パースの失敗はデータ供給側の異常を知らせるサインであり、コードを読み返しても答えは出てきません。

2コマ目でレスポンス本文を確認した判断が、解決までの時間を分けています。JSONの構文を疑い続けると、存在しないカンマや引用符を探して延々と時間を消費してしまうでしょう。実データを一度も見ずにデバッグを始めるのは、封も開けていない箱の中身を推測するようなものです。

この問題を放置したまま本番リリースすると、被害はエラー表示だけにとどまりません。日次バッチが例外で停止すれば在庫や売上の同期が丸一日途切れますし、決済APIの応答をパースできなければ注文が確定したのか判断できない状態が生まれます。取得と検証を分けて設計することが、障害の連鎖を断ち切る現実的な備えになります。

json.decoder.JSONDecodeError: Expecting value: line 1 column 1 (char 0)の基本情報

json.decoder.JSONDecodeError: Expecting value: line 1 column 1 (char 0) は、Python標準ライブラリの json モジュールが、与えられた文字列の先頭からJSONの値を1つも読み取れなかったときに送出する例外です。char 0 という表示は、1文字目の時点で解析が止まったことを意味します。

項目内容
エラーメッセージjson.decoder.JSONDecodeError: Expecting value: line 1 column 1 (char 0)
発生する言語・環境Python 3.5以降(json.loads() / json.load()、および requestsResponse.json()
エラーの意味値が来るはずの位置に値がなかった、という意味。JSONDecodeErrorValueError のサブクラスで、失敗位置を line / column / char の3表記で示す
主な原因パース対象が空文字列、またはHTMLなどJSON以外のテキストが渡っている
解決の基本方針パースする前に、実際に渡している文字列の長さと先頭数十文字を出力して目視する
Python JSONDecodeErrorの原因を解説するITKAGYO運営者デプロイ太郎のアイコン画像

コードは1行も変えていないのに突然出るタイプのエラー、焦りますよね。現場でもよく聞く相談です。

なぜ json.decoder.JSONDecodeError: Expecting value は line 1 column 1 で止まるのか?

JSONの書き方を間違えたせいだと思われがちですが、char 0 で止まっている場合は文法ミスではなく、そもそもJSONではないものを渡している可能性が極めて高いという点に注意が必要です。

json.decoder.JSONDecodeError: Expecting value は、CPythonの json/decoder.py にある raw_decode() の挙動をたどると理解が早くなります。この関数は内部のスキャナに1つ目の値を読ませ、読み取れずに StopIteration が返ってきた瞬間に JSONDecodeError("Expecting value", s, err.value) を投げる作りになっています。

つまりメッセージの数字は、失敗した文字位置をそのまま表しているにすぎません。データがどこから来たのかで原因は3系統に分かれるため、以下では入力の供給元ごとに切り分けていきます。なお、JSONそのものの構造については JSONとは?Web APIの標準データ形式を理解する最重要フォーマットを解説 もあわせて確認しておくと理解が深まります。

原因パターン1:APIが404や204を返し、ボディが空だった

最も件数が多いのがこのパターンです。IDの指定ミスやエンドポイントの変更で404が返り、多くのAPIは404時に本文を返さないため、パース対象が空文字列になります。

# Python 3.14 / requests 2.34
import requests

res = requests.get("https://api.example.com/v1/users/999999", timeout=10)

# res.status_code は 404、res.text は空文字列 '' になっている
data = res.json()  # ← ここで例外が発生する
# requests.exceptions.JSONDecodeError: Expecting value: line 1 column 1 (char 0)

# 標準ライブラリだけでも同じ結果になる
# json.loads("")
# json.decoder.JSONDecodeError: Expecting value: line 1 column 1 (char 0)

空文字列を渡すと、スキャナは0文字目で読むものを見失い、そのまま Expecting value となります。requestsResponse.json() は内部で json.loads() を呼んでいるだけなので、失敗の理由は標準ライブラリの場合とまったく同じです。修正はパースの前段に検証を挟むだけで済みます。

# Python 3.14 / requests 2.34
import requests

res = requests.get("https://api.example.com/v1/users/999999", timeout=10)

# 修正1: パースの前にステータスコードを検証する
if res.status_code != 200:
    raise RuntimeError(f"API異常応答 status={res.status_code} body={res.text[:200]!r}")

# 修正2: ボディが空でないことを確かめてからパースする
if not res.text.strip():
    raise RuntimeError("レスポンスボディが空でした")

data = res.json()
print(data)

原因パターン2:Content-Typeがtext/htmlなのに.json()を呼んでいる

ステータスコードが200でも安心はできません。セッション切れでログイン画面へ飛ばされた、プロキシやWAFが警告ページを差し込んだ、レートリミットの案内ページが返った、といった場面ではHTMLが200で届きます。

# Python 3.14 / requests 2.34
import requests

# セッション切れでログイン画面のHTMLが返ってくるケース
res = requests.get("https://example.com/api/items", timeout=10)

print(res.status_code)                  # 200(成功しているように見える)
print(res.headers.get("Content-Type"))  # text/html; charset=UTF-8
print(res.text[:30])                    # <!DOCTYPE html><html lang="ja">

data = res.json()  # ← 先頭の < をJSONとして読もうとして失敗する
# requests.exceptions.JSONDecodeError: Expecting value: line 1 column 1 (char 0)

JSONの値は { [ " 数字 t f n のいずれかで始まります。< はそのどれにも該当しないため、0文字目で即座に打ち切られるわけです。ヘッダーの型を先に確認する習慣をつけると、この種の混乱は起きなくなります。ブラウザ側で同じ現象に遭遇したときは JavaScript SyntaxError: Unexpected token < in JSONの原因と解決方法 が参考になります。

# Python 3.14 / requests 2.34
import requests
from requests.exceptions import JSONDecodeError  # requests 2.27.0 以降で利用できる

res = requests.get(
    "https://example.com/api/items",
    headers={"Accept": "application/json"},  # 修正1: JSONを要求することを明示する
    timeout=10,
)
res.raise_for_status()  # 修正2: 4xx/5xx をここで例外にする

content_type = res.headers.get("Content-Type", "")
if "json" not in content_type.lower():  # 修正3: 型をヘッダーで判定する
    raise RuntimeError(f"JSON以外の応答 type={content_type!r} head={res.text[:80]!r}")

try:
    data = res.json()
except JSONDecodeError as e:  # 修正4: 失敗時に生データを添えて記録する
    raise RuntimeError(f"JSON解析に失敗 {e.msg} head={res.text[:80]!r}") from e

原因パターン3:読み込んだローカルJSONファイルが空だった

ネットワークが絡まない場面でも発生します。書き込み処理が途中で落ちて0バイトのファイルが残った、初回起動でまだ設定ファイルを生成していない、といった状況が典型例でしょう。

# Python 3.14 / 標準ライブラリ json
import json

# config.json は 0 バイト(書き込みが途中で失敗して空になっている)
with open("config.json", encoding="utf-8") as f:
    config = json.load(f)  # ← 空文字列をパースしようとして失敗する
# json.decoder.JSONDecodeError: Expecting value: line 1 column 1 (char 0)

json.load() はファイルオブジェクトの中身を読み込んでから json.loads() に渡すだけの薄いラッパーです。したがって0バイトのファイルは空文字列として扱われ、結果はパターン1と同一になります。ファイル入力では、存在確認とサイズ確認に加えてBOM対策として utf-8-sig を指定するのが定石です。

# Python 3.14 / 標準ライブラリ json
import json
from pathlib import Path

path = Path("config.json")

# 修正1: 存在とサイズを先に確認し、空ファイルは既定値で処理を続ける
if not path.exists() or path.stat().st_size == 0:
    config = {}
else:
    # 修正2: utf-8-sig で読み込み、先頭にBOMが付いていても取り除く
    text = path.read_text(encoding="utf-8-sig")
    try:
        config = json.loads(text)
    except json.JSONDecodeError as e:
        # 修正3: 何文字目で失敗したかと、その周辺の実データを出力する
        print(f"{e.msg} at line {e.lineno} column {e.colno} (char {e.pos})")
        print(repr(text[max(0, e.pos - 30):e.pos + 30]))
        raise

print(config)
JSONDecodeErrorの原因パターンについて補足するITKAGYO運営者デプロイ太郎のアイコン画像

3パターンの中では、200番なのにHTMLが返っているケースが一番見抜きにくいと感じます。ステータスコードだけ見て安心しないのがコツです。

json.decoder.JSONDecodeError のメッセージは環境によって変わる?

json.decoder.JSONDecodeError は、実行しているライブラリのバージョンによって表示される例外クラス名が変化します。トレースバックの先頭に出るクラス名が違っても、コロン以降の本文が同じであれば原因は共通です。以下は公式ソースおよびリリースノートで確認できた範囲のみを掲載しています。

実行環境表示されるエラーメッセージ
Python 3.14 標準ライブラリ(json.loads("")json.decoder.JSONDecodeError: Expecting value: line 1 column 1 (char 0)
requests 2.27.0 以降の Response.json()requests.exceptions.JSONDecodeError: Expecting value: line 1 column 1 (char 0)
requests 2.26.0 以前の Response.json()json.decoder.JSONDecodeError: Expecting value: line 1 column 1 (char 0)
先頭にBOMが付いた文字列を json.loads() に渡した場合json.decoder.JSONDecodeError: Unexpected UTF-8 BOM (decode using utf-8-sig): line 1 column 1 (char 0)

DjangoやAWS Lambdaで json.decoder.JSONDecodeError が出るのはどんなときか?

Djangoでの発生パターン

json.decoder.JSONDecodeError は、Djangoのビューでリクエストボディを手動パースする実装でよく顔を出します。HttpRequest.body はバイト列を返すプロパティで、ボディを持たないリクエストが届くと b'' になります。ヘルスチェックのGETや、ブラウザが送るプリフライト後の空POSTがこの状態を作り出します。テストでは常にボディ付きで叩いているため、本番で初めて例外に気づくという流れが起こりがちです。

# Python 3.14 / Django 5.x
import json
from django.http import JsonResponse, HttpResponseBadRequest

# 発生するコード: ボディが空のリクエストが届くと例外になる
def create_item_ng(request):
    payload = json.loads(request.body)  # ← request.body が b'' のとき失敗する
    # json.decoder.JSONDecodeError: Expecting value: line 1 column 1 (char 0)
    return JsonResponse({"ok": True})

# 修正後のコード: 空ボディと不正JSONを400として明示的に返す
def create_item_ok(request):
    raw = request.body
    if not raw:
        return HttpResponseBadRequest("リクエストボディが空です")
    try:
        payload = json.loads(raw)
    except json.JSONDecodeError as e:
        return HttpResponseBadRequest(f"JSONの形式が不正です: {e.msg}")
    return JsonResponse({"ok": True, "received": payload})
Django REST Frameworkを使う場合は自前でパースせず、Serializerに検証を任せると空ボディも不正JSONも自動で400として扱えます。

AWS Lambdaでの発生パターン

json.decoder.JSONDecodeError は、API Gatewayのプロキシ統合で動くLambda関数でも定番の躓きどころになります。イベントの body キーは、ボディなしのリクエストでは None、空ボディでは空文字列になり、どちらも json.loads() に渡すと処理が破綻します。CloudWatch Logsにスタックトレースだけが積み上がり、原因の特定が遅れるケースも珍しくありません。

# Python 3.13 / AWS Lambda(API Gateway プロキシ統合)
import json

# 発生するコード: body が空文字列のイベントで落ちる
def lambda_handler_ng(event, context):
    payload = json.loads(event["body"])  # ← body が "" のとき失敗する
    # json.decoder.JSONDecodeError: Expecting value: line 1 column 1 (char 0)
    return {"statusCode": 200, "body": json.dumps({"ok": True})}

# 修正後のコード: body の有無を確認し、失敗時は400を返す
def lambda_handler_ok(event, context):
    raw = event.get("body") or ""
    if not raw.strip():
        return {"statusCode": 400, "body": json.dumps({"message": "body is empty"})}
    try:
        payload = json.loads(raw)
    except json.JSONDecodeError as e:
        return {"statusCode": 400, "body": json.dumps({"message": e.msg})}
    return {"statusCode": 200, "body": json.dumps({"ok": True, "received": payload})}
バイナリ扱いのリクエストでは isBase64Encoded が真になり本文がBase64文字列で届くため、デコードしてからパースする分岐も用意しておきましょう。

Expecting value 以外のメッセージが出たときはどう読み替える?

char 0 ではなく line 1 column 5 などと出る場合

json.decoder.JSONDecodeError の位置表示が0以外であれば、先頭は正しく読めており途中で崩れています。配列やオブジェクトの末尾に余分なカンマが残っていると、カンマの次に値を探しに行って失敗するため、このパターンになります。

# Python 3.14 / 標準ライブラリ json
import json

# 修正前: 配列の末尾に不要なカンマが残っている
json.loads("[1, ]")
# json.decoder.JSONDecodeError: Expecting value: line 1 column 5 (char 4)

# 修正後: 末尾カンマを取り除く(JSONは末尾カンマを許容しない)
json.loads("[1]")
# [1]

Unexpected UTF-8 BOM と出る場合

Excelやメモ帳が書き出したファイルには、先頭にBOMと呼ばれる不可視の印が入ることがあります。この文字列をそのまま json.loads() へ渡すと、CPythonはBOMを検知して専用のメッセージを返す仕様です。読み込み時のエンコーディング指定を変えるだけで解消します。

# Python 3.14 / 標準ライブラリ json
import json
from pathlib import Path

# 修正前: BOM が文字列の先頭に残る
text_ng = Path("data.json").read_text(encoding="utf-8")
json.loads(text_ng)
# json.decoder.JSONDecodeError: Unexpected UTF-8 BOM (decode using utf-8-sig): line 1 column 1 (char 0)

# 修正後: utf-8-sig を指定して BOM を取り除いてから読む
text_ok = Path("data.json").read_text(encoding="utf-8-sig")
data = json.loads(text_ok)
print(data)

json.decoder.JSONDecodeError はどうデバッグすればいい?

json.decoder.JSONDecodeError の調査は、パースを試みる前に生のデータを観測することから始めます。例外オブジェクトは msg doc pos lineno colno という属性を持っており、失敗した位置と元データを両方保持しています。この属性を出力すれば、推測に頼らず問題箇所を特定できるでしょう。

# Python 3.14 / requests 2.34
import json
import requests

res = requests.get("https://api.example.com/v1/items", timeout=10)

# 手順1: パースの前に生のレスポンスを必ず目で見る
print("status      :", res.status_code)
print("content-type:", res.headers.get("Content-Type"))
print("length      :", len(res.content))
print("head        :", repr(res.text[:200]))

# 手順2: 例外オブジェクトの属性から失敗位置を特定する
try:
    data = res.json()
except json.JSONDecodeError as e:  # requests.exceptions.JSONDecodeError もこれで捕まる
    print("msg :", e.msg)                      # Expecting value
    print("pos :", e.pos)                      # 0
    print("line:", e.lineno, "col:", e.colno)  # 1 1
    print("周辺:", repr(e.doc[max(0, e.pos - 40):e.pos + 40]))
    raise

取得した文字列が長くて構造を追いにくいときは、手元で整形してから眺めるのが早道です。ブラウザ上で貼り付けて確認できる JSON整形・検証ツール を使えば、どの階層で構造が崩れているかを視覚的に把握できます。

JSONDecodeErrorのデバッグのコツを紹介するITKAGYO運営者デプロイ太郎のアイコン画像

デバッグのコツはひとつだけ、res.text の先頭を出力すること。ここを飛ばすと遠回りになりがちなので、落ち着いて一つずつ確認していきましょう!

json.decoder.JSONDecodeError を未然に防ぐには?

json.decoder.JSONDecodeError の予防は、取得と検証と変換を分離する設計に集約されます。呼び出しのたびに検証コードを書くと抜け漏れが生じるため、共通のヘルパー関数へ寄せておくのが実務的です。例外を握りつぶさず、失敗時にレスポンスの先頭を必ずメッセージへ含めておくと、障害対応の速度が大きく変わります。

# Python 3.14 / requests 2.34
import requests
from requests.exceptions import JSONDecodeError


def fetch_json(url, *, timeout=10):
    """JSONを安全に取得する共通ヘルパー。失敗理由が特定できる例外を投げる。"""
    res = requests.get(url, headers={"Accept": "application/json"}, timeout=timeout)
    res.raise_for_status()                  # 予防1: 4xx/5xx をここで止める

    content_type = res.headers.get("Content-Type", "")
    if "json" not in content_type.lower():  # 予防2: 型が違うものはパースしない
        raise ValueError(f"JSON以外の応答 type={content_type!r} head={res.text[:100]!r}")

    if not res.content:                     # 予防3: 空ボディを明示的に弾く
        raise ValueError("レスポンスボディが空です")

    try:
        return res.json()
    except JSONDecodeError as e:            # 予防4: 生データを添えて再送出する
        raise ValueError(f"JSON解析に失敗 {e.msg} head={res.text[:100]!r}") from e
パース後の中身が期待どおりかまでは json モジュールでは保証できないため、pydanticなどのバリデーションライブラリで受け取ったデータの型と必須項目を検証するところまでを一続きの処理として組み込みましょう。

requestsのバージョンで json.decoder.JSONDecodeError の例外型はどう変わるのか?

json.decoder.JSONDecodeError を except で捕まえるとき、requestsのバージョン差を意識しないとハンドリングが空振りします。requests 2.27.0 のリリースノートには、Python 2と3のJSON例外を統一する目的で requests.exceptions.JSONDecodeError を追加し、これが response.json() から送出されると明記されています。

この新しいクラスは、requestsの InvalidJSONError と標準ライブラリの JSONDecodeError の両方を継承しています。したがって requests.exceptions.RequestException でも json.JSONDecodeError でも捕捉でき、2.26.0以前のコードを壊さない後方互換性が保たれました。いずれの型も最終的には ValueError を継承しているため、バージョンを問わず確実に受け止めたい場面では ValueError を最後の砦に置く書き方が有効です。

# Python 3.14 / requests のバージョン差を吸収する書き方
import json
import requests

url = "https://api.example.com/v1/items"

try:
    data = requests.get(url, timeout=10).json()
except json.JSONDecodeError as e:
    # requests 2.27.0 以降: requests.exceptions.JSONDecodeError(json版を継承)
    # requests 2.26.0 以前: json.decoder.JSONDecodeError がそのまま送出される
    print("JSONとして読めません:", e.msg)
except ValueError as e:
    # 別のJSON実装が使われている環境など、上で拾えない差分をここで受け止める
    print("パースに失敗しました:", e)
JSONDecodeErrorの対処法をまとめて励ますITKAGYO運営者デプロイ太郎のアイコン画像

ここまで読めば、あとは res.text を出力するだけで原因にたどり着けるはずです。焦らず、届いたデータを疑うところから始めてみてください。

よくある質問

Q
ローカルでは動くのに、本番環境でだけこのエラーが出るのはなぜですか?
A

本番の経路上に、ローカルには存在しない中継装置が挟まっているためです。企業プロキシの認証ページ、WAFのブロック画面、CDNのメンテナンス通知、レートリミット超過の案内などは、いずれもHTMLを200番で返してきます。切り分けるには、本番と同じネットワークから curl -i でレスポンスヘッダーと本文を取得し、Content-Typeを確認してください。

Q
DjangoやAWS Lambdaで空ボディのリクエストを受けたときも同じエラーになりますか?
A

はい、まったく同じメッセージが出ます。Djangoの request.body は空リクエストで b'' を返し、API Gateway統合のLambdaでは event["body"] が空文字列や None になります。どちらも json.loads() に渡す前に有無を判定し、空であれば400 Bad Requestとして返すのが正しい対応です。

Q
Linterや型チェックツールで事前に防ぐことはできますか?
A

静的解析だけで防ぐのは不可能です。ruffやmypyが検査するのはコードの構文と型注釈であり、実行時にサーバーから何が返るかまでは知り得ません。代わりに、レスポンス検証を通す共通ヘルパーを用意してレビュー基準に組み込み、.json() の直接呼び出しをコードレビューで弾く運用のほうが効果的です。

Q
自作APIの利用者には、このエラーをどう見せるべきですか?
A

スタックトレースをそのまま返さず、400番と機械可読なエラーボディに変換して返してください。{"error": "invalid_json", "detail": "Expecting value at char 0"} のように、原因コードと位置情報を分けて渡すと利用者が自力で修正できます。500番で返してしまうと、送信側の入力ミスがサーバー障害として扱われ、調査が長引く原因になります。

Q
json.decoder.JSONDecodeError: Expecting value と Extra data の違いは何ですか?
A

失敗した段階が正反対です。Expecting value は値をひとつも読めなかったときのメッセージで、char 0 なら入力が空かJSON以外だと判断できます。対して Extra data は最初の値を読み終えた後に文字が余っていた場合に出るもので、1行1JSONのJSON Lines形式を json.loads() でまとめて読もうとした場面が代表例です。後者は1行ずつループしてパースすれば解決します。

この記事と一緒に知っておきたいエラー解決

関連エラー この記事との関連
JavaScript SyntaxError: Unexpected token < in JSON HTMLがJSONとして渡されたときに起きる、ブラウザ側での同一現象です。
Python requests.exceptions.ConnectionError 同じrequestsでのAPI連携で、レスポンスを受け取る手前の通信段階で起きるエラーです。
PHP Fatal error: Cannot use object of type stdClass as array JSONのデコードには成功した後、変換されたデータ型の扱いを誤って起きるエラーです。
Python ValueError: invalid literal for int() JSONDecodeErrorと同じくValueError系で、想定外の文字列を変換しようとして発生します。
Python FileNotFoundError ローカルのJSONファイルを読み込む処理で、パース以前のファイル指定ミスとして起きます。

【出典】参考URL

https://docs.python.org/3/library/json.html:json.JSONDecodeErrorがValueErrorのサブクラスであること、msg/doc/pos/lineno/colno属性、BOMでValueErrorが送出される仕様の根拠
https://github.com/python/cpython/blob/main/Lib/json/decoder.py:raw_decode()がStopIteration捕捉時にExpecting valueを送出する実装と、line/column/charの算出式の根拠
https://github.com/python/cpython/blob/main/Lib/json/__init__.py:loads()の先頭BOM判定とUnexpected UTF-8 BOM (decode using utf-8-sig)というメッセージ文言の根拠
https://github.com/psf/requests/blob/main/HISTORY.md:requests 2.27.0でrequests.exceptions.JSONDecodeErrorが追加され、response.json()から送出されるようになった記述の根拠
https://github.com/psf/requests/blob/main/src/requests/models.py:Response.json()が内部でjson.loadsを呼び、失敗時にRequestsJSONDecodeErrorへ変換している実装の根拠
https://github.com/psf/requests/blob/main/src/requests/exceptions.py:requests.exceptions.JSONDecodeErrorがInvalidJSONErrorと標準ライブラリのJSONDecodeErrorを継承している根拠
https://www.python.org/downloads/:本文およびコード例で示したPythonのバージョン表記の根拠

コメント

デプロイ太郎のSNSを見てみる!!