PythonでJSONを読み書きするには、標準ライブラリの json モジュールを使い、ファイルは json.load() / json.dump()、文字列は json.loads() / json.dumps() で変換します。 日本語を含むなら、open() に encoding="utf-8"、書き出しに ensure_ascii=False を付けるのが基本形です。

この記事では、基本の使い分けに加えて、「datetimeが保存できない」「JSONDecodeErrorの原因がわからない」「数値キーが文字列に変わった」など実務のつまずきをコピペで動くレシピにまとめました。コードは Python 3.14.7 で実行し、出力をそのまま掲載しています(3.13以前で違う箇所は明記)。

Python JSON 読み書きの早見表

やりたいこと 書き方
ファイルを読む with open(path, encoding="utf-8") as f: data = json.load(f)
ファイルに書く json.dump(obj, f, ensure_ascii=False, indent=2)(open(path, "w", encoding="utf-8"))
文字列 → Python json.loads(text)(bytes も可)
Python → 文字列 json.dumps(obj, ensure_ascii=False)
日本語をそのまま出す ensure_ascii=False
整形/キー順を固定 indent=2 / sort_keys=True
空白なしで出す separators=(",", ":")
datetime・Decimal を保存 default=関数 または cls=JSONEncoder派生
小数を誤差なく読む parse_float=Decimal
読み込み時に型を戻す object_hook=関数
重複キーを検出 object_pairs_hook=関数
NaN・Infinity を禁止 allow_nan=False
1行1件(JSON Lines) 1行ずつ loads / dumps(obj) + "\n"
BOM付きファイルを読む encoding="utf-8-sig"
壊れた場所を知る except json.JSONDecodeError as e: → e.lineno e.colno
ターミナルで整形 python -m json file.json(3.14〜。以前は python -m json.tool)

本記事の内容は、ご自由にお使いください。
ご利用の際は、出典として本ページへのリンクを記載いただけますようお願いします。

(記載例)出典:株式会社RJC「Python JSON 読み書き|コピペレシピと実行結果・3.14対応」

load・loads・dump・dumps の使い分け

末尾に s が付くものは文字列(string)、付かないものはファイルを扱います。

関数 入力 → 出力 よく使う場面
json.load(fp) ファイル → Pythonの値 設定・保存データを読む
json.loads(s) 文字列 → Pythonの値 APIの応答本文を読む
json.dump(obj, fp) Pythonの値 → ファイル データを保存する
json.dumps(obj) Pythonの値 → 文字列 APIに送る、ログに出す

型は dict⇔object、list・tuple⇔array、str⇔string、int・float⇔number、True/False⇔true/false、None⇔null に対応します。tuple は list になり、キーはすべて文字列になるため、読み戻すと元と同じにならない場合があります(レシピ4)。

1. JSONファイルを読み書きする(基本形)

open() には encoding="utf-8" を必ず付けます。 省略するとOSの既定の文字コードが使われ、Windowsでは日本語を含むファイルで UnicodeDecodeError になることがあります。

import json

orders = [{"order_id": "A-001", "item": "ボールペン", "qty": 3}]

with open("orders.json", "w", encoding="utf-8") as f:
    json.dump(orders, f, ensure_ascii=False, indent=2)

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

print(type(data), data[0]["item"])
print(open("orders.json", encoding="utf-8").read())

実行結果:

<class 'list'> ボールペン
[
  {
    "order_id": "A-001",
    "item": "ボールペン",
    "qty": 3
  }
]

2. 日本語をそのまま出す・整形する

json.dumps() は既定で日本語を「バックスラッシュ+u+16進4桁」の英数字に置き換えます。 JSONとしては正しいものの人が読めないため、ensure_ascii=False を付けます。

import json
d = {"qty": 3, "item": "ペン", "tags": ["赤", "黒"]}
s = json.dumps(d)                                     # 既定
print("ペン" in s, s.isascii(), json.loads(s) == d)
print(json.dumps(d, ensure_ascii=False))              # 日本語そのまま
print(json.dumps(d, ensure_ascii=False, sort_keys=True))
print(json.dumps(d, ensure_ascii=False, separators=(",", ":")))
print(json.dumps(d, ensure_ascii=False, indent=2))

実行結果:

