Класс O: работаем с JSON как с объектом, а не как со словарём
Когда пишешь интеграцию с незнакомым API и SDK для него нет, основной способ работать с ответом — dict и квадратные скобки. Читается это быстро только у авторов доков контракта, для всех остальных хочется оформить цепочку payload["data"]["items"][0]["price"] как payload.data.items[0].price. Класс O — крошечный хелпер, который умеет именно это.
Что делает класс
O — наследник dict с двумя возможностями:
- десериализация: превратить сырые
dict/listизjson.loadsв вложенные объекты с атрибутным доступом —O.load(...); - сериализация: объяснить
json.dumps, как превращать значение обратно в обычные типы, — черезO.default.
Вот весь код:
from typing import Any
class O(dict):
@staticmethod
def default(x: Any) -> Any:
if isinstance(x, O):
return dict(x.items())
return x
@classmethod
def load(cls, d: Any) -> Any:
if isinstance(d, list):
return [cls.load(x) for x in d]
if isinstance(d, dict):
return cls((x, cls.load(y)) for x, y in d.items())
return d
def __getattr__(self, key: str) -> Any:
try:
return self[key]
except KeyError:
raise AttributeError(key) from None
def __setattr__(self, key: str, value: Any) -> None:
self[key] = value
Разбор по кусочкам:
__getattr__— вызывается, когда у объекта не нашли атрибут. Мы заворачиваем его вself[key]:o.userпревращается вo["user"]. Если ключа нет — честныйAttributeError, чтобы промахи не падали молча.__setattr__— зеркальная запись:o.user = xпишет вo["user"] = x.load(cls, d)— рекурсивно обходитdictиlistи строит из нихO. Строки, числа иboolпроходят как есть.default(x)— колбэк дляjson.dumps(..., default=...): умеет превращатьOобратно в обычныйdict.
Десериализация: читаем ответ API
Пусть некий гипотетический сервис вернул такой JSON:
{
"user": {
"name": "Аня",
"email": "anya@example.com",
"roles": ["admin", "editor"]
},
"plan": {
"name": "pro",
"price": 990
}
}
import json
raw = '{"user": {"name": "Аня", "roles": ["admin", "editor"]}, "plan": {"price": 990}}'
data = O.load(json.loads(raw))
print(data.user.name) # Аня
print(data.user.roles[0]) # admin
print(data.plan.price) # 990
Аня
admin
990
Привычные скобки и кавычки остаются только внутри json.loads. Дальше — сплошные атрибуты.
Промахнулись ключом — получите AttributeError
Нет ключа — O не молчит, а бросает AttributeError. Это удобно: опечатка видна сразу, а не отдаёт None, который где-то тихо поломает логику.
data = O.load({"user": {"name": "Аня"}})
print(data.user.name)
print(data.user.surname) # AttributeError: surname
Аня
AttributeError: surname
Сериализация: собираем объект и отдаём JSON
O можно собирать с нуля либо через конструктор, либо через присваивание атрибутов — и отдавать json.dumps.
import json
payload = O()
payload.user = O()
payload.user.name = "Борис"
payload.user.roles = O.load(["reader", "editor"])
payload.plan = O(price=990, name="pro") # dict(**kwargs) тоже работает
print(json.dumps(payload, ensure_ascii=False, default=O.default))
{"user": {"name": "Борис", "roles": ["reader", "editor"]}, "plan": {"price": 990, "name": "pro"}}
Так как O — это dict, json.dumps и без default= сам отдаст корректный объект. O.default нужен там, где во вложенных значениях встречается что-то нестандартное, — правда, он умеет превращать в обычный dict только сам класс O. Для произвольного типа вроде Money придётся дописать собственный колбэк, иначе json.dumps упадёт.
class Money:
def __init__(self, rub, kop):
self.rub, self.kop = rub, kop
payload = O(price=Money(990, 50))
print(json.dumps(payload, default=O.default)) # TypeError! O.default не знает Money
TypeError: Object of type Money is not JSON serializable
Круг замкнулся: load и round-trip
Десериализовали → дозаполнили → сериализовали → снова распарсили. Объект переживает путешествие без потерь.
import json
payload = O.load(json.loads('{"user": {"name": "Аня"}}'))
payload.user.city = "Москва"
wire = json.dumps(payload, default=O.default)
again = O.load(json.loads(wire))
print(again.user.name, again.user.city)
Аня Москва
Когда это реально удобно
- Чужие API без SDK. Один
O.load(json.loads(r.text))превращает незнакомый ответ в объект, по которому точкой добраться до любого поля. IDE не подскажет пути внутри json-структур, зато код становится короче, а «квадратные скобки + кавычки» исчезают. - Быстрые скрипты и прототипы. Написать парсер ответа в одну строчку вместо вложенных
get()и["..."]. - Тесты и глубокая вложенность. Сравнивать
a.deep.pathвместо цепочек индексов, меньше шума при рефакторинге структуры.
Единственный нюанс: цепочка вида data.plan.price работает только для ключей-идентификаторов, так что для полей вроде "123" или "created-at" придётся вернуться к data["123"]. Но это редкий случай, а доступ по атрибуту остаётся основным.
Вывод
Десять строчек класса — и работа с незнакомым API перестаёт жонглировать скобками и кавычками. O.load десериализует, O.default сериализует, а сам объект остаётся обычным dict, совместимым со всей экосистемой json. Держите под рукой для любой интеграции.
Комментарии
Пока нет комментариев.
Войдите, чтобы комментировать.