FHIR PATCH with JSON Patch: five pitfalls, and how an agent should write patches

JSON Patch looks like the safe way for an agent to change one field. On FHIR resources full of arrays, it's easy to replace a whole list, edit the wrong entry, or skip validation. Here are the traps.

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

A FHIR server can accept a write and quietly drop a field. Deedglass reads the resource back and names what differs. Made-up resource.

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.

The takeaway

A JSON Patch add to an existing FHIR array member replaces the whole list; pin edits with test ops and If-Match, validate the result, and read back.

Read nextFHIR If-Match version conflicts: why you get 409 when the spec says 412 Try it: install in 7 s

Related on this site

More posts

Sources

  1. 1HL7 FHIR R4: RESTful API, patch · hl7.org · checked 27 October 2026
  2. 2RFC 6902: JavaScript Object Notation (JSON) Patch · rfc-editor.org · checked 27 October 2026
  3. 3HAPI FHIR: Repository Validating Interceptor · hapifhir.io · checked 27 October 2026
  4. 4HL7 FHIR R4: Managing Resource Contention · hl7.org · checked 27 October 2026
  5. 5deedglass/deedglass: FHIR write-integrity benchmark results · github.com · checked 27 October 2026

Published 27 October 2026, sources last checked 27 October 2026. If something here is out of date, write to hello@deedglass.com.

Try it on your own setup

npx deedglass scan

Grades only your own setup. Nothing is uploaded.

Pricing