結論:保存する型と意味を決め、変換できない値は明示的に扱う
JSONへ渡せるのは、オブジェクト・配列・文字列・数値・真偽値・nullなどの限られた型である。Pythonのdatetime、set、NumPyのスカラーや配列をそのまま何でも保存できるわけではない。日時はオフセット付きの文字列、数値配列は通常の数値リストなど、受け取り側と合意した形式へ変換する。保存できたというだけで元の型や意味が全て残るとは考えないこと。
手早くdefault=strを指定するとエラーは減るが、未知の値まで文字列へ変わり、後で正しく復元できないことがある。何を変換するかを列挙し、それ以外はTypeErrorで止める方が、型の追加や誤入力に気付きやすい。スキーマの版も付けておくと、後から項目や変換規則を変えたときに読み分けられる。
そのまま動かせる例
例はオフセット付き日時、NumPy整数、浮動小数点配列、日本語、欠測を表すNoneをJSONへ変換し、読み戻して検証する。NumPyと標準ライブラリを使い、ファイルやネットワークへの書き込みは行わない。countはvaluesに含まれる値の件数で、読込後に長さとの一致も確認する。このような項目間の関係も受け取り側との取り決めに含める必要がある。
後半はNaN、set、タイムゾーンのない日時、非標準のNaNを含むJSON入力、件数へ入ったboolを意図的に拒否する。これらは想定した失敗例であり、読込失敗を無視して先へ進めるための処理ではない。表示されるJSONは実際に変換した文字列で、日本語のまま読めるようensure_ascii=Falseを指定している。
from datetime import datetime, timezone
import json
import math
import numpy as np
def encode_extra(value):
if isinstance(value, datetime):
if value.tzinfo is None or value.utcoffset() is None:
raise ValueError("timezone required")
return value.isoformat()
if isinstance(value, np.integer):
return int(value)
if isinstance(value, np.floating):
return float(value)
if isinstance(value, np.ndarray):
return value.tolist()
raise TypeError(f"unsupported type: {type(value).__name__}")
def reject_constant(value):
raise ValueError("nonstandard JSON constant: " + value)
def validate(data):
keys = {"schema_version", "measured_at", "count", "values", "note", "missing"}
if not isinstance(data, dict) or set(data) != keys:
raise ValueError("unexpected keys")
if type(data["schema_version"]) is not int or data["schema_version"] != 1:
raise ValueError("unknown schema version")
if type(data["count"]) is not int or data["count"] < 0:
raise ValueError("count must be a nonnegative integer")
if not isinstance(data["values"], list) or not all(type(x) in (int, float) and math.isfinite(x) for x in data["values"]):
raise ValueError("values must be finite numbers")
if data["count"] != len(data["values"]):
raise ValueError("count does not match values")
if not isinstance(data["measured_at"], str):
raise ValueError("timestamp must be text")
stamp = datetime.fromisoformat(data["measured_at"])
if stamp.tzinfo is None or stamp.utcoffset() is None:
raise ValueError("timezone required")
if not isinstance(data["note"], str) or data["missing"] is not None:
raise ValueError("invalid note or missing marker")
return stamp
payload = {"schema_version": 1,
"measured_at": datetime(2026, 10, 2, 12, 0, tzinfo=timezone.utc),
"count": np.int64(2), "values": np.array([1.5, 2.5]),
"note": "測定A", "missing": None}
text = json.dumps(payload, ensure_ascii=False, allow_nan=False,
default=encode_extra, separators=(",", ":"))
loaded = json.loads(text, parse_constant=reject_constant)
assert validate(loaded) == payload["measured_at"]
assert loaded["count"] == 2 and loaded["values"] == [1.5, 2.5]
print(text)
for label, value, error in [("NaN", float("nan"), ValueError),
("set", {1, 2}, TypeError),
("naive datetime", datetime(2026, 10, 2), ValueError)]:
try:
json.dumps(value, allow_nan=False, default=encode_extra)
except error:
print("rejected:", label)
else:
raise AssertionError(label)
try:
json.loads('{"value":NaN}', parse_constant=reject_constant)
except ValueError:
print("rejected: nonstandard NaN on input")
else:
raise AssertionError("NaN accepted")
try:
validate({**loaded, "count": True})
except ValueError:
print("rejected: boolean count")
else:
raise AssertionError("invalid count accepted")
実行結果
{"schema_version":1,"measured_at":"2026-10-02T12:00:00+00:00","count":2,"values":[1.5,2.5],"note":"測定A","missing":null}
rejected: NaN
rejected: set
rejected: naive datetime
rejected: nonstandard NaN on input
rejected: boolean count
日時と日本語を往復させる
datetimeはisoformatで文字列へ変換し、読込側でfromisoformatを使う。どの時点かを区別するため、この例はUTCのオフセットを持つ日時だけを受け付ける。タイムゾーンのない日時へ勝手にUTCを付ければ正しくなるわけではなく、元の時刻がどの地域の時計なのかを先に確認する必要がある。サービスが要求する日時形式に違いがあれば、その仕様を優先しよう。
ensure_ascii=Falseは非ASCII文字をエスケープせず出す指定であり、ファイルの文字コードを決める指定ではない。文字列をファイルへ保存するときはopenなどでUTF-8を明示する。逆に日本語が\u形式へエスケープされていても、正しいJSONなら読込後の文字列は同じになる。見た目の違いと、データが壊れたことを区別したい。
非有限値と数値の精度
JSONの標準的な数値にはNaNやInfinityがない。Pythonのjsonは既定でこれらを出力・入力する拡張を持つため、他ツールとの交換ではallow_nan=Falseなどで意図を明確にする。欠測をnullへ変換する設計なら、その変換を明示し、数値0や文字列NaNと混同しない規則を決める。この例では非有限値を自動でnullへ変えず、入力不正として停止する。
NumPy整数をintへ変換できても、受け取る側の数値範囲が同じとは限らない。特に大きな整数をJavaScriptなどで扱う場合は精度の条件に注意する。Decimalを文字列へ変えるか、桁数付きの構造にするかも用途次第である。数値の意味と精度を保つ必要があるなら、単にJSONへ変換できるかだけでなく、受け取り側での往復も確認しよう。
JSON解析とスキーマ検証を分ける
json.loadsは構文を解析するが、countが非負整数であることなどの契約は検証しない。例のvalidateではキー集合、版、数値の型、有限性、日時のオフセットを明示的に調べている。Pythonではboolがintの派生なので、Trueを件数として通したくない場合はその条件も必要となる。型注釈だけでこの実行時検証が行われるわけではない。
配列をtolistで変換すると、shapeやdtypeなどNumPy固有の情報はJSONだけでは完全には保持されない。必要なら別項目へ記録し、読込後に照合する。辞書のキーもJSONでは文字列になるため、任意のPython辞書がそのまま同じ型で戻るとは限らない。外部からの巨大なJSONや重複キーの扱いなど、実際の入力条件に応じた制限も追加し、交換する範囲を明確にしたい。
確認環境と参考資料
例はLinux・CPython 3.12.14・NumPy 2.3.5で実行した。掲載した出力はこの環境での結果である。公式資料のstable版や最新版は更新されるため、手元のバージョンと対応する仕様も確認してほしい。
- https://docs.python.org/3/library/json.html(2026年10月2日参照)
- https://docs.python.org/3/library/datetime.html#datetime.datetime.isoformat(2026年10月2日参照)
- https://numpy.org/doc/stable/reference/generated/numpy.ndarray.tolist.html(2026年10月2日参照)
関連項目:測定配列を形と型を保って保存する / TOML設定ファイルをtomllibで読み込み、入力ミスを早めに見つける / dataclassで実験条件をまとめる:default_factoryとfrozenの注意点 / 解析をやり直せる実行記録を残す:条件・入力ハッシュ・環境情報 / Requestsで通信の失敗を扱う:timeout・HTTPエラー・JSON解析
