Nested Children
RestAPI.com enables efficient management of hierarchical data structures. Update parent and child collections within a single transaction with automatic rollback support.
How It Works
When updating nested data, three simple rules apply:
| Scenario | Action |
|---|---|
| Items with an ID | Updated |
| Items without an ID | Inserted as new |
| Items omitted from request | Deleted |
All operations occur within the same transaction, ensuring data integrity.
Syntax
Nested collections use the pattern:
childCollection.fieldPointingToParent
For example, tasks.project refers to the tasks collection where project is the lookup field pointing to the parent.
Nested children are limited to one level of depth. You cannot nest children within children in a single request.
Examples below use /api virtual paths for hosted web apps. External clients should replace /api with https://<region>.restapi.com/<api-name>. See Hosted Webapps for details.
Example
Update a project with its tasks in a single request:
PATCH /api/projects/abc-123
Content-Type: application/json
{
"name": "Website Redesign",
"tasks.project": [
{ "id": "task-1", "title": "Design mockups", "status": "done" },
{ "id": "task-2", "title": "Implement frontend", "status": "in-progress" },
{ "title": "Write tests" }
]
}
In this example:
task-1andtask-2are updated (they have IDs)- A new task "Write tests" is created (no ID)
- Any other tasks linked to this project are deleted (not in the array)
On a PATCH/PUT of an existing parent, the array is the complete list:
- every child without an
idis inserted; - every child with an
idgets a fullPATCHof its own — its functions run,modifiedis bumped, and the child collection'sPATCHsecurity policy must reach that row; - every existing child not listed is deleted, and the child collection's
DELETEsecurity policy must reach that row. If the array is posted under a view, only children inside the view's filter count.
If any of those checks fails the whole request is rolled back — nothing is
silently skipped or deleted. So to add one child, send the existing ones (with
their id) plus the new one, and make sure the caller may PATCH and
DELETE every child it lists or leaves out.
Leave the child key out of the body entirely and the children are untouched.
On a POST of a new parent nothing is deleted.
A PATCH whose only content is a child array is refused with
No valid properties for item supplied (after the children were processed,
so they roll back too). Include at least one of the parent's own properties.
The same goes for each child listed by id: { "id": "…" } alone is refused
— send at least one property with it.
Sending a child array at all requires the caller to pass the
token gate
for POST, PATCH and DELETE on the child collection — all three,
whichever rows the request actually touches — through _EVERYONE,
_AUTHENTICATED_USER or a role on the caller's token. _CREATOR and roles
that live only on junction rows don't open this gate. Otherwise the request
fails with
Error updating child property '…'. Access denied. Requires POST, PATCH and DELETE access.
Benefits
Atomic Operations
All changes succeed or fail together. No partial updates that leave your data inconsistent.
Security Policy Compliance
Works with security policies for the common junction shape: create a parent and the junction row that grants access to it in the same transaction, so the parent is reachable the moment it exists. This is the recommended way to create junction rows — see Creating junction rows:
POST /api/tournaments
Content-Type: application/json
{
"name": "Spring Cup",
"tournamentMembers.tournament": [
{ "user": { "id": "<my user id>" }, "role": { "id": "<TournamentOrganizer role id>" } }
]
}
On each inserted child, the lookup back to the parent (tournament here)
is set from the parent and is not checked against the security policy —
the parent is being created, or was already authorized by the parent's own
PATCH, in the same request. Children listed by id are ordinary PATCHes
and are checked as usual. This only holds when the paths line up — see
When a nested request cannot help before relying on it.
Simplified Client Code
No need for multiple API calls to manage related data. One request handles the entire hierarchy.
Use Cases
- Projects with tasks — Update project details and task list together
- Orders with line items — Modify order and items atomically
- Categories with products — Reorganize category contents in one call
- Documents with comments — Manage document and feedback together
When a Nested Request Cannot Help
Four limitations. If your shape hits one of them, restructure the model (or the request) — a nested request will not save you. Direction arrows below (↑/↓) and their enum values are defined in the security-policy direction table; the PathFinder tool is how you inspect paths.
The Child-Side Lookup Must Point the Right Way
The "create the parent and the row that grants access to it together" pattern
only works when the child is a junction whose lookup toward the new parent
record is walked ↓ (EntityFilteredByTarget) — so a security path can
descend from that record into the junction (e.g. companyUsers.company ↓).
If that lookup is instead walked ↑ (TargetFilteredByEntity, e.g.
teamPlayers.player ↑), the nested create succeeds but no path ever
reaches the new record: it is permanently invisible to everyone, including
its creator (GET on it returns 404) — the nested request creates
exactly the dangling entity it is supposed to prevent. There is no error at
write time. Check the direction with the PathFinder before choosing this
pattern.
Functions Cannot Pin Missing Lookups Into the Body
Security-policy checks run against the final item, after any functions — so a function that writes the missing lookup into the incoming item does not help; the request is denied exactly as if the client had sent that value. See How writes are actually checked.
Child Functions Cannot See the Same-Transaction Parent
A data function on the child collection that reads or updates the just-created
parent (via ctx.get/ctx.patch) fails: the parent is not visible inside the
same transaction, and the failure is not catchable in the function. With
haltOnError=true the entire nested request fails with an opaque
400 "Error updating child property … error during function execution".
Restructure so no function needs to read the new parent inside the same
transaction — for example, move the work to a separate request after the
nested create returns.
Operation failed on Create Is Not a Lookup-Count Limit
There is no limit on how many security-policy lookups a POST body may
carry. A security-policy denial reads Permission denied, and a duplicate
id reads Duplicate item key not accepted. A generic Operation failed is
some other database rejection — for example a lookup pointing at an id that
doesn't exist in its target collection, a value that breaks a unique
property, or a limitTo constraint (whose own lookup can be filtered by the
security policy, so authorization isn't always unrelated). If the body looks
right, inspect the collection's paths with the
PathFinder tool. See
Creating rows (POST)
for what is actually checked.
A junction's user lookup is never checked against the security policy,
and its role lookup usually isn't either (see
Creating rows), so a
{ resource, user, role } junction row typically has one checked lookup —
the resource. Created nested under the parent, even that one is skipped; see
Creating junction rows.