What this is about
In one request you can write not only one object, but also its children: the objects in its relation fields. Create a company with new employees, change an invoice together with its items, assign an employee to an existing department. This is called nested writing.
For each child in the request, CDMS asks two questions:
- Does the child have an
id? - Does the relation allow the matching action, that is, the flag
CREATEorUPDATE?
The decision tree
flowchart TB
K["Child in the request"] --> I{"has an id?"}
I -->|no| C{"Flag CREATE?"}
C -->|yes| A1["1 · create"]
C -->|no| A4["4 · rejected: 400"]
I -->|yes| E{"does the object exist?"}
E -->|no| A5["404"]
E -->|yes| U{"Flag UPDATE?"}
U -->|yes| A2["2 · update along"]
U -->|no| A3["3 · only link"]
All four cases in one request
A department whose relation employees has the flags CREATE and UPDATE, and whose relation location has no flags:
PUT /api/rest/department/update/d1…
{
"data": {
"id": "d1…",
"name": "IT",
"employees": [
{ "firstname": "Ada", "lastname": "Lovelace" },
{ "id": "e2…", "firstname": "Alan", "lastname": "Turing" }
],
"location": { "id": "l7…", "city": "Wiesbaden" }
},
"response": ["+"]
}Ada → case 1: is created (no id, CREATE)
Alan → case 2: is changed (id, UPDATE)
location → case 3: is only linked (id, no UPDATE),
"city" is ignoredIf location had a child without an id, that would be case 4: location has no CREATE, so the whole request fails with 400 recursive-create-not-allowed|location.
The four cases in detail
When: Child without id, relation with CREATE.
-
1CDMSchecks the create role of the child model (or a field role on the relation field)
-
2CDMScreates the child: its own
id,_createdOn, default values, CREATE rules -
3CDMSconnects it to the parent object, on both sides
Result: The child exists and belongs to the parent object. This works the same for Create, PUT and PATCH.
When: Child with id, relation with UPDATE.
-
1CDMS→Databaselooks up the child
-
2CDMSchecks the update role of the child model (or a field role)
-
3CDMSchanges the child according to the rules of the verb: with Create and PUT it replaces the child, with PATCH it changes only the fields you sent
-
4CDMSconnects it to the parent object
Result: Watch out with PUT: { "id": "e2…" } alone clears all fields of the child. For required fields you get 422 employees[0].firstname cannot-be-null.
When: Child with id, relation without UPDATE.
-
1CDMS→Databaselooks up the child
-
2CDMS→Clientdoes not exist → 404
-
3CDMSchecks whether you could read the child: read role (or read field role) and row filters
-
4CDMS→Clientyou may not see it → 404, as if the
iddid not exist -
5CDMSconnects it to the parent object; other fields of the child are ignored
Result: The child stays as it is. Only the connection is new. What is required is read permission for the child, not its update role — the child does not change.
When: Child without id, relation without CREATE.
-
1CDMSflag CREATE is missing
-
2CDMS→Client400
recursive-create-not-allowed|<feld>, nothing is saved
Result: This is how CDMS prevents new objects from being created by accident through a reference.
Lists with PATCH
In a list, PATCH follows the same four cases as Create and PUT, and as a single reference. The only difference is how a child with UPDATE is changed: PATCH changes only the fields you sent, PUT replaces the child.
| Create and PUT | PATCH | |
|---|---|---|
| Entry with id, without UPDATE | is only linked, fields you sent are ignored | is only linked, fields you sent are ignored |
| Entry without id, without CREATE | 400 recursive-create-not-allowed|<feld> | 400 recursive-create-not-allowed|<feld> |
| Entry with id, with UPDATE | child is replaced | only the fields you sent change |
Decision table
| id | Object exists | Flag | Verb, field kind | Result |
|---|---|---|---|---|
| no | – | CREATE | all | create |
| no | – | no CREATE | all | 400 recursive-create-not-allowed|<feld> |
| yes | no | – | Create, PUT (single reference) | 404 missing-object|<id>|<feld> |
| yes | no | – | PATCH | 404 missing-object-for-field|<id>|<feld> |
| yes | yes | UPDATE | all | update along (PUT replaces, PATCH changes) |
| yes | yes | no UPDATE | all | only link |
If the target model is abstract, a new child also needs @type. Otherwise CDMS does not know which subtype to create. If it is missing, you get 400 missing-type-for-abstract-field with the path of the field: …|mainPhone with PATCH, …|data.mainPhone with Create and PUT. An unknown type returns 400 unknown-type-for-abstract-field|<pfad>|<typ>.
Roles of the children
Every child that CDMS creates, changes or deletes is checked against the roles of its own model. A field role on the relation field can replace the role of the child model, but only for this path. If both are missing, the whole request fails with 403 missing-permission|<rolle>. See Relation types and recursive flags and Permissions on relations (field roles).
Strict mode off
Strict mode is the default: Anything that is not allowed is rejected with an error. An installation can turn it off. Then CDMS rejects less and leaves things out instead:
| Case | Strict mode on | Strict mode off |
|---|---|---|
| new child in a list, no CREATE | 400 | entry is skipped |
| new child as a single reference, no CREATE | 400 | the field becomes empty |
| role of the child is missing | 403 missing-permission|<rolle> | 403 missing-create-role / missing-update-role / missing-delete-role |
See Strict mode: error or silently ignore.
Pitfalls
What comes next
- What happens to members that are missing from the list: Lists as target state
- Who maintains the other side: Both sides of a relation
- The flags: Relation types and recursive flags