False True True
{"qty": 3, "item": "ペン", "tags": ["赤", "黒"]}
{"item": "ペン", "qty": 3, "tags": ["赤", "黒"]}
{"qty":3,"item":"ペン","tags":["赤","黒"]}
{
  "qty": 3,
  "item": "ペン",
  "tags": [
    "赤",
    "黒"
  ]
}

1行目の True(3つ目)のとおり、どちらで書いても読み戻した値は同じです。違うのは見た目だけなので、目的で選びます。

目的 設定
人が読む設定ファイル indent=2, ensure_ascii=False
Gitで差分を見る 上に加えて sort_keys=True
通信量を減らす separators=(",", ":")
ASCIIしか通らない所へ渡す 既定(ensure_ascii=True)

キーの順番は、既定では dict に入れた順のままです。

3. datetime を保存しようとして出るエラー(3.14で場所がわかる)

datetime・Decimal・set などは、そのままでは json.dumps() できません。

import json
from datetime import datetime
order = {"order_id": "A-001", "items": [{"name": "ペン", "added": datetime(2026, 9, 28, 10, 30)}]}
json.dumps(order, ensure_ascii=False)

Python 3.14 の実行結果(末尾):

TypeError: Object of type datetime is not JSON serializable
when serializing dict item 'added'
when serializing list item 0
when serializing dict item 'items'

Python 3.11 では最初の1行だけです。3.14 ではエラーに「どこの値か」の注記が付き、下から items → 0番目 → added とたどれます。直し方はレシピ5です。

4. キーが数値 → 文字列になる罠

JSONのobjectのキーは必ず文字列です。

import json
print(json.dumps({"t": (1, 2), 1: "a", False: "b", None: "c"}))
d = {1: "one", 10: "ten", 2: "two"}
back = json.loads(json.dumps(d, sort_keys=True))
print(back, back == d, back.get(1), back.get("1"))
try:
    json.dumps({(1, 2): "x"})
except TypeError as e:
    print("TypeError:", e)

実行結果:

{"t": [1, 2], "1": "a", "false": "b", "null": "c"}
{'1': 'one', '2': 'two', '10': 'ten'} False None one
TypeError: keys must be str, int, float, bool or None, not tuple
  • 読み戻すと back.get(1) が None になります。back["1"] で引くか、{int(k): v for k, v in back.items()} で戻します。
  • sort_keys=True は文字列化の前の値で並べるため、1, 2, 10 の数値順になります。
  • tuple のキーはエラーです(skipkeys=True はそのキーを捨てて続行します)。

5. datetime・Decimal・set・dataclass・Enum を変換する

default= に「変換できない値を受け取り、JSONにできる値を返す関数」を渡します。

import dataclasses
import json
from datetime import date, datetime, timezone
from decimal import Decimal
from enum import Enum


class Status(Enum):
    NEW = "new"


@dataclasses.dataclass
class Item:
    name: str
    price: Decimal


def to_json(o):
    if isinstance(o, (datetime, date)):
        return o.isoformat()
    if isinstance(o, Decimal):
        return str(o)                  # 精度を保つため文字列に
    if isinstance(o, (set, frozenset)):
        return sorted(o)               # 順序を安定させる
    if isinstance(o, Enum):
        return o.value
    if dataclasses.is_dataclass(o) and not isinstance(o, type):
        return dataclasses.asdict(o)
    raise TypeError(f"{type(o).__name__} は JSON にできません")


order = {
    "ordered_at": datetime(2026, 9, 28, 10, 30, tzinfo=timezone.utc),
    "deliver_on": date(2026, 10, 1),
    "status": Status.NEW,
    "tags": {"急ぎ", "ギフト"},
    "items": [Item("ペン", Decimal("120.50"))],
}
print(json.dumps(order, default=to_json, ensure_ascii=False))

実行結果:

{"ordered_at": "2026-09-28T10:30:00+00:00", "deliver_on": "2026-10-01", "status": "new", "tags": ["ギフト", "急ぎ"], "items": [{"name": "ペン", "price": "120.50"}]}
型 おすすめの変換 理由
datetime / date isoformat() fromisoformat() で戻せる。タイムゾーンも残る
Decimal str() float() にすると誤差が入る
set sorted() 出力順が毎回同じになる
Enum .value IntEnum / StrEnum は変換なしで出力される
dataclass dataclasses.asdict() 入れ子も dict になる
Path / UUID str()

