What this is about
There are two verbs for changing an object. Both go to the same path {basis}/update/{id}, but they read the body differently:
| PUT | PATCH | |
|---|---|---|
| What the body says | "This is how the object looks now." | "Only this changes." |
| Field is missing | is cleared | stays unchanged |
| Field is null | is cleared | is cleared |
| Validation | all fields | only sent fields |
| good for | saving a complete form | changing single fields |
Which verb for what?
flowchart TB
A["What do you want to do?"] --> B{"Does the object already exist?<br/>(do you have an id?)"}
B -->|no| CREATE["POST /create"]
B -->|yes| C{"Does your body describe<br/>the whole object?"}
C -->|"yes, all fields and lists"| PUT["PUT /update/{id}"]
C -->|"no, only single fields"| PATCH["PATCH /update/{id}"]
There is no endpoint that decides on its own whether to create or change (often called save or “upsert”). The client decides that, usually based on the id: if the object has one, the client changes it, otherwise it creates it.
All variants
When: An edit form shows all fields of the object, including the lists.
Use PUT with the whole form content. An empty form field should be empty afterwards too, and that is exactly what PUT does. Important: the form must really contain everything. Anything it does not know, such as a list of attachments, would be cleared by PUT.
Result: The object matches the form exactly.
When: A "done" toggle, a status change, inline editing.
-
1Client→CDMSsends
PATCHwith{ "data": { "id": "5a2b…", "status": "DONE" }, "response": ["id", "status"] } -
2CDMSchanges only
status, checks onlystatus
Result: All other fields stay, even if the client does not know them at all.
When: Removing an end date, removing an assignment.
With PATCH you send the field explicitly with null: { "id": "5a2b…", "endDate": null }. Leaving it out would change nothing. With PUT, leaving it out is enough, and null works just as well.
Result: The field is empty. If it is a required field, you get 422 cannot-be-null.
When: The client has a class Customer with all fields and sends an instance of it.
-
1Clientcreates
new Customer(), setsidandemail -
2Client→CDMSserializes all fields, the rest as
null:{ "id": "5a2b…", "email": "neu@…", "name": null, "phone": null, "orders": null } -
3CDMSsees a key with
nullfor every field → clearsname,phoneand removes allorders
Result: This is the null trap. Depending on the required fields, you get 422 or, worse, 200 with deleted data.
When: The client reads an object, changes one field and sends the whole object back with PATCH.
A read response also contains fields that you did not request, and they come as null. If you send them back with PATCH, CDMS clears exactly these fields. The same applies to a form model that is pre-filled with null.
Result: Fields that you never saw are empty afterwards.
The null trap: wrong and right
// form is pre-filled with null for every field
const form = { id, name: null, email: null, phone: null, orders: null };
form.email = 'neu@muster.de';
await patch(form);name → cleared
email → "neu@muster.de"
phone → cleared
orders → all orders removedawait patch({ id, email: 'neu@muster.de' });email → "neu@muster.de"
everything else stays as it isIn JavaScript and TypeScript, fields with undefined are dropped when converting to JSON, so they count as “not sent”. The dangerous fields are the ones that really are null: a form pre-filled with null, a response you read back, an expression like wert || null. In Java, a typical JSON mapper writes every field, including the empty ones, as null.
Decision table
| Object exists | Body contains all fields and lists | Fields should be cleared | Verb |
|---|---|---|---|
| no | – | – | POST /create |
| yes | yes | – | PUT with the whole object |
| yes | no | no | PATCH with only the changed fields |
| yes | no | yes | PATCH, fields to clear set explicitly to null |
Pitfalls
Where to go next
- The rules in detail: Replacing with PUT and Changing with PATCH
- A new object: Creating an object
- Two clients change the same object: Concurrent changes