Сериализация сложных объектов Python в JSON с помощью пользовательских кодировщиков
Модуль json обрабатывает только фиксированный набор типов Python. Для сериализации комплексных чисел, дат, путей или классов пользователей необходимо предоставить функцию default или подклассить JSONEncoder, затем сопоставить вывод с object_hook на стороне декодирования.
В этом материале
Короткий ответ
Модуль Python json преобразует только dict, list, tuple, str, int, float, bool и None по умолчанию. Всё остальное вызывает TypeError, если не перехватить это. Перехват происходит через параметр default функции json.dumps или через метод default подкласса JSONEncoder. Оба подхода получают неподдерживаемый объект и должны вернуть значение совместимое с JSON или вызвать TypeError для сигнализации об ошибке. Для точности обратного пути вы также должны предоставить соответствующий object_hook на стороне декодирования, который восстанавливает исходный тип из маркерного словаря или строки.
Почему JSON не сериализует произвольные объекты Python
Модуль json определяет фиксированную таблицу преобразования. Словарь Python отображается в объект JSON, список и кортеж отображаются в массив, строка отображается в строку, int и float отображаются в число, True и False отображаются в их литералы JSON, а None отображается в null. Комплексные числа, экземпляры datetime, объекты pathlib.Path и экземпляры классов пользователей не имеют записи в этой таблице. Когда кодировщик встречает значение вне таблицы, он вызывает хук default. Если хук не предоставлен, стандартный хук вызывает TypeError. Документация явно заявляет об этом: кодировщик поддерживает только перечисленные типы, и для его расширения необходимо подклассифицировать JSONEncoder и реализовать метод default.
import json
# This raises TypeError because complex has no default mapping
try:
json.dumps(1 + 2j)
except TypeError as e:
print(e)Практический совет
При сериализации пользовательского класса всегда встраивайте ключ дискриминатора типа, такой как __type__, чтобы декодер мог отличить ваш маркерный словарь от обычного словаря, который случайно имеет те же имена полей.
Сериализация через параметр default в json.dumps
Параметр default принимает вызываемый объект, который получает неподдерживаемый объект и возвращает значение, совместимое с JSON. Возвращаемое значение может быть словарем, списком, строкой, числом или булевым значением. Если вызываемый объект не может обработать объект, он должен вызвать TypeError, чтобы другие объекты в том же документе могли быть закодированы по своим правилам или через цепочку super call. Этот подход идеален, когда вам нужна разовая трансформация в одном месте вызова, и вы не хотите определять переиспользуемый класс.
import json
def custom_json(obj):
if isinstance(obj, complex):
return {"__complex__": True, "real": obj.real, "imag": obj.imag}
raise TypeError(f"Cannot serialize object of {type(obj)}")
print(json.dumps(1 + 2j, default=custom_json))Расширение поведения через подкласс JSONEncoder
Когда одна и та же логика сериализации должна использоваться повторно в нескольких местах вызова или когда вам нужно настроить параметры уровня кодировщика, такие как indent или ensure_ascii вместе с пользовательской обработкой типов, подклассификация JSONEncoder является более чистым вариантом. Вы переопределяете метод default и вызываете super().default(o) для неподдерживаемых типов, чтобы стандартный TypeError был вызван с собственным сообщением библиотеки. Подкласс передается в json.dumps или json.dump через параметр cls.
import json
class SimpleEncoder(json.JSONEncoder):
def default(self, o):
if isinstance(o, complex):
return {"__complex__": True, "real": o.real, "imag": o.imag}
return super().default(o)
print(json.dumps({"z": 3 + 4j}, cls=SimpleEncoder))Выбор между функцией default и подклассом JSONEncoder
Используйте простую функцию default, когда трансформация специфична для одного вызова, логика короткая, и вам не нужно делиться конфигурацией кодировщика. Функция передается инлайн и исчезает после вызова. Используйте подкласс JSONEncoder, когда логика должна использоваться повторно, когда вы хотите объединить пользовательскую обработку типов с настройками кодировщика по умолчанию, такими как sort_keys или пользовательский кортеж separators, или когда цепочка трансформаций растет за пределы двух или трех проверок isinstance, и плоская функция становится трудно читаемой.
import json
from datetime import datetime, timezone
class ProjectEncoder(json.JSONEncoder):
def default(self, o):
if isinstance(o, datetime):
return o.isoformat()
if isinstance(o, complex):
return {"__complex__": True, "real": o.real, "imag": o.imag}
return super().default(o)
payload = {"ts": datetime(2024, 1, 15, tzinfo=timezone.utc), "z": 1+1j}
print(json.dumps(payload, cls=ProjectEncoder, sort_keys=True))Кодирование дат, путей и классов пользователей
Объекты datetime не имеют отображения JSON. Конвенциональное представление - строка ISO 8601, производимая методом isoformat. Сторона декодера использует datetime.fromisoformat для восстановления значения. Объекты pathlib.Path сериализуются в свою строковую форму через str(path); декодер оборачивает строку обратно с Path(). Для классов пользователей рекомендуемый паттерн - маркерный словарь, включающий ключ дискриминатора типа, например __type__, установленный в имя класса, плюс поля, необходимые для восстановления.
import json
from datetime import datetime, timezone
from pathlib import Path
class Point:
def __init__(self, x, y):
self.x = x
self.y = y
def encode(obj):
if isinstance(obj, datetime):
return obj.isoformat()
if isinstance(obj, Path):
return str(obj)
if isinstance(obj, Point):
return {"__type__": "Point", "x": obj.x, "y": obj.y}
raise TypeError(f"Unsupported type {type(obj)}")
def decode(dct):
if dct.get("__type__") == "Point":
return Point(dct["x"], dct["y"])
return dct
p = Point(3, 4)
s = json.dumps(p, default=encode)
print(s)
restored = json.loads(s, object_hook=decode)
print(type(restored).__name__, restored.x, restored.y)Параметры кодировщика, влияющие на вывод
ensure_ascii контролирует, будут ли символы не-ASCII экранированы как последовательности \uXXXX или излучены как есть. Когда ваш пользовательский default возвращает строки, содержащие текст не-ASCII, эта настройка меняет представление байтов, но не логическое содержимое. indent добавляет пробелы для читаемости и изменяет разделители по умолчанию с (',', ':') на (', ', ': '). sort_keys сортирует ключи словаря алфавитно перед строковым преобразованием, что полезно для детерминированного вывода в тестах. separators позволяет производить компактный вывод путем передачи (',',':'). allow_nan управляет тем, кодируются ли NaN, Infinity и -Infinity как литералы JavaScript или вызывают ValueError. check_circular обнаруживает циклы ссылок в контейнерах и вызывает ValueError при включении; если отключено, цикл вызывает RecursionError. skipkeys молча удаляет ключи словаря, которые не являются str, int, float, bool или None вместо вызова TypeError.
import json
from datetime import datetime
class Enc(json.JSONEncoder):
def default(self, o):
if isinstance(o, datetime):
return o.isoformat()
return super().default(o)
print(json.dumps({"b": 1, "a": datetime(2024,1,1)}, cls=Enc, sort_keys=True, indent=2))Обработка ошибок и ограничения переносимости
Когда default или метод default кодировщика вызывает TypeError, весь вызов dumps завершается неудачей; нет частичного вывода. Это означает, что один неподдерживаемый объект где угодно в дереве прерывает всю сериализацию. Спроектируйте свою функцию default так, чтобы она вызывала TypeError с четким сообщением, называющим проблемный тип, чтобы отладка была быстрой.
import json, io
buf = io.StringIO()
json.dump({"a": 1}, buf)
json.dump({"b": 2}, buf) # second call makes the file invalid
print(buf.getvalue()) # {"a": 1}{"b": 2} - not valid JSONПрактический пример объединения комплексных чисел и пользовательского класса
Следующий полный пример демонстрирует оба подхода бок о бок. Самостоятельная функция default обрабатывает комплексные числа в одном месте вызова. Подкласс JSONEncoder обрабатывает комплексные числа и пользовательский класс Event в другом месте вызова, в сочетании с sort_keys для детерминированного вывода. Сторона декодера использует object_hook для восстановления обоих типов из их маркерных словарей.
import json
class Event:
def __init__(self, name, ts, payload):
self.name = name
self.ts = ts
self.payload = payload
# Approach 1: standalone default function
def complex_default(obj):
if isinstance(obj, complex):
return {"__complex__": True, "real": obj.real, "imag": obj.imag}
raise TypeError(f"Cannot serialize {type(obj)}")
s1 = json.dumps(1 + 2j, default=complex_default)
print(s1)
# Approach 2: reusable JSONEncoder subclass
class AppEncoder(json.JSONEncoder):
def default(self, o):
if isinstance(o, complex):
return {"__complex__": True, "real": o.real, "imag": o.imag}
if isinstance(o, Event):
return {"__type__": "Event", "name": o.name,
"ts": o.ts, "payload": o.payload}
return super().default(o)
s2 = json.dumps({"z": 3+4j, "ev": Event("click", 100, {"x": 1})},
cls=AppEncoder, sort_keys=True)
print(s2)
# Round-trip decode
def app_hook(dct):
if dct.get("__complex__"):
return complex(dct["real"], dct["imag"])
if dct.get("__type__") == "Event":
return Event(dct["name"], dct["ts"], dct["payload"])
return dct
restored = json.loads(s2, object_hook=app_hook)
print(type(restored["z"]).__name__, restored["ev"].name)Что проверить
- json.dumps без default или cls вызывает TypeError для complex, datetime, Path или произвольных экземпляров классов
- Функция default должна вызывать TypeError для необработанных типов; возврат None тихо кодирует null
- Подкласс JSONEncoder должен вызывать super().default(o) для необработанных типов, чтобы сохранить стандартное сообщение об ошибке
- Точность обратного пути требует соответствующего object_hook на стороне декодирования, который проверяет ключ маркера
- Повторные вызовы json.dump к одному объекту файла производят недействительный конкатенированный вывод
- ensure_ascii, indent, sort_keys и separators влияют на представление, но не на логическое содержимое, производимое default
- allow_nan=False превращает кодирование NaN и Infinity в ValueError
- check_circular=True обнаруживает циклы в пользовательско-закодированных объектах и вызывает ValueError
Границы применения
Эта статья охватывает только стандартную библиотеку json модуля на CPython 3.6 и позже. Она не затрагивает сторонние сериализаторы, такие как orjson, ujson или rapidjson, которые имеют свои собственные механизмы расширения. Гарантии обратного пути, обсуждаемые здесь, полностью зависят от согласованности между вашими маркерами кодирования и хуками декодирования; библиотека сама по себе не предоставляет реестра типов или проверки схемы. Очень большие целые числа и значения decimal.Decimal могут потерять точность в потребителях JSON, которые парсят числа как двойной точности IEEE 754. Ограничения размера ввода для ненадежного JSON лежат на ответственности приложения, а не модуля.