Сериализация типов данных не поддерживаемых JSON

То что в JSON нет встроенной поддержки типа bytes, не значит что вы не сможете сериализовать объекты типа bytes. Модуль json предоставляет расширяемые хуки для кодирования и декодирования неизвестных типов данных. (Под "неизвестными" я имел в виду "не определенные в json". Очевидно, что модуль json знает о массивах байт, но он создан с учетом ограничений спецификации json). Если вы хотите закодировать тип bytes или другие типы данных, которые json не поддерживает, вам необходимо предоставить особые кодировщики и декодировщики для этих типов данных.

>>> shell
1
>>> entry ①
{'comments_link': None,
'internal_id': b'\xDE\xD5\xB4\xF8',
'title': 'Dive into history, 2009 edition',
'tags': ('diveintopython', 'docbook', 'html'),
'article_link': 'http://diveintomark.org/archives/2009/03/27/dive-into-history-2009-edition',
'published_date': time.struct_time(tm_year=2009, tm_mon=3, tm_mday=27, tm_hour=22, tm_min=20, tm_sec=42, tm_wday=4, tm_yday=86, tm_isdst=-1),
'published': True}
>>> import json
>>> with open('entry.json', 'w', encoding='utf-8') as f: ②
... json.dump(entry, f) ③
...
Traceback (most recent call last):
File "<stdin>", line 5, in <module>
File "C:\Python31\lib\json\__init__.py", line 178, in dump
for chunk in iterable:
File "C:\Python31\lib\json\encoder.py", line 408, in _iterencode
for chunk in _iterencode_dict(o, _current_indent_level):
File "C:\Python31\lib\json\encoder.py", line 382, in _iterencode_dict
for chunk in chunks:
File "C:\Python31\lib\json\encoder.py", line 416, in _iterencode
o = _default(o)
File "C:\Python31\lib\json\encoder.py", line 170, in default
raise TypeError(repr(o) + " is not JSON serializable")
TypeError: b'\xDE\xD5\xB4\xF8' is not JSON serializable

① Хорошо, настало время вновь обратиться к структуре данных entry. Там есть все: логические значения, пустое значение, строка, кортеж строк, объект типа bytes, и структура хранящая время.

② Я знаю, что говорил это ранее, но повторюсь еще раз: json это текстовый формат. Всегда открывайте файлы json в текстовом режиме с кодировкой utf-8.

③Чтож... _ЭТО_ не хорошо. Что произошло?

А вот что: функция json.dump() попробовала сериализовать объект bytes b'\xDE\xD5\xB4\xF8', но ей не удалось, потому что в json нет поддержки объектов bytes. Однако, если сохранение таких объектов важно для вас, вы можете определить свой "мини формат сериализации".

def to_json(python_object): ①
if isinstance(python_object, bytes): ②
return {'__class__': 'bytes',
'__value__': list(python_object)} ③
raise TypeError(repr(python_object) + ' is not JSON serializable') ④

① Чтобы определить свой собственный "мини формат сериализации" для тех типов данных, что json не поддерживает из коробки, просто определите функцию, которая принимает объект Python как параметр. Этот объект Python будет именно тем объектом, который функция json.dump() не сможет сериализовать сама - в данном случае это объект bytes b'\xDE\xD5\xB4\xF8'

② Вашей специфичной функции сериализации следует проверять тип объектов Python, которые передала ей функция json.dump(). Это не обязательно, если ваша функция сериализует только один тип данных, но это делает кристально ясным какой случай покрывает данная функция, и делает более простым улучшение функции, если вам понадобится сериализовать больше типов данных позже

③ В данном случае я решил конвертировать объект bytes в словарь. Ключ __class__ будет содержать название оригинального типа данных, а ключ __value__ будет хранить само значение. Конечно, это не может быть объекты типа bytes, поэтому нужно преобразовать его во что-нибудь сериализуемое при помощи json. Объекты типа bytes это просто последовательность чисел, каждое число будет где-то от 0 до 255. Мы можем использовать функцию list() чтобы преобразовать объект bytes в список чисел. Итак b'\xDE\xD5\xB4\xF8' становится [222, 213, 180, 248]. (Посчитайте! Это работает! Байт \xDE в шестнадцатеричной системе это 222 в десятичной, \xD5 это 213, и так далее.)

