Nested writes
Create, update, and upsert a parent and its children — to any depth — in one request with writable nested serializers.
Writable nested objects
Create a parent row and its children in a single request. Declare the nested relationship on the resource:
RestPlugin::default() .resource(ResourceConfig::for_::<Order>().nested("items", "order_item"))Now a POST whose body carries the nested array creates everything:
POST /api/order/Content-Type: application/json { "customer": "Ada", "items": [ { "product": "Widget", "qty": 2 }, { "product": "Gadget", "qty": 1 } ]}The order is inserted first; then each item is inserted with its foreign key to the order set automatically, so you don't repeat the parent id in each child. The response echoes the created children back:
{ "id": 7, "customer": "Ada", "items": [ { "id": 1, "order": 7, "product": "Widget", "qty": 2 }, { "id": 2, "order": 7, "product": "Gadget", "qty": 1 } ]}How the FK is found
.nested("items", "order_item") says "the items array holds order_item rows." The framework discovers which column on order_item points back at order (the column whose foreign key targets the parent's table) and fills it. If the child has no FK to the parent, or more than one (ambiguous), the request fails with a clear 400.
All-or-nothing on failure
If any child fails validation (a missing required field, a bad FK, a constraint clash), the whole write is rolled back in a single database transaction - the parent and all children either commit together or not at all, so you never get a half-created parent:
POST /api/order/ { "customer": "Grace", "items": [ {"product":"ok","qty":1}, {"qty":5} ] }→ 400 (the second item has no product) → no order is createdThis is a real database transaction: create_nested opens one umbral::db::Transaction, inserts the parent + every child on it (DynQuerySet::insert_json_in_tx), and commit()s only after they all succeed. Any failure - a bad child, a constraint clash, or even a process crash between inserts - leaves zero rows, because the transaction is never committed and the database rolls it back. (This replaced an earlier compensating delete-on-failure handler; see planning/orm_fixes.md #2.)
Nested updates
The same .nested(...) declaration also powers PATCH/PUT. A nested array on an update upserts its children in one transaction alongside the parent's own columns:
PATCH /api/order/7Content-Type: application/json { "customer": "Ada Lovelace", "items": [ { "id": 1, "qty": 99 }, { "product": "Sprocket", "qty": 4 } ]}Each item is matched by whether it carries the child's primary key:
- With a pk (
{ "id": 1, ... }) → that child is updated in place. The update is scoped to this parent via the foreign key, so one parent's payload can never touch another parent's child (a pk that belongs to a different parent returns404and the whole PATCH rolls back). - Without a pk (
{ "product": "Sprocket", ... }) → a new child is created, its FK set to the parent automatically (just like nested create).
No implicit deletes. Children you don't mention in the array are left untouched — a forgotten item never silently deletes a row. In the example above, order 7's other existing items survive; only item 1 is updated and one new item is added. Full "replace the whole set" (delete-the-missing) semantics are intentionally not the default; if you need to remove a child, delete it through its own resource.
Like nested create, the entire update — parent columns plus every child upsert — runs on one umbral::db::Transaction and commits only if all writes succeed. Any failure (a bad child, a cross-parent pk, a constraint clash) rolls everything back, including the parent's column changes.
Going deeper — multi-level nesting
Nesting recurses. Declare .nested(...) on each level and a single request writes the whole tree — order → items → components:
RestPlugin::default() .resource(ResourceConfig::for_::<Order>().nested("items", "order_item")) .resource(ResourceConfig::for_::<OrderItem>().nested("components", "component"))POST /api/order/{ "customer": "Ada", "items": [ { "product": "Widget", "qty": 2, "components": [ { "name": "Screw", "grams": 5 } ] } ]}The order, its item, and the item's components are all inserted on one transaction, each FK auto-wired to its parent. PATCH upserts the same way at every level (update-by-pk, create-without-pk, ownership-scoped so a grandchild pk from another parent is a 404).
The rule is simple: a level is written only if its parent's table also declared .nested(...). One declaration per level, no magic — and no silent drops. (If a table has no .nested() for an array key you send, that key is rejected rather than quietly ignored.) Depth is bounded at 16 to stop a cyclic declaration from recursing forever.
Declaring .nested() on a mid-level table (like OrderItem above) currently also exposes it as its own /api/order_item/ resource. If you don't want that, gate it with permissions for now; a declare-nesting-without-exposing option is on the roadmap.
Scope
Arbitrary depth, one .nested(...) declaration per level. Declare multiple sibling arrays on the same table by calling .nested(...) more than once. The flat create/update paths are unchanged and carry zero overhead when no nested resources are declared.
See also
- Exposure: which models become resources.
- Authentication & permissions: gate the create action.