最後の raise TypeError は残します。 何でも str(o) にすると、想定外の値が黙って文字列化されます。

複数箇所で使うなら JSONEncoder を継承したクラスにして cls= で渡します。

import json
from datetime import datetime
from decimal import Decimal


class AppEncoder(json.JSONEncoder):
    def default(self, o):
        if isinstance(o, datetime):
            return o.isoformat()
        if isinstance(o, Decimal):
            return str(o)
        return super().default(o)  # 対応外は TypeError


print(json.dumps({"at": datetime(2026, 9, 28, 9, 0), "total": Decimal("1980.00")}, cls=AppEncoder))

実行結果:

{"at": "2026-09-28T09:00:00", "total": "1980.00"}

6. 読み込み時に型を戻す(parse_float・object_hook・object_pairs_hook)

import json
from datetime import datetime
from decimal import Decimal

text = '{"total": 0.1, "ordered_at": "2026-09-28T10:30:00+09:00", "id": 1, "id": 2}'

print(json.loads(text))                               # 重複キーは後勝ち
print(json.loads(text, parse_float=Decimal)["total"] * 3)


def restore(obj):                                     # 日時文字列を戻す
    for k, v in obj.items():
        if k.endswith("_at") and isinstance(v, str):
            obj[k] = datetime.fromisoformat(v)
    return obj

print(json.loads(text, object_hook=restore)["ordered_at"])


def no_dup(pairs):                                    # 重複キーをエラーに
    keys = [k for k, _ in pairs]
    if len(keys) != len(set(keys)):
        raise ValueError(f"重複キーがあります: {keys}")
    return dict(pairs)

try:
    json.loads(text, object_pairs_hook=no_dup)
except ValueError as e:
    print(e)

実行結果:

{'total': 0.1, 'ordered_at': '2026-09-28T10:30:00+09:00', 'id': 2}
0.3
2026-09-28 10:30:00+09:00
重複キーがあります: ['total', 'ordered_at', 'id', 'id']
  • parse_float=Decimal で 0.1 * 3 が 0.3 のまま計算できます(float なら 0.30000000000000004)。
  • object_hook はすべての object ごとに呼ばれるため、入れ子の中の日時も戻ります。
  • 重複キーは標準では後の値で黙って上書きされます。外部データを厳密にチェックするなら object_pairs_hook を使います。

7. JSONDecodeError のメッセージ別の原因と直し方

json.JSONDecodeError は ValueError のサブクラスで、msg・lineno(行)・colno(列)・pos(文字位置)を持っています。

import json

cases = {
    "シングルクォート": "{'id': 1}",
    "末尾のカンマ": '{"id": 1, "qty": 2,}',
    "True(Python表記)": '{"active": True}',
    "空文字": "",
    "コメント": '{"id": 1 // メモ\n}',
    "2つのJSONが連続": '{"id": 1}\n{"id": 2}',
}
for label, text in cases.items():
    try:
        json.loads(text)
    except json.JSONDecodeError as e:
        print(f"{label}: {e.msg} | 行{e.lineno} 列{e.colno}")

実行結果:

シングルクォート: Expecting property name enclosed in double quotes | 行1 列2
末尾のカンマ: Illegal trailing comma before end of object | 行1 列19
True(Python表記): Expecting value | 行1 列12
空文字: Expecting value | 行1 列1
コメント: Expecting ',' delimiter | 行1 列10
2つのJSONが連続: Extra data | 行2 列1

末尾のカンマは 3.13 から専用のメッセージになりました。3.12.3 では Expecting property name enclosed in double quotes(列20)と表示されます。

メッセージ よくある原因 直し方
Expecting property name enclosed in double quotes ' で囲んだ、str(dict) の結果を渡した 作る側で json.dumps() を使う
Illegal trailing comma before end of object 最後の要素の後ろに , カンマを消す
Expecting value(列1) 空ファイル、HTMLのエラーページを受け取った repr() で中身を確認。APIはステータスコードを先に確認
Expecting value(途中) True None などPythonの書き方 true null にする
Expecting ',' delimiter コメント、カンマ抜け JSONはコメント不可
Extra data JSON Lines を json.load() で読んだ 1行ずつ読む(レシピ8)
Unexpected UTF-8 BOM (decode using utf-8-sig) BOM付きUTF-8のファイル encoding="utf-8-sig" で開く

