skip to content

Two standard patch formats exist for HTTP PATCH bodies: JSON Merge Patch (RFC 7386) and JSON Patch (RFC 6902). How do they differ, and how would you decide which one an API should accept?

level: middleimportance: should knowfreq 48%

answer

  1. merge-patch+json: null = delete, arrays replaced
  2. json-patch+json: op/path/value, JSON Pointer
  3. /tags/- = append → not idempotent
  4. test op = in-body compare-and-swap, atomic
  5. Accept-Patch advertises formats; 415 rejects others

basics

~20 s

JSON Merge Patch is a partial object: present fields are set, null deletes a field, arrays are replaced wholesale. JSON Patch is a list of operations (add/remove/replace/move/copy/test) with JSON Pointer paths, so it can edit array elements and assert preconditions — at the cost of verbosity.

solid answer

~50 s

**JSON Merge Patch** (`application/merge-patch+json`) looks like the resource: `{"status":"paid","note":null}` sets status and deletes note. It is trivial for clients to build and read, and it is effectively idempotent because it sets absolute values. Its limits are structural: `null` is reserved for deletion so you cannot set a field *to* null, and arrays can only be replaced entirely — you cannot say "add one tag". **JSON Patch** (`application/json-patch+json`) is an ordered array of operations addressed by JSON Pointer: `[{"op":"add","path":"/tags/-","value":"vip"},{"op":"test","path":"/version","value":7}]`. It handles array element edits, explicit nulls, and optimistic concurrency via `test`. It is not idempotent in general (`add` to `/tags/-` appends every replay), and it is harder to validate and authorize because the affected fields are computed, not declared. Default to merge patch for CRUD-ish resources; reach for JSON Patch when clients must mutate collections in place or need per-operation preconditions.

go deeper

for a junior

Know that PATCH sends only what changes, that merge patch uses null to delete, and that JSON Patch is a list of operations.

for a middle

Contrast them on arrays, null semantics, and idempotency, and name the two media types.

for a senior

Discuss authorization of pointer-addressed operations, concurrency (test vs If-Match), and when sub-resources beat patch formats entirely.

for a principal

Set the org-wide policy: which formats are supported, how they are described in the API spec, and how retry safety is guaranteed uniformly.

## Why a patch needs a format at all PATCH's definition says the body is a set of instructions describing how to change the resource. It deliberately does not say what those instructions look like — that is what `Content-Type` declares. A server that accepts `application/json` and applies "whatever fields are present" has invented a private format; that is legal and common, but it means clients cannot rely on cross-API knowledge, and the meaning of `null` becomes a per-endpoint mystery. ## JSON Merge Patch (RFC 7386) The patch document mirrors the resource's shape. The algorithm is recursive and short: - a key present with a non-null value → set it (recursing into nested objects), - a key present with `null` → **delete** that key, - a key absent → leave untouched, - any array value → replace the whole array. ``` PATCH /orders/42 Content-Type: application/merge-patch+json {"status":"paid","note":null,"tags":["vip","rush"]} ``` Strengths: readable, tiny, and easy to generate from a UI form that tracks dirty fields. Because every operation assigns an absolute value, applying the same merge patch twice yields the same state — practically idempotent. Weaknesses, all consequences of the same design choice: 1. **`null` is overloaded.** You cannot express "set `discount` to JSON null" because null means "remove". If your domain distinguishes "absent" from "explicitly null", merge patch cannot say it. 2. **Arrays are all-or-nothing.** Adding one tag requires sending the entire tag list, which reintroduces the lost-update problem PATCH was meant to avoid — two concurrent adders clobber each other. 3. **No preconditions.** Concurrency control must come from outside the body, typically `If-Match` with an ETag. ## JSON Patch (RFC 6902) The body is an ordered array of operations, each addressing a location with a JSON Pointer (`/tags/0`, `/customer/address/city`, `/tags/-` meaning "end of array"): ``` [ {"op":"test","path":"/version","value":7}, {"op":"replace","path":"/status","value":"paid"}, {"op":"add","path":"/tags/-","value":"vip"}, {"op":"remove","path":"/note"} ] ``` Operations are `add`, `remove`, `replace`, `move`, `copy`, `test`. They apply in order and are **atomic**: if any fails — including a failing `test` — the whole patch is rejected and the resource is unchanged. That makes `test` a genuine compare-and-swap primitive inside the body. Strengths: in-place array edits, explicit null values (`replace` with `value: null` really sets null), embedded preconditions, and the ability to express intent ("append") rather than outcome ("the list is now …"), which is what lets concurrent clients coexist. Weaknesses: 1. **Not idempotent.** `add` to `/tags/-` appends on every replay. A retried request duplicates data. Fix it with a `test` guard, an ETag, or an idempotency key. 2. **Verbose and error-prone.** Clients hand-building pointers get array indices wrong; index-based ops race against concurrent modifications. 3. **Hard to validate and authorize.** With merge patch you can look at the top-level keys and decide "this caller may not change `role`". With JSON Patch you must interpret pointers, including `move`/`copy` sources, before you know which fields are touched — a real source of authorization bypasses. 4. **Tooling gap.** OpenAPI describes a merge-patch body naturally (it is the resource schema with everything optional); a JSON Patch body is just "array of operations", so per-endpoint documentation must carry the real constraints. ## Deciding Ask what clients actually need to express. - Editing scalar fields on a document-shaped resource → **merge patch**. It is the least surprising thing for consumers and the easiest to secure. - Collections that multiple actors mutate concurrently (tags, members, permissions) → either **JSON Patch** for in-place ops, or better, **model the collection as sub-resources** (`PUT /orders/42/tags/vip`, `DELETE /orders/42/tags/vip`) and skip patch formats entirely. Sub-resources give you clean idempotency and per-item authorization for free. - Need transactional preconditions inside the body → **JSON Patch** with `test`, or `If-Match` plus merge patch, which is usually simpler. Whatever you choose, declare it: advertise the accepted media types, answer `415 Unsupported Media Type` for a patch format you do not implement, and list them in the `Accept-Patch` response header so clients can discover support.

  • With JSON Merge Patch, how do you let a client set a field to null rather than delete it?
    You cannot within the format — null is reserved for removal. The usual answers are to redesign so that removal and null mean the same thing to the domain, to expose the field as its own sub-resource you can PUT or DELETE, or to switch that endpoint to JSON Patch where `replace` with a null value is unambiguous. Inventing a sentinel like the string "__null__" is a hack that leaks into every client.
  • How would you authorize a JSON Patch request when some fields are admin-only?
    Resolve the patch to the set of affected paths before applying it, including the source paths of `move` and `copy`, and check each against the caller's permissions; then apply atomically. The naive alternative — applying first and validating the result — can leak information or perform partial side effects, and checking only `path` while ignoring `from` is a classic bypass.

saying these in an interview costs you the question

  • Believing PATCH bodies are always "just the changed fields" regardless of media type
  • Claiming JSON Patch is idempotent, ignoring `add` to an array end
  • Using JSON Merge Patch to append to an array and expecting concurrent clients not to clobber each other
  • Authorizing a JSON Patch by inspecting only top-level keys or only the `path` field
  • Accepting any JSON body on PATCH and never documenting what null means

context