Besonderheiten der Serialisierung von Action-Ergebnissen#
Dieses Dokument beschreibt eine wichtige Nuance in RESTAlchemy: wie Serializer (Packer) und das Feld-Set einer Ressource bei der Verarbeitung von actions ausgewählt werden, und warum eine Action, die auf einem Controller für Ressource A definiert ist, standardmäßig als Ressource A serialisiert wird – selbst wenn sie tatsächlich ein Modell B zurückgibt.
Außerdem wird ein praktikabler Workaround (ohne Änderungen an der Bibliothek) beschrieben und auf ein funktionierendes Beispiel in den Functional Tests verwiesen.
TL;DR#
- Eine Action wird im Kontext des Controllers ausgeführt, auf dem sie definiert bzw. an den sie gebunden ist.
- Packer und Serialisierungsschema werden aus
__resource__dieses Controllers genommen. - Gibt eine Action ein Modell eines anderen Typs zurück, versucht der Packer „fremde“ Felder zu lesen und scheitert mit
AttributeError. - Praktischer Workaround ohne RESTAlchemy zu verändern: Action an einen dedizierten Controller binden, dessen
__resource__dem Rückgabe-Typ entspricht.
1. Warum passiert das?#
1.1. Grober Ablauf der Action-Verarbeitung#
Vereinfacht für eine URL wie:
/v1/vms/<uuid>/actions/ip_addresses
passiert:
- Der Router löst den Route-Baum auf (z. B.
v1 → vms → actions/ip_addresses). - VM wird über
<uuid>geladen (das ist der Input der Action). - Die Action-Klasse (
routes.Action) wird aufgelöst und ihr__controller__bestimmt. - Die Controller-Methode mit
@actions.get/@actions.postwird aufgerufen. - Das Ergebnis wird an
controller.process_result()übergeben. process_result()baut die HTTP-Response übercontroller.get_packer().- Der Packer serialisiert anhand der Resource-Felder aus
controller.__resource__.
1.2. Die zentrale Nuance#
Der Packer „errät“ den Modelltyp zur Laufzeit nicht aus dem Rückgabewert (OpenAPI-Annotationen werden zur Laufzeit nicht zum Packen verwendet). Stattdessen wird die Response als die Resource serialisiert, die an den Controller gebunden ist.
Wenn der Controller deklariert:
__resource__ = ResourceByRAModel(models.VM)
dann erwartet der Packer VM-Felder (z. B. state, name, ...).
2. Symptome#
2.1. Typischer Error-Stack#
Gibt eine Action auf dem VM-Controller ein IpAddress zurück, versucht der Packer (serialisiert „als VM“) das Feld state bei IpAddress zu lesen:
AttributeError: IpAddress object has no attribute state
Das wirkt wie „kaputte Serialisierung in Actions“, ist aber eigentlich ein Mismatch zwischen __resource__ des Controllers und dem tatsächlichen Ergebnis-Typ.
3. Praxisbeispiel aus den Functional Tests (VM → IpAddress)#
3.1. Ziel#
Ein Action-Endpunkt:
GET /v1/vms/<uuid>/actions/ip_addresses
soll eine Liste von IpAddress für die VM zurückgeben.
3.2. Warum es nicht reicht, IpAddress direkt aus VMController zurückzugeben#
Wenn ip_addresses() direkt in VMController implementiert wird, wird weiterhin „als VM“ serialisiert und es kommt zum Fehler (siehe Abschnitt 2).
3.3. Fix ohne Änderungen an der Bibliothek (Ansatz 1)#
Der Fix besteht aus zwei Teilen:
- (A) Dedizierter Controller für das Action-Ergebnis mit korrektem
__resource__. - (B) Action-Route an diesen Controller binden.
A) Ergebnis-Controller#
Datei:
restalchemy/tests/functional/restapi/ra_based/microservice/controllers.py
Klasse:
VMIpAddressesController
Idee: Der Input-Parameter resource ist weiterhin VM (Parent Resource), aber __resource__ ist IpAddress, sodass der Packer korrekt serialisiert.
Zusätzliche Nuance: RESTAlchemy hält eine globale Zuordnung model → resource.
- Wenn ein weiteres
ResourceByRAModel(models.IpAddress)erstellt wird, kommt es zu einem Duplicate-Mapping-Fehler. - Deshalb wird die Resource wiederverwendet:
IpAddressController.__resource__.
B) Action-Route#
Datei:
restalchemy/tests/functional/restapi/ra_based/microservice/routes.py
Klasse:
VMIPAddressesAction
Sie muss auf VMIpAddressesController zeigen, und in VMRoute wird sie so deklariert:
ip_addresses = routes.action(VMIPAddressesAction, invoke=False)
3.4. Test für das korrekte Verhalten#
Datei:
restalchemy/tests/functional/restapi/ra_based/test_resources.py
Test:
TestRetryOnErrorMiddlewareBaseResourceTestCase.test_vm_get_ip_addresses_action_returns_success
Er prüft:
- HTTP 200
- JSON-Response ist eine Liste von IpAddress-Objekten mit:
uuidipport(URI des Ports)
4. Empfehlungen#
4.1. Dedizierten Controller verwenden (empfohlen)#
Nutze einen dedizierten Controller, wenn:
- die Action einen anderen Modelltyp (oder eine Liste davon) zurückgibt,
- du einen Action-Endpunkt beibehalten willst,
- du RESTAlchemy nicht ändern möchtest.
Das liefert:
- den passenden Packer,
- das passende Feld-Set,
- vorhersagbares Verhalten.
4.2. dict / Primitive zurückgeben#
Wenn das Ergebnis keine Resource/kein Modell ist (z. B. {"state": "on"}), sind einfache Strukturen (dict, list[dict], Strings) sinnvoll, da sie nicht von __resource__ abhängen.
4.3. Eine Action, mehrere Rückgabe-Typen#
Wenn eine Action mehrere Typen zurückgeben kann (manchmal VM, manchmal IpAddress), ist das ein ungünstiger Vertrag:
- eine einzelne
__resource__kann nicht beide Typen korrekt serialisieren.
Dann lieber:
- separate Endpunkte/Controller,
- oder die Bibliothek erweitern (z. B. „action-specific resource type“).
5. Siehe auch#
- How-to zu Nested Resources und Actions:
docs/de/how-to/api-nested-resources-and-actions.md