④ Это строка важна. Структура данных, которую вы сериализуете может содержать типы данных которых нет в json, и которые не обрабатывает ваша функция. В таком случае, ваш обработчик должен raise ошибку TypeError чтобы функция json.dump() узнала, что ваш обработчик не смог распознать тип данных.

Вот оно, больше вам ничего не нужно. Действительно, определенная вами функция обработчик возвращает словарь Python, не строку. Вы не пишите сериализацию в json полностью сами, вы просто делаете конвертацию-в-поддерживаемый-тип-данных. Функция json.dump() сделает остальное за вас.

>>> shell
1
>>> import customserializer ①
>>> with open('entry.json', 'w', encoding='utf-8') as f: ②
... json.dump(entry, f, default=customserializer.to_json) ③
...
Traceback (most recent call last):
File "<stdin>", line 9, in <module>
json.dump(entry, f, default=customserializer.to_json)
File "C:\Python31\lib\json\__init__.py", line 178, in dump
for chunk in iterable:
File "C:\Python31\lib\json\encoder.py", line 408, in _iterencode
for chunk in _iterencode_dict(o, _current_indent_level):
File "C:\Python31\lib\json\encoder.py", line 382, in _iterencode_dict
for chunk in chunks:
File "C:\Python31\lib\json\encoder.py", line 416, in _iterencode
o = _default(o)
File "/Users/pilgrim/diveintopython3/examples/customserializer.py", line 12, in to_json
raise TypeError(repr(python_object) + ' is not JSON serializable') ④
TypeError: time.struct_time(tm_year=2009, tm_mon=3, tm_mday=27, tm_hour=22, tm_min=20, tm_sec=42, tm_wday=4, tm_yday=86, tm_isdst=-1) is not JSON serializable

① Модуль customserializer, это то где вы только что определили функцию to_json() в предыдущем примере

② Текстовый режим, utf-8, тра-ля-ля. (Вы забудете! Я иногда забываю! И все работает замечательно, пока в один момент не сломается, и тогда оно начинает ломаться еще театральнее)

③ Это важный кусок: чтобы встроить вашу функцию обработчик преобразования в функцию json.dump() передайте вашу функцию в json.dump() в параметре default. (Ура, все в Python - объект!)

④ Замечательно, это и правда работает. Но посмотрите на исключение. Теперь функция json.dump() больше не жалуется о том, что не может сериализовать объект bytes. Теперь она жалуется о совершенно другом объекте: time.struct_time.

Хоть получить другое исключение и не выглядит как прогресс, на самом деле это так. Нужно просто добавить пару строк кода, чтобы и это работало.

import time

def to_json(python_object):
if isinstance(python_object, time.struct_time): ①
return {'__class__': 'time.asctime',
'__value__': time.asctime(python_object)} ②
if isinstance(python_object, bytes):
return {'__class__': 'bytes',
'__value__': list(python_object)}
raise TypeError(repr(python_object) + ' is not JSON serializable')

① Добавляя в уже существующую функцию customserializer.to_json() мы должны проверить, что объект Python(с которым у функции json.dump() проблемы) на самом деле time.struct_time.

② Если так, мы сделаем нечто похожее на конвертацию, что мы делали с объектом bytes: преобразуем объект time.struct_time в словарь который содержит только те типы данных что можно сериализовать в json. В данном случае, простейший путь преобразовать дату в значение которое можно сериализовать в json это преобразовать ее к строке при помощи функции time.asctime(). Функция time.asctime() преобразует отвратительно выглядящую time.struct_time в строку 'Fri Mar 27 22:20:42 2009'.

С этими двумя особыми преобразованиями, структура данных entry должна сериализовать полность без каких либо проблем.

>>> shell
1
>>> with open('entry.json', 'w', encoding='utf-8') as f:
... json.dump(entry, f, default=customserializer.to_json)
...

you@localhost:~/diveintopython3/examples$ ls -l example.json
-rw-r--r-- 1 you you 391 Aug 3 13:34 entry.json
you@localhost:~/diveintopython3/examples$ cat example.json
{"published_date": {"__class__": "time.asctime", "__value__": "Fri Mar 27 22:20:42 2009"},
"comments_link": null, "internal_id": {"__class__": "bytes", "__value__": [222, 213, 180, 248]},
"tags": ["diveintopython", "docbook", "html"], "title": "Dive into history, 2009 edition",
"article_link": "http://diveintomark.org/archives/2009/03/27/dive-into-history-2009-edition",
"published": true}