The short answer
JSON Patch is risky on FHIR resources because most interesting fields are arrays. Under RFC 6902, an add to an object member that already exists replaces its value, so add /telecom with one phone number throws away every other contact point; add at an index inserts, and removing an element shifts the ones after it.2 Guard every patch with test operations that check what you expect is there, send If-Match with the version you read, make sure the server validates the patched result, and read the resource back. If any operation in a patch fails, the whole patch must fail.2
Deedglass
The server said “saved”. Deedglass looked.
Deedglass reads it back
What R4 and RFC 6902 say
FHIR R4 accepts three patch formats: JSON Patch (application/json-patch+json), XML Patch, and FHIRPath Patch. The server applies the patch to its own copy, then processes the result as an update, with all of update’s version and error handling. Clients SHOULD always consider version-specific patches.1
RFC 6902 defines six operations: add, remove, replace, move, copy and test.2
| Operation | On an object member | On an array index |
|---|---|---|
| add | Creates it, or replaces it if it exists | Inserts at that index; - appends |
| remove | Deletes it | Deletes; later elements shift left |
| replace | Target must exist | Target must exist |
| test | Succeeds only if equal | Succeeds only if equal |
On error, evaluation SHOULD stop and the entire patch SHALL NOT be deemed successful; HTTP PATCH is atomic.2
Pitfall 1: add replaces an existing array
An agent wants to add a mobile number to a Patient that already has a home number and an email:
Code Data in JSON, the format software uses to pass records and settings.
[{ "op": "add", "path": "/telecom",
"value": [{ "system": "phone", "use": "mobile", "value": "+91 90000 00000" }] }]
Because /telecom exists, add replaces it.2 The Patient now has one contact point. The server answers 200; nothing was refused. In our earlier benchmark on Medplum 5.2.6, a JSON Patch add that replaced an array was one of the scenarios our own checker missed.5 The fix is to append:
Code Data in JSON, the format software uses to pass records and settings.
[{ "op": "add", "path": "/telecom/-",
"value": { "system": "phone", "use": "mobile", "value": "+91 90000 00000" } }]
Pitfall 2: indexes shift
Paths like /name/1/given/0 point by position. Positions change when anyone adds or removes an element. Remove /identifier/0 and the old index 1 becomes 0.2 A patch built from a read ten minutes ago may now point at a different element.
Two consequences for agents. Never build a multi-step patch that removes an early element and then edits a later one by its old index. And never reuse indexes across separate patches without re-reading.
This is where language models are weakest. A model sees a resource once, early in a long session, and later writes a path from memory. The path is syntactically perfect and points at whatever now sits at that position. Nothing in the reply says the agent edited a different name, address or identifier than it meant to. If you let agents generate patches, have your code, not the model, compute indexes from a fresh read immediately before sending.
Pitfall 3: no test, no guard
test succeeds only if the value at a path equals what you give.2 Because a failed operation fails the whole patch, a test placed first turns “edit index 1” into “edit index 1, but only if it’s still the entry I think it is”:
Code Data in JSON, the format software uses to pass records and settings.
[
{ "op": "test", "path": "/telecom/1/value", "value": "home@example.org" },
{ "op": "replace", "path": "/telecom/1/value", "value": "new@example.org" }
]
If someone else changed the list, the test fails and nothing is applied. It’s the cheapest safety check JSON Patch offers, and agents rarely generate it unless told to.
Pitfall 4: no version check
test guards individual paths. If-Match guards the whole resource: send the version you read, and a stale patch is refused with 412 under R4.4 Servers we’ve tested answer 409 for the same case; see our If-Match post. R4 asks clients to consider version-specific patches for exactly this reason.1
Use both. If-Match catches any concurrent change; test documents and checks the specific values your edit depends on.
Pitfall 5: validation that never sees the result
A patch body isn’t a resource, so validating the request tells you nothing about the resource after the patch. HAPI FHIR’s documentation notes that request and response validation can’t validate FHIR Patch results; its Repository Validating Interceptor validates the resource just before it’s saved, which covers patches.3 Our request validation post shows the setup.
If your server only validates requests, a patch can produce a resource that would have been refused as a PUT.
A safe patch template
| Step | Why |
|---|---|
| Read the resource; note its version | Base for the patch |
Build ops with - to append, never add on an existing array path |
Avoid replacing lists2 |
Put a test before each replace or remove on an indexed path |
Fail if the list moved2 |
Send If-Match with the version read |
Fail on any concurrent change4 |
| Validate the stored result, not the request | Patches bypass request validation3 |
| Read back and compare the fields you meant to change, and the ones you didn’t | Catch replaced arrays and dropped fields |
The last row matters most. Compare not just the field you changed, but the fields around it: the length of the array before and after is a one-line check that catches pitfall 1.
Questions people ask
Does JSON Patch add append to a FHIR array?
Only with - at the end of the path, or an explicit index to insert at. add on the array’s own path replaces the whole array if it exists.2
Is a FHIR PATCH atomic?
Yes: if any operation fails, the patch as a whole is not successful, and HTTP PATCH is atomic.2 The server then processes the result as an update.1
Should agents use PATCH or PUT?
PATCH with test and If-Match changes less and fails safely. PUT replaces the whole resource and is easier to validate. Either way, read back.
The FHIR write integrity study has more cases where servers stored something different from what was sent; hospitals can start from our healthcare page, and the record spec shows how to record a patch with its read-back. To try approvals and read-backs on a real database in your browser, open the playground; to grade what an agent can reach on your own machine, run npx deedglass scan (the grade, explained).
Written with AI assistance and checked against our claims register.