壊れた場所を見やすく表示する読み込み関数です。utf-8-sig はBOMがなくても読めるので、出どころがわからないファイルに向きます。

import json
from pathlib import Path

Path("broken.json").write_text('{\n  "id": 1,\n  "qty": 3,\n}\n', encoding="utf-8")


def load_json(path):
    text = Path(path).read_text(encoding="utf-8-sig")  # BOM付きも読める
    try:
        return json.loads(text)
    except json.JSONDecodeError as e:
        line = text.splitlines()[e.lineno - 1]
        print(f"{path}:{e.lineno}:{e.colno}: {e.msg}\n  {line}\n  {' ' * (e.colno - 1)}^")
        raise

try:
    load_json("broken.json")
except json.JSONDecodeError:
    pass

実行結果:

broken.json:3:11: Illegal trailing comma before end of object
    "qty": 3,
            ^

8. JSON Lines(.jsonl)を読み書きする

JSON Lines は「1行に1つのJSON」を並べた形式です。 追記が1行で済み、1行ずつ読めるため、ログや大量データに向きます。

import json

with open("events.jsonl", "w", encoding="utf-8") as f:
    for e in [{"type": "order", "qty": 3}, {"type": "cancel"}, {"type": "order", "qty": 1}]:
        f.write(json.dumps(e, ensure_ascii=False) + "\n")   # indentは付けない

total = 0
with open("events.jsonl", encoding="utf-8") as f:
    for lineno, line in enumerate(f, 1):
        if line.strip():
            rec = json.loads(line)
            total += rec.get("qty", 0)
print("合計:", total)

実行結果:

合計: 4

indent を付けると1件が複数行になり、JSON Lines として読めなくなります。 追記は "a" モードで同じように1行書くだけです。

9. NaN・Infinity の扱い(allow_nan)

JSONの仕様(RFC 8259)に NaN と Infinity はありませんが、Pythonの json は既定で出力も読み込みもします。

import json
data = {"avg": float("nan"), "max": float("inf")}
print(json.dumps(data))
try:
    json.dumps(data, allow_nan=False)
except ValueError as e:
    print("ValueError:", e)
print(json.loads('{"v": NaN}'))

実行結果:

{"avg": NaN, "max": Infinity}
ValueError: Out of range float values are not JSON compliant: nan
{'v': nan}

他の言語のパーサーでは読めないことがあるため、外部に渡すデータは allow_nan=False にし、事前に None(null)へ置き換えると安全です。読み込み側で拒否するには parse_constant= に例外を出す関数を渡します。

10. 大きいJSONファイルを読む(ijson の紹介)

json.load() はファイル全体を一度にメモリへ載せます。 巨大なファイルは、元データを JSON Lines にするか、少しずつ読める外部ライブラリ ijson を使います。

import ijson

with open("big.json", "rb") as f:
    for rec in ijson.items(f, "orders.item"):   # orders 配列の各要素
        ...

約5MB・20万件の配列で tracemalloc のピークを比べたところ、json.load() は 49.2MB、ijson.items() は 1.0MB でした(ijson 3.5.1)。処理時間は標準の json が速いことが多いので、メモリに収まるなら json.load() で十分です。

11. ターミナルで整形・検証する(python -m json)

Python 3.14 から python -m json でJSONを整形・検証できます(python -m json.tool も引き続き使えます)。3.14 では出力が色付きになり、環境変数 NO_COLOR や PYTHON_COLORS で制御できます。

echo '{"item":"ペン","qty":3}' > item.json
python3.14 -m json --no-ensure-ascii --indent 2 item.json
python3.14 -m json --compact --no-ensure-ascii item.json
echo '{"a":1,}' | python3.14 -m json; echo "exit=$?"

実行結果(NO_COLOR=1 で実行):

{
  "item": "ペン",
  "qty": 3
}
{"item":"ペン","qty":3}
Illegal trailing comma before end of object: line 1 column 7 (char 6)
exit=1

壊れたJSONは終了コード1になるため、CIの構文チェックにも使えます。Python 3.11 で python3.11 -m json とすると No module named json.__main__; 'json' is a package and cannot be directly executed になるので、json.tool を使います。

オプション 意味
--sort-keys キーを並べ替える
--no-ensure-ascii 日本語をそのまま出す
--indent N / --tab インデント幅/タブ
--no-indent / --compact 1行(空白あり/なし)
--json-lines 1行ずつ別のJSONとして読む

