Skip to main content

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:

ScenarioAction
Items with an IDUpdated
Items without an IDInserted as new
Items omitted from requestDeleted

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.

note

Nested children are limited to one level of depth. You cannot nest children within children in a single request.

External Clients

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-1 and task-2 are 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)
The array replaces the whole child list

On a PATCH/PUT of an existing parent, the array is the complete list:

  • every child without an id is inserted;
  • every child with an id gets a full PATCH of its own — its functions run, modified is bumped, and the child collection's PATCH security policy must reach that row;
  • every existing child not listed is deleted, and the child collection's DELETE security 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 body needs a real property

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.

Access needed on the child collection

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​

Wrong direction = a permanently invisible record

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.