- json.decoder.JSONDecodeError: Expecting value は、渡された文字列が空、または先頭がJSONとして解釈できないときに発生するエラーだ。
- 実務で圧倒的に多いのは、APIが404や500を返してボディが空だったり、ログイン画面のHTMLが戻ってきていたりするケース。そもそもJSONを受け取れていないのが本当の原因だ。
- 直すときは、パースの前に
status_codeとContent-Type、そしてレスポンス本文の先頭を目で確認する。この3点を検証するだけで大半は防げる。
この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 で止まるのか?
- json.decoder.JSONDecodeError のメッセージは環境によって変わる?
- DjangoやAWS Lambdaで json.decoder.JSONDecodeError が出るのはどんなときか?
- Expecting value 以外のメッセージが出たときはどう読み替える?
- json.decoder.JSONDecodeError はどうデバッグすればいい?
- json.decoder.JSONDecodeError を未然に防ぐには?
- requestsのバージョンで json.decoder.JSONDecodeError の例外型はどう変わるのか?
- よくある質問
- この記事と一緒に知っておきたいエラー解決
- 【出典】参考URL
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()、および requests の Response.json()) |
| エラーの意味 | 値が来るはずの位置に値がなかった、という意味。JSONDecodeError は ValueError のサブクラスで、失敗位置を line / column / char の3表記で示す |
| 主な原因 | パース対象が空文字列、またはHTMLなどJSON以外のテキストが渡っている |
| 解決の基本方針 | パースする前に、実際に渡している文字列の長さと先頭数十文字を出力して目視する |

コードは1行も変えていないのに突然出るタイプのエラー、焦りますよね。現場でもよく聞く相談です。
なぜ json.decoder.JSONDecodeError: Expecting value は line 1 column 1 で止まるのか?
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 となります。requests の Response.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)

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})
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})}
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整形・検証ツール を使えば、どの階層で構造が崩れているかを視覚的に把握できます。

デバッグのコツはひとつだけ、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
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)

ここまで読めば、あとは res.text を出力するだけで原因にたどり着けるはずです。焦らず、届いたデータを疑うところから始めてみてください。
よくある質問
-
Qローカルでは動くのに、本番環境でだけこのエラーが出るのはなぜですか?
-
A
本番の経路上に、ローカルには存在しない中継装置が挟まっているためです。企業プロキシの認証ページ、WAFのブロック画面、CDNのメンテナンス通知、レートリミット超過の案内などは、いずれもHTMLを200番で返してきます。切り分けるには、本番と同じネットワークから
curl -iでレスポンスヘッダーと本文を取得し、Content-Typeを確認してください。
-
QDjangoやAWS Lambdaで空ボディのリクエストを受けたときも同じエラーになりますか?
-
A
はい、まったく同じメッセージが出ます。Djangoの
request.bodyは空リクエストでb''を返し、API Gateway統合のLambdaではevent["body"]が空文字列やNoneになります。どちらもjson.loads()に渡す前に有無を判定し、空であれば400 Bad Requestとして返すのが正しい対応です。
-
QLinterや型チェックツールで事前に防ぐことはできますか?
-
A
静的解析だけで防ぐのは不可能です。ruffやmypyが検査するのはコードの構文と型注釈であり、実行時にサーバーから何が返るかまでは知り得ません。代わりに、レスポンス検証を通す共通ヘルパーを用意してレビュー基準に組み込み、
.json()の直接呼び出しをコードレビューで弾く運用のほうが効果的です。
-
Q自作APIの利用者には、このエラーをどう見せるべきですか?
-
A
スタックトレースをそのまま返さず、400番と機械可読なエラーボディに変換して返してください。
{"error": "invalid_json", "detail": "Expecting value at char 0"}のように、原因コードと位置情報を分けて渡すと利用者が自力で修正できます。500番で返してしまうと、送信側の入力ミスがサーバー障害として扱われ、調査が長引く原因になります。
-
Qjson.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のバージョン表記の根拠

コメント