12. json・orjson・pydantic の選び方

観点 json(標準) orjson pydantic
導入 不要 uv add orjson uv add pydantic
dumps の戻り値 str bytes ―(model_dump_json() は str)
datetime・dataclass default= が必要 そのまま出力できる 型定義どおり変換
型の検証 なし なし あり
向いている場面 設定・小〜中規模データ 大量のJSONを速く変換 APIの入出力、外部データの検証

orjson 3.12.0 では orjson.dumps({"item": "ペン", "at": datetime(2026, 9, 28, 10, 30)}) が default= なしで bytes を返し、.decode() すると {"item":"ペン","at":"2026-09-28T10:30:00"} でした。pydantic 2.13.5 では "qty": "3" を int 型のフィールドに渡すと 3 に変換され、"たくさん" なら 1 validation error for Order で止まりました。まず標準の json、速度が必要なら orjson、検証が必要なら pydantic と段階的に選ぶとよいでしょう。

よくある質問

Q. print(dict) の出力をJSONとして読むとエラーになるのはなぜですか? A. print や str() はPythonの表記(シングルクォート、True、None)で出力するため、JSONではありません。JSONが必要なら必ず json.dumps() を使います。

Q. datetime を保存して、読み込むときに datetime に戻せますか? A. 書き出しは default= で isoformat() にし、読み込みは object_hook= で datetime.fromisoformat() を使えば戻せます(レシピ5・6)。JSONに日時の型はないため、どのキーが日時かのルールを決めておくのがポイントです。

Q. JSONにコメントを書けますか? A. 標準のJSONには書けません。人が編集する設定ファイルなら、コメントが書けて標準ライブラリの tomllib(3.11〜)で読める TOML も選択肢です。

まとめ

  • ファイルは load / dump、文字列は loads / dumps。open() には encoding="utf-8"
  • 日本語をそのまま出すなら ensure_ascii=False、差分を見るなら indent=2, sort_keys=True
  • datetime・Decimal・set・dataclass は default= で変換し、対応外は TypeError のまま残す
  • 読み込み側は parse_float=Decimal・object_hook・object_pairs_hook で型と品質を整える
  • JSONDecodeError の lineno / colno で壊れた場所を特定し、メッセージ別の表で直す
  • 数値キーは文字列になる、NaN は既定で出力されるの2点は読み戻しで確認する
  • 3.14 は python -m json と、位置がわかる TypeError が加わった

本記事の内容は、ご自由にお使いください。
ご利用の際は、出典として本ページへのリンクを記載いただけますようお願いします。

(記載例)出典:株式会社RJC「Python JSON 読み書き|コピペレシピと実行結果・3.14対応」

株式会社RJCでは、データの受け渡しでつまずきやすいポイントを、実際に動かして確かめながらチームで共有しています。こうした知識を共有しながら、日々技術を磨いています。

株式会社RJC ― SI事業・SES事業・AI駆動開発。RJCは一緒に成長を楽しめる会社です。

WE ARE HIRING

RJCで一緒に開発しながら、
成長を楽しみませんか?

RJCは、Web・モバイル・AIを活用した開発プロジェクトで、テックリードやPM・PMOも活躍するシステム開発会社です。会社を知る、待遇を確かめる、話を聞いてみる。気になるところ見てみてください!

  • 127日年間休日
  • 12時間平均残業時間
  • 毎日ガチャ遊びココロも大切にする福利厚生。アマギフなどの賞品ラインナップ!

ほかにも、チケットレストラン、書籍読み放題、2年ごとの慰労報奨(休暇 or 金一封)、11期連続の黒字決算。

ABOUT RJC RJCがどんな会社か知る 考え方、研修、働き方、福利厚生、社員の前職まで。RJCのことが丸わかり! RJC丸わかりページへ JOB DESCRIPTION 仕事内容・待遇を見てみる 仕事内容、給与・待遇、選考の流れ。経験者も未経験も!応募前に知りたいこと、まとめました! 募集要項を見る ENTRY エントリーする エントリーは1〜2分・履歴書不要です。まずは話を聞いてみたい、という方でも歓迎です! エントリーフォームへ

RJCで一緒に開発しながら、 成長を楽しみませんか?