Нюансы сериализации результатов actions#
Этот документ описывает важный нюанс RESTAlchemy: как именно выбирается сериализатор (packer) и набор полей ресурса при обработке actions, и почему action, объявленный на контроллере ресурса A, по умолчанию сериализуется как ресурс A, даже если фактически возвращает модель B.
Также приводится практический паттерн обхода (без правок библиотеки) и ссылки на рабочий пример в функциональных тестах.
TL;DR#
- Action выполняется в контексте контроллера, на котором он объявлен/к которому привязан.
- Packer и схема сериализации берутся из
__resource__этого контроллера. - Если action вернул модель другого типа, packer начнёт читать «чужие» поля и упадёт с
AttributeError. - Практичный обход без модификации RestAlchemy: привязывать action к отдельному контроллеру, у которого
__resource__соответствует типу возвращаемых моделей.
1. Почему так происходит#
1.1. Общий поток обработки action#
Упрощённо, для URL вида:
/v1/vms/<uuid>/actions/ip_addresses
происходит:
- Router резолвит route дерева (например,
v1 → vms → actions/ip_addresses). - По
<uuid>загружается родительский ресурс VM (это вход в action). - Выбирается класс action (
routes.Action) и его__controller__. - Вызывается метод контроллера, помеченный
@actions.get/@actions.post. - Результат передаётся в
controller.process_result(). process_result()строит HTTP response черезcontroller.get_packer().packerсериализует объект, используя поля ресурса изcontroller.__resource__.
1.2. Ключевой нюанс#
packer не «угадывает» тип модели по возвращаемому значению (и не использует OpenAPI-аннотации для runtime). Он сериализует ответ как тот ресурс, к которому привязан контроллер.
Если контроллер объявлен как:
__resource__ = ResourceByRAModel(models.VM)
то packer ожидает, что объекты ответа содержат поля VM (например, state, name, …).
2. Симптомы проблемы#
2.1. Типичный стек ошибки#
Если action на контроллере VM вернёт IpAddress, то packer, сериализующий «как VM», попытается прочитать у IpAddress поле state:
AttributeError: IpAddress object has no attribute state
Это выглядит как «сломанная сериализация моделей в actions», но по сути это неконсистентность между типом __resource__ контроллера и типом фактического результата action.
3. Практический пример из функциональных тестов (VM → IpAddress)#
3.1. Цель#
Сделать endpoint action:
GET /v1/vms/<uuid>/actions/ip_addresses
который возвращает список IpAddress для данной VM.
3.2. Почему нельзя просто вернуть IpAddress из VMController#
Если разместить ip_addresses() прямо в VMController, то сериализация будет выполняться как VM и упадёт (см. раздел 2).
3.3. Решение без правок библиотеки (вариант 1)#
Решение состоит из двух частей:
- (A) Отдельный контроллер для action результата с правильным
__resource__. - (B) Привязать
routes.Actionк этому контроллеру.
A) Контроллер результата#
Файл:
restalchemy/tests/functional/restapi/ra_based/microservice/controllers.py
Класс:
VMIpAddressesController
Идея: входным параметром resource остаётся VM (родительский ресурс), но __resource__ выставлен как IpAddress, поэтому packer сериализует результат корректно.
Дополнительный нюанс: в RestAlchemy есть глобальная таблица соответствия model → resource.
- Если создать второй
ResourceByRAModel(models.IpAddress), будет ошибка о duplicate mapping. - Поэтому ресурс переиспользуется:
IpAddressController.__resource__.
B) Route action#
Файл:
restalchemy/tests/functional/restapi/ra_based/microservice/routes.py
Класс:
VMIPAddressesAction
Он должен указывать на VMIpAddressesController, а в VMRoute action объявляется как:
ip_addresses = routes.action(VMIPAddressesAction, invoke=False)
3.4. Тест, фиксирующий корректное поведение#
Файл:
restalchemy/tests/functional/restapi/ra_based/test_resources.py
Тест:
TestRetryOnErrorMiddlewareBaseResourceTestCase.test_vm_get_ip_addresses_action_returns_success
Проверяет:
- HTTP 200
- JSON-ответ — список объектов IpAddress вида:
uuidipport(URI на порт)
4. Рекомендации#
4.1. Когда использовать отдельный контроллер (рекомендуется)#
Используйте отдельный контроллер, если:
- action возвращает модель другого типа (или список таких моделей),
- вы хотите сохранить action endpoint (а не добавлять отдельный nested route),
- вы не хотите модифицировать RestAlchemy.
Это даёт:
- правильный packer,
- правильный набор полей,
- воспроизводимость и предсказуемость.
4.2. Когда лучше вернуть dict/primitive#
Если результат не является ресурсом/моделью (например, {"state": "on"}), лучше возвращать простые структуры (dict, list[dict], строки), т.к. они сериализуются без привязки к полям __resource__.
4.3. Что делать, если нужно «универсально» в одном action#
Если action потенциально может вернуть разные типы (то VM, то IpAddress) — это плохой контракт:
- packer не сможет корректно сериализовать оба типа в рамках одного
__resource__.
В таком случае лучше:
- разнести на разные endpoints/controllers,
- либо менять библиотеку (см. отдельные дизайн-варианты: «action-specific resource type»).
5. См. также#
- How-to по nested resources и actions:
docs/ru/how-to/api-nested-resources-and-actions.md