PythonでJSONを扱う方法|読み込み・書き込み・変換を徹底解説
JSONは、Web API、設定ファイル、システム間のデータ交換などで広く使われるテキスト形式です。正式名称はJavaScript Object Notationですが、JavaScript専用ではなく、Python、Java、C#、PHPなど多くの言語で利用できます。RFC 8259では、JSONは軽量でテキストベースの言語非依存なデータ交換形式として定義されています。
Pythonでは、標準ライブラリのjsonモジュールを使って、JSON文字列を辞書やリストへ変換したり、PythonのデータをJSON文字列やファイルへ書き出したりできます。外部パッケージを追加しなくても基本的なJSON処理を実装できます。
本記事では、loads、dumps、load、dumpの基本から、日本語、ファイル操作、Web API、独自クラス、数値精度、エラー処理、大容量データまで順番に解説します。
1. JSONの基本構造を理解する
JSONをPythonで扱う前に、JSONが表現できる値と記述規則を理解する必要があります。Pythonの辞書に似ていますが、同じものではありません。
1.1 JSONとは
JSONは、構造化されたデータをテキストとして表現する形式です。JSONの値には、オブジェクト、配列、文字列、数値、true、false、nullがあります。
オブジェクトは波括弧、配列は角括弧で表します。オブジェクト内では、名前と値をコロンで結び、複数の項目をカンマで区切ります。
JSONオブジェクトの例
{
"name": "山田",
"age": 28,
"active": true
}
1.2 JSONオブジェクトと配列
JSONオブジェクトは、名前と値の組み合わせを持つ構造です。Pythonへ変換すると、通常はdictになります。
JSON配列は、値を順番に並べた構造です。Pythonではlistへ変換されます。JSONの配列は順序を持つ値の並びとして定義されています。
JSON配列の例
[
{
"id": 1,
"name": "商品A"
},
{
"id": 2,
"name": "商品B"
}
]
1.3 JSONの記述規則
JSONのプロパティ名と文字列は、二重引用符で囲む必要があります。Pythonの辞書で使える一重引用符は、JSONでは使用できません。
末尾のカンマやコメントも、標準的なJSONでは認められません。True、False、Noneではなく、小文字のtrue、false、nullを使用します。
{
"name": "Python",
"enabled": true,
"description": null
}
次の記述はPythonの辞書としては有効ですが、JSONとしては無効です。
{
'name': 'Python',
'enabled': True,
}
1.4 JSONとPythonの型対応
Pythonのjsonモジュールは、JSONの値をPythonの基本型へ変換します。オブジェクトは辞書、配列はリスト、nullはNoneになります。
反対方向では、PythonのタプルもJSON配列へ変換されます。ただしJSONから読み戻すと、タプルではなくリストになります。
| JSON | Python |
|---|---|
| object | dict |
| array | list |
| string | str |
| 整数 | int |
| 小数 | float |
true | True |
false | False |
null | None |
1.5 jsonモジュールを読み込む
PythonでJSONを扱うには、標準ライブラリのjsonをインポートします。追加インストールは不要です。
import json
基本的な処理は、次の四つの関数を中心に行います。
| 関数 | 役割 |
|---|---|
json.dumps() | Pythonの値をJSON文字列へ変換 |
json.loads() | JSON文字列をPythonの値へ変換 |
json.dump() | Pythonの値をJSONファイルへ書き込み |
json.load() | JSONファイルをPythonの値として読み込み |
2. json.dumpsでJSON文字列へ変換する
json.dumpsは、Pythonの辞書やリストをJSON形式の文字列へ変換する関数です。API送信データの作成や、ログ・キャッシュ用の文字列生成に利用できます。
2.1 辞書をJSON文字列へ変換する
Pythonの辞書をjson.dumpsへ渡すと、JSON形式のstrが返されます。
import json
user = {
"name": "山田",
"age": 28,
"active": True,
}
json_text = json.dumps(user)
print(json_text)
print(type(json_text))
実行結果は次のようになります。
{"name": "\u5c71\u7530", "age": 28, "active": true}
<class 'str'>
2.2 リストをJSON文字列へ変換する
リスト全体をJSON配列へ変換することもできます。
import json
languages = ["Python", "Java", "C++"]
json_text = json.dumps(languages)
print(json_text)
実行結果は次のとおりです。
["Python", "Java", "C++"]
辞書を要素として持つリストも変換できます。
products = [
{"id": 1, "name": "商品A"},
{"id": 2, "name": "商品B"},
]
json_text = json.dumps(products)
2.3 入れ子のデータを変換する
JSONでは、オブジェクトの中に配列を持たせたり、配列の中にオブジェクトを入れたりできます。
Python側でも、辞書とリストを組み合わせて入れ子構造を表現します。
import json
order = {
"id": 1001,
"customer": {
"id": 10,
"name": "山田",
},
"items": [
{
"product": "商品A",
"quantity": 2,
},
{
"product": "商品B",
"quantity": 1,
},
],
}
json_text = json.dumps(order)
print(json_text)
入れ子が深くなりすぎると処理や検証が難しくなるため、APIや設定ファイルでは必要以上に複雑な構造を作らないことも重要です。
2.4 変換できるPythonの型
標準のJSONエンコーダーは、辞書、リスト、タプル、文字列、整数、小数、真偽値、Noneなどを変換できます。整数型または浮動小数点型を継承したenumも対応します。
import json
data = {
"text": "Python",
"number": 100,
"rate": 0.8,
"enabled": True,
"value": None,
"items": ("A", "B"),
}
print(json.dumps(data))
タプルはJSON配列になります。読み戻したときはリストになるため、元の型が完全には保持されません。
2.5 変換できない型で発生するエラー
datetime、Decimal、set、独自クラスなどは、初期状態ではJSONへ変換できません。
import json
from datetime import datetime
data = {
"created_at": datetime.now(),
}
json.dumps(data)
このコードでは、次のようなTypeErrorが発生します。
TypeError: Object of type datetime is not JSON serializable
変換できない型は、文字列、数値、辞書、リストなどJSONで表現できる型へ変換してから渡します。default関数や独自エンコーダーを使う方法もあります。
3. json.loadsでJSON文字列を読み込む
json.loadsは、JSON形式の文字列をPythonの辞書、リスト、文字列、数値などへ変換します。
3.1 JSON文字列を辞書へ変換する
JSONオブジェクトを含む文字列をjson.loadsへ渡すと、辞書として取得できます。
import json
json_text = """
{
"name": "山田",
"age": 28,
"active": true
}
"""
user = json.loads(json_text)
print(user)
print(type(user))
print(user["name"])
実行結果は次のとおりです。
{'name': '山田', 'age': 28, 'active': True}
<class 'dict'>
山田
3.2 JSON配列をリストへ変換する
JSON配列を読み込むと、Pythonのリストになります。
import json
json_text = """
[
"Python",
"Java",
"C++"
]
"""
languages = json.loads(json_text)
print(languages)
print(type(languages))
配列内にJSONオブジェクトがある場合は、辞書を要素として持つリストになります。
json_text = """
[
{"id": 1, "name": "商品A"},
{"id": 2, "name": "商品B"}
]
"""
products = json.loads(json_text)
print(products[0]["name"])
3.3 true・false・nullの変換
JSONのtrue、false、nullは、PythonではTrue、False、Noneになります。
import json
json_text = """
{
"enabled": true,
"deleted": false,
"description": null
}
"""
data = json.loads(json_text)
print(data["enabled"] is True)
print(data["deleted"] is False)
print(data["description"] is None)
Pythonのコード内でJSON文字列を直接書く場合は、大文字と小文字を間違えないようにします。
3.4 bytesとbytearrayを読み込む
json.loadsは、strだけでなく、JSON文書を含むbytesとbytearrayも受け取れます。入力はUTF-8、UTF-16またはUTF-32として解釈されます。
import json
json_bytes = b'{"name": "Python", "version": 3}'
data = json.loads(json_bytes)
print(data)
HTTPレスポンスやファイル読み込みでバイト列を受け取った場合にも利用できます。ただし、文字コードが明確なら、自分でdecodeしてから処理すると意図が分かりやすくなる場合があります。
3.5 読み込み後に型を確認する
json.loadsが成功しても、期待した構造とは限りません。API仕様ではオブジェクトを期待していても、配列や文字列が返される可能性があります。
import json
from typing import Any
json_text = '{"name": "山田"}'
data: Any = json.loads(json_text)
if not isinstance(data, dict):
raise ValueError("JSONの最上位はオブジェクトである必要があります")
name = data.get("name")
if not isinstance(name, str):
raise ValueError("nameは文字列である必要があります")
print(name)
JSONの構文確認と、業務上必要な項目・型の確認は別の処理です。
4. JSONファイルを読み書きする
文字列ではなく.jsonファイルを扱う場合は、json.dumpとjson.loadを使用できます。
4.1 JSONファイルへ書き込む
json.dumpは、Pythonの値をファイル形式のオブジェクトへ書き込みます。JSONモジュールは文字列を生成するため、テキストモードでファイルを開きます。
import json
settings = {
"language": "ja",
"theme": "dark",
"notifications": True,
}
with open(
"settings.json",
"w",
encoding="utf-8",
) as file:
json.dump(
settings,
file,
ensure_ascii=False,
indent=2,
)
with文を使うと、処理終了時にファイルが自動的に閉じられます。
4.2 JSONファイルを読み込む
json.loadへ読み込み用ファイルを渡すと、JSON文書をPythonの値へ変換できます。
import json
with open(
"settings.json",
"r",
encoding="utf-8",
) as file:
settings = json.load(file)
print(settings["theme"])
JSONファイルの最上位が配列であればリスト、オブジェクトであれば辞書になります。
4.3 pathlibを使って読み書きする
pathlib.Pathを使うと、ファイルパスをオブジェクトとして扱えます。
import json
from pathlib import Path
from typing import Any
path = Path("data") / "settings.json"
settings = {
"language": "ja",
"theme": "light",
}
path.parent.mkdir(
parents=True,
exist_ok=True,
)
with path.open(
"w",
encoding="utf-8",
) as file:
json.dump(
settings,
file,
ensure_ascii=False,
indent=2,
)
def read_json(path: Path) -> Any:
with path.open(
"r",
encoding="utf-8",
) as file:
return json.load(file)
複数の場所でJSONファイルを扱う場合は、読み書き処理を関数へまとめると設定を統一できます。
4.4 上書きと追記を使い分ける
ファイルモード"w"で開くと、既存内容は上書きされます。更新前の内容を残したい場合は、バックアップや一時ファイルを利用します。
JSON文書へ単純に追記するために"a"モードを使うと、無効なJSONになる可能性があります。
# 1回目
{"id": 1}
# そのまま追記した結果
{"id": 1}{"id": 2}
複数データを一つのJSONファイルへ保存するなら、配列としてまとめて書き直すか、JSON Lines形式を検討します。
4.5 dumpを繰り返さない
JSONは、複数の独立した文書を自動的に区切るフレーム形式ではありません。同じファイルへjson.dumpを繰り返すと、通常は無効なJSONになります。
import json
with open(
"invalid.json",
"w",
encoding="utf-8",
) as file:
json.dump({"id": 1}, file)
json.dump({"id": 2}, file)
複数オブジェクトを保存する場合は、次のようにリストとして一回で書き込みます。
records = [
{"id": 1},
{"id": 2},
]
with open(
"records.json",
"w",
encoding="utf-8",
) as file:
json.dump(records, file)
5. 読みやすいJSONと日本語を出力する
json.dumpsとjson.dumpには、インデント、日本語、キー順、空白などを調整するオプションがあります。
5.1 indentで整形する
indentへ正の整数を指定すると、階層ごとに指定数の空白を入れて整形されます。
import json
data = {
"user": {
"name": "山田",
"roles": [
"ADMIN",
"USER",
],
}
}
json_text = json.dumps(
data,
indent=2,
)
print(json_text)
設定ファイルや確認用出力ではindent=2またはindent=4が読みやすいでしょう。通信量を抑えたい場合はインデントを付けません。
5.2 ensure_ascii=Falseで日本語を表示する
ensure_asciiの初期値はTrueで、非ASCII文字はUnicodeエスケープへ変換されます。Falseにすると、日本語をそのまま出力できます。
import json
data = {
"name": "山田",
"city": "東京",
}
print(
json.dumps(
data,
ensure_ascii=False,
)
)
実行結果は次のとおりです。
{"name": "山田", "city": "東京"}
ファイルへ保存するときは、encoding="utf-8"も併せて指定します。
5.3 sort_keysでキーを並べる
sort_keys=Trueを指定すると、辞書のキーを並べ替えて出力します。テスト結果の比較や差分確認で便利です。
import json
data = {
"z": 1,
"a": 2,
"m": 3,
}
json_text = json.dumps(
data,
sort_keys=True,
)
print(json_text)
表示順が業務上重要な場合は、単なるアルファベット順ではなく、データ構造や表示処理そのものを設計します。
5.4 separatorsで空白を削除する
separators=(",", ":")を指定すると、カンマやコロンの後ろにある空白を削除できます。最小サイズに近いJSON文字列を生成できます。
import json
data = {
"name": "Python",
"version": 3,
}
compact = json.dumps(
data,
ensure_ascii=False,
separators=(",", ":"),
)
print(compact)
実行結果は次のとおりです。
{"name":"Python","version":3}
API通信ではコンパクト形式、人が確認するファイルではインデント形式という使い分けができます。
5.5 出力設定を関数へまとめる
プロジェクト内でJSON出力の設定が異なると、ファイルやログの形式が統一されません。
共通関数を作り、日本語、インデント、厳格な数値処理などを統一できます。
import json
from typing import Any
def to_json(value: Any) -> str:
return json.dumps(
value,
ensure_ascii=False,
indent=2,
sort_keys=True,
allow_nan=False,
)
data = {
"name": "山田",
"score": 92,
}
print(to_json(data))
API送信用とファイル保存用で必要な形式が違う場合は、関数を分けます。
6. 入れ子のJSONを取得・更新する
実際のJSONには、辞書とリストが何層にも組み合わされた構造がよく登場します。
6.1 入れ子の値を取得する
辞書にはキー、リストにはインデックスを指定します。
data = {
"customer": {
"id": 10,
"name": "山田",
},
"orders": [
{
"id": 1001,
"amount": 3000,
},
{
"id": 1002,
"amount": 4500,
},
],
}
customer_name = data["customer"]["name"]
first_order_id = data["orders"][0]["id"]
print(customer_name)
print(first_order_id)
構造が深い場合は、途中の値を変数へ分けると読みやすくなります。
6.2 getで存在しないキーへ対応する
辞書へ存在しないキーを角括弧で指定すると、KeyErrorが発生します。
任意項目を取得する場合は、getを使用できます。
user = {
"name": "山田",
}
email = user.get("email")
language = user.get(
"language",
"ja",
)
print(email)
print(language)
必須項目まで無条件にgetで取得すると、データ不足を見逃す可能性があります。必須項目と任意項目を分けましょう。
6.3 入れ子の値を更新する
辞書とリストは変更可能なオブジェクトです。読み込んだ後、通常のPythonデータとして値を更新できます。
data = {
"profile": {
"name": "山田",
"age": 28,
}
}
data["profile"]["age"] = 29
data["profile"]["city"] = "東京"
print(data)
更新した内容をファイルへ反映するには、再度json.dumpで書き込みます。
6.4 要素を追加・削除する
JSON配列から変換されたリストには、append、removeなどを使用できます。
data = {
"tags": [
"Python",
"JSON",
],
"temporary": True,
}
data["tags"].append("API")
del data["temporary"]
print(data)
削除対象が存在するか不明な場合は、辞書のpopへ初期値を指定できます。
removed = data.pop(
"unknown",
None,
)
6.5 原本を残して変更する
読み込んだデータを直接変更すると、元の値も失われます。変更前後を比較したい場合は、深いコピーを作ります。
from copy import deepcopy
original = {
"profile": {
"name": "山田",
"age": 28,
}
}
updated = deepcopy(original)
updated["profile"]["age"] = 29
print(original["profile"]["age"])
print(updated["profile"]["age"])
単純なdict.copy()では、入れ子になった辞書やリストが共有されるため注意が必要です。
7. JSONのエラーを処理する
外部ファイルやAPIから受け取るJSONは、必ず正しいとは限りません。構文、文字コード、ファイル、データ型をそれぞれ確認します。
7.1 JSONDecodeErrorを処理する
不正なJSONをjson.loadsまたはjson.loadで読み込むと、json.JSONDecodeErrorが発生します。
import json
json_text = """
{
"name": "山田",
}
"""
try:
data = json.loads(json_text)
except json.JSONDecodeError as error:
print(f"JSONが不正です: {error}")
この例では末尾のカンマが原因で読み込みに失敗します。
7.2 行番号と列番号を表示する
JSONDecodeErrorには、メッセージ、位置、行番号、列番号などの属性があります。
import json
json_text = """
{
"name": "山田",
"age": 28,
}
"""
try:
json.loads(json_text)
except json.JSONDecodeError as error:
print(f"内容: {error.msg}")
print(f"行: {error.lineno}")
print(f"列: {error.colno}")
print(f"位置: {error.pos}")
利用者へ表示するメッセージには、JSON文書全体を含めない方が安全です。個人情報や認証情報が含まれる可能性があります。
7.3 ファイル関連の例外を処理する
JSONファイルを読む処理では、FileNotFoundError、PermissionError、UnicodeDecodeErrorなども発生します。
import json
from pathlib import Path
from typing import Any
def load_json_file(path: Path) -> Any:
try:
with path.open(
"r",
encoding="utf-8",
) as file:
return json.load(file)
except FileNotFoundError as error:
raise RuntimeError(
f"JSONファイルが見つかりません: {path}"
) from error
except PermissionError as error:
raise RuntimeError(
f"JSONファイルを読み取れません: {path}"
) from error
except UnicodeDecodeError as error:
raise RuntimeError(
f"文字コードがUTF-8ではありません: {path}"
) from error
except json.JSONDecodeError as error:
raise RuntimeError(
f"JSONの形式が不正です: "
f"{error.lineno}行{error.colno}列"
) from error
異なる原因をすべて同じ例外として隠すと、問題の調査が難しくなります。
7.4 必須項目と型を検証する
JSONとして正しいだけでは、アプリケーションで利用できるとは限りません。
from typing import Any
def validate_user(data: Any) -> dict[str, Any]:
if not isinstance(data, dict):
raise ValueError(
"利用者データはオブジェクトである必要があります"
)
if "id" not in data:
raise ValueError(
"idがありません"
)
if "name" not in data:
raise ValueError(
"nameがありません"
)
if not isinstance(data["id"], int):
raise ValueError(
"idは整数である必要があります"
)
if not isinstance(data["name"], str):
raise ValueError(
"nameは文字列である必要があります"
)
return data
データ項目が多い場合は、Pydantic、JSON Schemaなどの検証手段も候補になります。
7.5 重複キーを検出する
Pythonの標準デコーダーは、同じ名前が複数回現れた場合、最後の値だけを残します。
import json
data = json.loads(
'{"id": 1, "id": 2}'
)
print(data)
結果は{"id": 2}です。重複をエラーにしたい場合は、object_pairs_hookを利用できます。
import json
from typing import Any
def reject_duplicate_keys(
pairs: list[tuple[str, Any]],
) -> dict[str, Any]:
result: dict[str, Any] = {}
for key, value in pairs:
if key in result:
raise ValueError(
f"JSONキーが重複しています: {key}"
)
result[key] = value
return result
data = json.loads(
'{"id": 1, "name": "山田"}',
object_pairs_hook=reject_duplicate_keys,
)
8. Web APIでJSONを送受信する
Web APIでは、要求本文と応答本文にJSONがよく使われます。HTTPとJSONは別の仕組みであり、通信エラーとJSONエラーを分けて処理します。
8.1 application/jsonを理解する
JSON文書のメディアタイプはapplication/jsonです。RFC 8259にも登録情報が示されています。
JSONを送信する場合は、通常Content-Type: application/jsonを指定します。応答をJSONとして処理する場合は、サーバーが返したContent-Typeも確認すると安全です。
8.2 GETでJSONを取得する
Python標準ライブラリのurllib.request.urlopenを使ってURLを開き、応答本文を読み込めます。
import json
from typing import Any
from urllib.request import urlopen
def fetch_json(url: str) -> Any:
with urlopen(
url,
timeout=10,
) as response:
content_type = response.headers.get_content_type()
if content_type != "application/json":
raise ValueError(
f"JSON以外の応答です: {content_type}"
)
body = response.read()
return json.loads(body)
外部APIでは、必ずタイムアウトを指定します。応答が返らない場合に処理が長時間停止することを防げます。
8.3 POSTでJSONを送信する
json.dumpsで作成した文字列をUTF-8のバイト列へ変換し、HTTP要求本文として送信します。
import json
from typing import Any
from urllib.request import Request, urlopen
def post_json(
url: str,
payload: dict[str, Any],
) -> Any:
body = json.dumps(
payload,
ensure_ascii=False,
allow_nan=False,
).encode("utf-8")
request = Request(
url,
data=body,
method="POST",
headers={
"Content-Type": "application/json",
"Accept": "application/json",
},
)
with urlopen(
request,
timeout=10,
) as response:
response_body = response.read()
return json.loads(response_body)
認証トークンやAPIキーを扱う場合は、コードへ直接書き込まず、環境変数や秘密情報管理機能を利用します。
8.4 HTTPエラーとJSONエラーを分ける
urllib.requestでは、HTTPエラーにHTTPError、接続などの問題にURLErrorが使われます。
import json
from urllib.error import HTTPError, URLError
from urllib.request import urlopen
try:
with urlopen(
"https://example.com/api/users",
timeout=10,
) as response:
data = json.loads(
response.read()
)
except HTTPError as error:
print(
f"HTTPエラー: {error.code}"
)
except URLError as error:
print(
f"通信エラー: {error.reason}"
)
except json.JSONDecodeError as error:
print(
f"応答JSONが不正です: {error}"
)
通信が成功しても、HTMLのエラーページや空の応答が返される可能性があります。
8.5 APIレスポンスを検証する
APIから辞書が返されても、必要なデータが含まれているとは限りません。
from typing import Any
def extract_users(
data: Any,
) -> list[dict[str, Any]]:
if not isinstance(data, dict):
raise ValueError(
"API応答の最上位がオブジェクトではありません"
)
users = data.get("users")
if not isinstance(users, list):
raise ValueError(
"usersが配列ではありません"
)
for index, user in enumerate(users):
if not isinstance(user, dict):
raise ValueError(
f"users[{index}]がオブジェクトではありません"
)
return users
外部APIの仕様変更に備え、必須項目と任意項目を明確に分けます。
9. datetime・Decimal・setをJSONへ変換する
JSONが直接表現できないPython型は、互換性の高い文字列、数値、配列などへ変換します。
9.1 datetimeをISO形式へ変換する
日時は、ISO 8601形式の文字列として表現する方法が一般的です。
import json
from datetime import datetime, timezone
created_at = datetime.now(
timezone.utc
)
data = {
"created_at": created_at.isoformat(),
}
json_text = json.dumps(data)
print(json_text)
読み込み後は、datetime.fromisoformatで戻せます。
parsed = json.loads(json_text)
created_at = datetime.fromisoformat(
parsed["created_at"]
)
タイムゾーン情報を持たない日時は解釈が曖昧になるため、APIではUTCまたはオフセットを含める設計が安全です。
9.2 Decimalを文字列へ変換する
Decimalは、そのままでは標準のJSONエンコーダーで変換できません。
金額など精度を失いたくない値は、文字列へ変換する方法があります。
import json
from decimal import Decimal
data = {
"price": str(
Decimal("1234.56")
)
}
json_text = json.dumps(data)
print(json_text)
JSONの数値として出力すると、受信側が浮動小数点数へ変換し、精度を失う可能性があります。送受信するシステム間で形式を決めます。
9.3 setをリストへ変換する
JSONには集合型がないため、setはリストへ変換します。
import json
data = {
"roles": list({
"ADMIN",
"USER",
})
}
print(json.dumps(data))
集合には順序がないため、出力を安定させたい場合は並べ替えます。
data = {
"roles": sorted({
"ADMIN",
"USER",
})
}
読み戻した後に重複を許可したくない場合は、再びsetへ変換します。
9.4 dataclassを辞書へ変換する
dataclasses.asdictを使うと、データクラスと入れ子のデータクラスを辞書へ変換できます。
import json
from dataclasses import asdict, dataclass
@dataclass
class User:
id: int
name: str
active: bool
user = User(
id=1,
name="山田",
active=True,
)
json_text = json.dumps(
asdict(user),
ensure_ascii=False,
)
print(json_text)
フィールドにdatetimeやDecimalが含まれる場合は、追加の変換処理が必要です。
9.5 Enum・Path・UUIDを変換する
enumは通常、valueまたはnameをJSONへ保存します。PathやUUIDは文字列へ変換できます。
import json
from enum import Enum
from pathlib import Path
from uuid import UUID
class Status(Enum):
ACTIVE = "active"
INACTIVE = "inactive"
data = {
"status": Status.ACTIVE.value,
"path": str(
Path("data") / "users.json"
),
"request_id": str(
UUID(
"12345678-1234-5678-1234-567812345678"
)
),
}
print(
json.dumps(
data,
ensure_ascii=False,
)
)
文字列から元の型へ戻す処理も、入力検証を含めて用意します。
10. 独自エンコーダーとデコーダーを作る
変換対象の型が複数ある場合は、共通の変換関数やJSONEncoderを用意できます。
10.1 default関数を使用する
json.dumpsのdefaultには、標準では変換できないオブジェクトをJSON互換型へ変える関数を指定できます。
import json
from datetime import date, datetime
from decimal import Decimal
from pathlib import Path
from typing import Any
def json_default(value: Any) -> Any:
if isinstance(
value,
(datetime, date),
):
return value.isoformat()
if isinstance(value, Decimal):
return str(value)
if isinstance(value, set):
return sorted(value)
if isinstance(value, Path):
return str(value)
raise TypeError(
f"{type(value).__name__}はJSONへ変換できません"
)
data = {
"created_at": datetime.now(),
"price": Decimal("1200.50"),
"tags": {"Python", "JSON"},
}
json_text = json.dumps(
data,
default=json_default,
ensure_ascii=False,
)
print(json_text)
対応していない型では必ずTypeErrorを発生させます。誤って文字列表現へ変換すると、問題を見逃す可能性があります。
10.2 JSONEncoderを継承する
複数箇所で同じエンコード処理を使う場合は、json.JSONEncoderを継承できます。
import json
from datetime import datetime
from decimal import Decimal
from typing import Any
class ApplicationJSONEncoder(
json.JSONEncoder
):
def default(
self,
value: Any,
) -> Any:
if isinstance(
value,
datetime,
):
return value.isoformat()
if isinstance(
value,
Decimal,
):
return str(value)
if isinstance(value, set):
return sorted(value)
return super().default(value)
data = {
"created_at": datetime.now(),
"price": Decimal("500.25"),
}
json_text = json.dumps(
data,
cls=ApplicationJSONEncoder,
)
標準エンコーダーを拡張する場合、対応できない型はsuper().defaultへ渡します。
10.3 型情報をJSONへ含める
JSONから独自型へ戻したい場合は、型を識別する項目を含める方法があります。
import json
from datetime import datetime
from typing import Any
def json_default(value: Any) -> Any:
if isinstance(value, datetime):
return {
"__type__": "datetime",
"value": value.isoformat(),
}
raise TypeError(
f"未対応の型です: {type(value).__name__}"
)
data = {
"created_at": datetime.now(),
}
json_text = json.dumps(
data,
default=json_default,
)
外部から受け取った型名をそのままクラス生成へ利用することは避けます。許可した型だけを明示的に変換します。
10.4 object_hookで独自型へ戻す
object_hookは、JSONオブジェクトが辞書へ変換されるたびに呼び出されます。戻り値は元の辞書の代わりに使用されます。
import json
from datetime import datetime
from typing import Any
def object_hook(
value: dict[str, Any],
) -> Any:
if value.get("__type__") == "datetime":
date_value = value.get("value")
if not isinstance(
date_value,
str,
):
raise ValueError(
"datetimeのvalueが不正です"
)
return datetime.fromisoformat(
date_value
)
return value
json_text = """
{
"created_at": {
"__type__": "datetime",
"value": "2026-07-20T10:30:00+00:00"
}
}
"""
data = json.loads(
json_text,
object_hook=object_hook,
)
print(type(data["created_at"]))
object_hookはすべてのJSONオブジェクトへ適用されるため、識別条件を慎重に設計します。
10.5 データ変換層を分ける
独自クラスを直接JSONへ結び付けると、内部モデルと外部API形式が強く依存します。
実務では、内部オブジェクトとJSON用辞書を相互変換する専用関数やクラスを作る方法があります。
from dataclasses import dataclass
from typing import Any
@dataclass(frozen=True)
class Product:
id: int
name: str
price: int
def product_to_dict(
product: Product,
) -> dict[str, Any]:
return {
"id": product.id,
"name": product.name,
"price": product.price,
}
def product_from_dict(
value: dict[str, Any],
) -> Product:
product_id = value.get("id")
name = value.get("name")
price = value.get("price")
if not isinstance(product_id, int):
raise ValueError("idが不正です")
if not isinstance(name, str):
raise ValueError("nameが不正です")
if not isinstance(price, int):
raise ValueError("priceが不正です")
return Product(
id=product_id,
name=name,
price=price,
)
APIの項目名が変わっても、変換層だけで対応しやすくなります。
11. JSONの数値と厳格な形式を扱う
JSONの数値は、Python内では整数と浮動小数点数へ変換されます。ただし、ほかの言語やシステムと交換するときは範囲と精度へ注意が必要です。
11.1 小数をDecimalで読み込む
json.loadsのparse_floatへDecimalを指定すると、JSONの小数をfloatではなくDecimalへ変換できます。
import json
from decimal import Decimal
data = json.loads(
'{"price": 0.1}',
parse_float=Decimal,
)
print(data["price"])
print(type(data["price"]))
金額や精密な小数計算では、読み込み時点からDecimalを使う方法があります。
11.2 大きな整数に注意する
Pythonの整数は大きな値を扱えますが、JSONを受け取る別システムが同じ精度で扱えるとは限りません。
Python公式ドキュメントでも、多くの実装がJSON数値をIEEE 754の倍精度浮動小数点数へ変換するため、非常に大きな整数では相互運用上の注意が必要だと説明されています。
識別番号や口座番号など、計算しない大きな数値は文字列で送る設計も候補です。
data = {
"customer_id": "9007199254740993",
}
11.3 NaNとInfinityを禁止する
PythonのJSONエンコーダーは初期状態でNaN、Infinity、-Infinityを出力できますが、これらはJSON仕様上の有効な数値ではありません。
厳格なJSONを生成する場合は、allow_nan=Falseを指定します。
import json
import math
data = {
"score": math.nan,
}
try:
json.dumps(
data,
allow_nan=False,
)
except ValueError as error:
print(error)
外部システムへ送信するJSONでは、厳格な設定を使う方が安全です。
11.4 不正な特殊数値を拒否する
読み込み側では、parse_constantを指定してNaNやInfinityを拒否できます。
import json
from typing import NoReturn
def reject_constant(
value: str,
) -> NoReturn:
raise ValueError(
f"JSON仕様外の数値です: {value}"
)
data = json.loads(
'{"score": 10}',
parse_constant=reject_constant,
)
次の入力ではValueErrorになります。
json.loads(
'{"score": NaN}',
parse_constant=reject_constant,
)
11.5 非文字列キーに注意する
JSONオブジェクトのキーは常に文字列です。Pythonの辞書で整数などをキーにしても、JSON化すると文字列へ変換されます。
import json
original = {
1: "A",
2: "B",
}
json_text = json.dumps(original)
restored = json.loads(json_text)
print(original)
print(restored)
print(original == restored)
実行結果は次のようになります。
{1: 'A', 2: 'B'}
{'1': 'A', '2': 'B'}
False
JSONへ保存する辞書では、最初から文字列キーを使用すると混乱を防げます。
12. 大容量JSONとJSON Linesを扱う
通常のjson.loadは、JSON文書全体をPythonオブジェクトとして構築します。大容量データでは、ファイル形式や処理方法を見直す必要があります。
12.1 JSON全体を読み込む問題
巨大なJSON配列をjson.loadすると、ファイル内容と変換後のPythonオブジェクトがメモリを消費します。
import json
with open(
"large.json",
"r",
encoding="utf-8",
) as file:
records = json.load(file)
数MB程度なら問題にならなくても、数GBのデータでは処理できない可能性があります。データ量と利用環境を確認します。
12.2 JSON Lines形式を使用する
JSON Linesは、一行ごとに独立したJSON値を記録する形式です。標準的な一つのJSON配列とは異なります。
{"id": 1, "name": "商品A"}
{"id": 2, "name": "商品B"}
{"id": 3, "name": "商品C"}
一行ずつ処理できるため、大量のログ、イベント、機械学習データなどで便利です。
12.3 JSON Linesを書き込む
一件ずつjson.dumpsし、末尾へ改行を付けます。
import json
from pathlib import Path
from typing import Any
def write_json_lines(
path: Path,
records: list[dict[str, Any]],
) -> None:
with path.open(
"w",
encoding="utf-8",
) as file:
for record in records:
line = json.dumps(
record,
ensure_ascii=False,
allow_nan=False,
separators=(",", ":"),
)
file.write(line)
file.write("\n")
通常のJSON文書としてjson.loadすることはできません。一行ずつjson.loadsします。
12.4 JSON Linesを一行ずつ読む
import json
from collections.abc import Iterator
from pathlib import Path
from typing import Any
def read_json_lines(
path: Path,
) -> Iterator[dict[str, Any]]:
with path.open(
"r",
encoding="utf-8",
) as file:
for line_number, line in enumerate(
file,
start=1,
):
stripped = line.strip()
if not stripped:
continue
try:
value = json.loads(stripped)
except json.JSONDecodeError as error:
raise ValueError(
f"{line_number}行目のJSONが不正です"
) from error
if not isinstance(value, dict):
raise ValueError(
f"{line_number}行目がオブジェクトではありません"
)
yield value
ジェネレーターを使うことで、全件をリストへ保持せず処理できます。
12.5 ストリーミング用ライブラリを検討する
巨大な単一JSON配列を部分的に解析したい場合、標準のjsonだけでは扱いにくいことがあります。
データ形式を変更できるならJSON Linesへ変える方法が単純です。変更できない場合は、ストリーミングJSONパーサーや、Parquet・データベースなど別形式も検討します。
最適化では、先にファイルサイズ、メモリ使用量、解析時間を計測し、実際の問題に合う方法を選びます。
13. JSONを検証・整形・テストする
JSON処理では、コードだけでなく、コマンドライン検証、単体テスト、差分比較なども役立ちます。
13.1 コマンドラインでJSONを検証する
Python 3.14では、python -m jsonを使ってJSONの構文検証と整形表示ができます。従来のpython -m json.toolも引き続き利用できます。
python -m json settings.json
正しいJSONであれば整形して表示され、不正なJSONであればエラー位置が表示されます。
echo '{"name":"Python"}' | python -m json
13.2 JSONを別ファイルへ整形する
入力ファイルと出力ファイルを指定して、整形済みJSONを作成できます。
python -m json \
compact.json \
formatted.json
Python 3.13以前との互換性が必要な環境では、次の形式を利用できます。
python -m json.tool \
compact.json \
formatted.json
13.3 dumpsとloadsの往復をテストする
基本型だけで構成されたデータでは、JSON化して読み戻した値が期待どおりか確認します。
import json
def test_json_round_trip() -> None:
original = {
"name": "山田",
"age": 28,
"roles": [
"ADMIN",
"USER",
],
"active": True,
"description": None,
}
json_text = json.dumps(
original,
ensure_ascii=False,
)
restored = json.loads(
json_text
)
assert restored == original
タプル、非文字列キー、独自クラスなどは元と同じ型へ戻らないため、期待する変換結果を明示します。
13.4 出力を固定して比較する
テストやスナップショット比較では、sort_keys=Trueと一定のインデントを使用すると差分を安定させられます。
import json
from typing import Any
def stable_json(
value: Any,
) -> str:
return json.dumps(
value,
ensure_ascii=False,
indent=2,
sort_keys=True,
allow_nan=False,
)
ただし、キー順がデータの意味を表す仕様では、単なる並べ替えで問題を隠さないようにします。
13.5 不正データのテストを追加する
正常なJSONだけでなく、構文エラー、必須項目不足、型違い、重複キー、大きすぎる入力も確認します。
import json
import pytest
def parse_user(
json_text: str,
) -> dict[str, object]:
value = json.loads(json_text)
if not isinstance(value, dict):
raise ValueError(
"オブジェクトが必要です"
)
if not isinstance(
value.get("name"),
str,
):
raise ValueError(
"nameが必要です"
)
return value
def test_invalid_json() -> None:
with pytest.raises(
json.JSONDecodeError
):
parse_user(
'{"name": "山田",}'
)
def test_missing_name() -> None:
with pytest.raises(
ValueError
):
parse_user(
'{"id": 1}'
)
構文検証と業務データ検証を別々にテストすると、失敗原因を把握しやすくなります。
14. JSON処理を安全・保守しやすくする
JSONは簡単に扱えますが、外部入力、機密情報、巨大データ、形式変更などへ対応する設計が必要です。
14.1 信頼できないJSONのサイズを制限する
Python公式ドキュメントでは、悪意あるJSONが大量のCPUやメモリを消費する可能性があるため、解析対象のサイズ制限が推奨されています。
import json
from typing import Any
MAX_JSON_BYTES = 1_000_000
def loads_limited(
body: bytes,
) -> Any:
if len(body) > MAX_JSON_BYTES:
raise ValueError(
"JSONデータが大きすぎます"
)
return json.loads(body)
HTTPサーバーでは、Webフレームワークやプロキシ側の要求サイズ制限も設定します。
14.2 JSONをevalで読み込まない
JSON文字列をPythonのevalへ渡してはいけません。JSONとPythonの構文は異なり、任意コード実行の危険があります。
# 使用しない
data = eval(json_text)
必ずjson.loadsを使用します。
data = json.loads(json_text)
JSONはJavaScriptから派生した形式ですが、代入や関数呼び出しを含まないデータ形式として設計されています。
14.3 機密情報をログへ出さない
API応答や設定JSONには、パスワード、トークン、個人情報が含まれる可能性があります。
def mask_secrets(
data: dict[str, object],
) -> dict[str, object]:
masked = data.copy()
for key in (
"password",
"access_token",
"refresh_token",
"api_key",
):
if key in masked:
masked[key] = "***"
return masked
エラー時にJSON全文を出力するのではなく、必要な識別情報とエラー位置だけを記録します。
14.4 スキーマの版を管理する
長期間保存するJSONや外部システムと交換するJSONでは、項目追加や名前変更が発生します。
{
"schema_version": 2,
"user": {
"id": 1,
"display_name": "山田"
}
}
読み込み側では、版ごとに変換処理を用意できます。
def migrate(
data: dict[str, object],
) -> dict[str, object]:
version = data.get(
"schema_version",
1,
)
if version == 1:
user = data.get("user")
if isinstance(user, dict):
if "name" in user:
user["display_name"] = user.pop(
"name"
)
data["schema_version"] = 2
return data
14.5 入出力処理を一か所へまとめる
アプリケーションの各所で直接json.loadやjson.dumpsを呼ぶと、文字コード、エラー処理、変換設定がばらつきます。
import json
from pathlib import Path
from typing import Any
class JSONFileRepository:
def __init__(
self,
path: Path,
) -> None:
self._path = path
def load(self) -> Any:
with self._path.open(
"r",
encoding="utf-8",
) as file:
return json.load(file)
def save(
self,
value: Any,
) -> None:
temporary_path = self._path.with_suffix(
".tmp"
)
with temporary_path.open(
"w",
encoding="utf-8",
) as file:
json.dump(
value,
file,
ensure_ascii=False,
indent=2,
allow_nan=False,
)
temporary_path.replace(
self._path
)
一時ファイルへ書いてから置き換えると、書き込み途中で元ファイルが壊れる危険を減らせます。
15. JSON関連の関数・形式の違い
最後に、混同しやすい関数やデータ形式を個別に比較します。
15.1 dumpsとdumpの違い
json.dumpsはJSON文字列を返し、json.dumpはファイル形式のオブジェクトへ書き込みます。設定項目はほぼ共通です。
API要求本文、ログ、キャッシュ用文字列を作るならdumps、JSONファイルへ直接保存するならdumpが適しています。
| 比較項目 | json.dumps | json.dump |
|---|---|---|
| 入力 | Pythonの値 | Pythonの値とファイル |
| 出力 | JSON形式のstr | ファイルへ書き込み |
| 主な用途 | 通信、ログ、文字列生成 | JSONファイル保存 |
| 戻り値 | JSON文字列 | 通常None |
| 例 | json.dumps(data) | json.dump(data, file) |
15.2 loadsとloadの違い
json.loadsはJSON文字列、バイト列、bytearrayを読み込みます。json.loadは読み込み可能なファイル形式のオブジェクトを受け取ります。
変数内やHTTP応答内のJSONを処理するならloads、ファイルから直接読み込むならloadが分かりやすいでしょう。
| 比較項目 | json.loads | json.load |
|---|---|---|
| 入力 | str・bytes・bytearray | ファイル形式オブジェクト |
| 出力 | Pythonの値 | Pythonの値 |
| 主な用途 | API応答、文字列 | JSONファイル |
| 例 | json.loads(text) | json.load(file) |
| 主な例外 | JSONDecodeError | JSONDecodeErrorなど |
15.3 JSONとPython辞書の違い
JSONオブジェクトはテキスト形式のデータ表現です。Pythonの辞書は、プログラム実行中に利用するPythonオブジェクトです。
JSONではキーが文字列であり、文字列を二重引用符で囲み、真偽値や空値にはtrue、false、nullを使います。
| 比較項目 | JSON | Python辞書 |
|---|---|---|
| 種類 | テキスト形式 | Pythonオブジェクト |
| 文字列 | 二重引用符 | 一重・二重引用符 |
| 真偽値 | true・false | True・False |
| 空値 | null | None |
| キー | 文字列 | ハッシュ可能な値 |
| 用途 | 保存・通信 | プログラム内部処理 |
15.4 JSONとpickleの違い
JSONは複数の言語で交換できるテキスト形式です。pickleはPythonオブジェクトをPython向け形式へシリアライズします。
pickleは信頼できないデータを読み込む用途には適しません。外部システムとの交換や人が読む設定にはJSON、Python内部の限定用途では要件を確認してpickleを検討します。
| 比較項目 | JSON | pickle |
|---|---|---|
| 形式 | テキスト | バイナリ |
| 言語間交換 | しやすい | Python中心 |
| 対応型 | 基本的なデータ型 | 多くのPython型 |
| 人による確認 | 可能 | 困難 |
| 信頼できない入力 | サイズ等へ注意 | 読み込み禁止 |
| 主な用途 | API・設定・保存 | Python内部の限定用途 |
15.5 JSONとJSON Linesの違い
通常のJSONファイルは、一つのJSON値として全体が成立する必要があります。複数レコードは配列へまとめます。
JSON Linesでは、一行ごとに独立したJSON値を記録します。大量データを一件ずつ処理したり、ログへ追記したりする用途に向いています。
| 比較項目 | JSON | JSON Lines |
|---|---|---|
| 全体構造 | 一つのJSON文書 | 一行に一つのJSON値 |
| 複数レコード | 配列として保存 | 行ごとに保存 |
json.load | 利用可能 | ファイル全体には不可 |
| 追記 | 難しい | 比較的容易 |
| 部分処理 | 工夫が必要 | 一行ずつ可能 |
| 主な用途 | API、設定ファイル | ログ、大量レコード |
おわりに
PythonでJSONを扱う基本は、文字列ならjson.dumpsとjson.loads、ファイルならjson.dumpとjson.loadを使い分けることです。日本語をそのまま出力する場合はensure_ascii=False、読みやすく整形する場合はindentを指定します。
外部APIや利用者から受け取ったJSONでは、構文が正しいかだけでなく、最上位の型、必須項目、各値の型、サイズ、重複キーなども検証する必要があります。厳格な相互運用が必要な場合は、allow_nan=Falseやparse_constantも利用します。
datetime、Decimal、set、データクラスなどを扱う場合は、JSONで表現できる文字列、数値、配列、オブジェクトへ明示的に変換します。データ量が大きい場合はJSON Linesや別の保存形式も検討し、用途に合った方法を選ぶことが重要です。
EN
JP
KR