← All articles

JSON Patch vs JSON Merge Patch: RFC 6902 vs RFC 7396 for API Partial Updates

Choose between JSON Patch and JSON Merge Patch with concrete API examples: null handling, arrays, JSON Pointer paths, test operations, ETags, validation, idempotency, and server-side safety checks.

PATCH /users/123 sounds simple until the first real edge case lands in production. One client wants to update a display name. Another wants to remove a field. A mobile app wants to append one tag without sending the whole array. A background job wants to update a nested setting only if nobody changed it first.

That is where the two JSON patch standards split:

  • JSON Merge Patch (RFC 7396) sends a partial object that is merged into the target document.
  • JSON Patch (RFC 6902) sends an ordered list of operations such as replace, remove, add, and test.

The short version: use JSON Merge Patch for simple object-shaped updates where null means "delete this field." Use JSON Patch when you need precise array edits, real null values, conditional writes, replayable change logs, or JSON Pointer paths.

Quick Decision Table

Requirement Better choice Why
Update a few profile fields JSON Merge Patch The body looks like the resource and is easy to read.
Delete a field by sending null JSON Merge Patch null is the built-in delete signal.
Store an actual null field value JSON Patch Merge Patch treats object-member null as deletion.
Append, insert, remove, or replace one array item JSON Patch Paths such as /tags/- and /items/2 target array positions.
Move or copy a value inside the document JSON Patch move and copy are first-class operations.
Reject the update unless the document is in an expected state JSON Patch test can express a precondition inside the patch body.
Accept hand-written simple settings updates JSON Merge Patch Fewer concepts for clients to learn.
Build an audit log or migration stream JSON Patch The operation list is closer to a change record.

If your endpoint does both, do not guess from the body shape. Branch on Content-Type.

The HTTP Part: PATCH Is the Method, Not the Format

HTTP PATCH says "apply a partial modification." It does not define the body format. Your media type does that.

PATCH /users/u_123 HTTP/1.1
Content-Type: application/merge-patch+json
If-Match: "profile-v7"
PATCH /users/u_123 HTTP/1.1
Content-Type: application/json-patch+json
If-Match: "profile-v7"

Practical server rules:

  • Return 415 Unsupported Media Type when the Content-Type is not supported.
  • Validate the patch body before applying it.
  • Apply the patch against the current stored document, not a partial ORM object.
  • Re-run normal business validation after the patch.
  • Use If-Match ETags, a version field, or JSON Patch test operations to avoid lost updates.
  • Return the updated resource or a fresh ETag if clients need to continue editing.

PATCH is not automatically idempotent. A JSON Patch operation like add to /tags/- appends each time it is retried. A Merge Patch that sets "displayName": "Ada" is idempotent in practice because applying the same overlay twice leaves the same value.

JSON Merge Patch: A Partial Object Overlay

JSON Merge Patch is the body format most teams expect when they say "partial update."

Target document:

{
  "displayName": "Ada",
  "email": "ada@example.com",
  "phone": "+1-555-0100",
  "settings": {
    "theme": "dark",
    "marketingEmail": true
  },
  "tags": ["admin", "beta"]
}

Merge Patch body:

{
  "displayName": "Ada Lovelace",
  "phone": null,
  "settings": {
    "marketingEmail": false
  }
}

Result:

{
  "displayName": "Ada Lovelace",
  "email": "ada@example.com",
  "settings": {
    "theme": "dark",
    "marketingEmail": false
  },
  "tags": ["admin", "beta"]
}

The important rules are small:

  • Object members present in the patch are applied to the target.
  • Object members with null are removed from the target object.
  • Object members not mentioned are left alone.
  • Nested objects are merged recursively.
  • Arrays are replaced as complete values.
  • If the patch itself is not an object, it replaces the whole target.

That last rule is easy to miss. A Merge Patch body of "closed" or null does not "merge" with the target object. It replaces the entire target document with that scalar value. Many API teams disallow non-object Merge Patch bodies at the route layer because resource endpoints usually expect object resources.

The Merge Patch Null Trap

In Merge Patch, an object member set to null means "remove this member." That is convenient for optional fields:

{
  "phone": null
}

But it is a bad fit when null is a meaningful value in your data model.

Imagine this field:

{
  "middleName": null
}

If a client sends this Merge Patch:

{
  "middleName": null
}

the server removes middleName; it does not store an explicit JSON null. If your API must distinguish "field is absent" from "field is present and null," choose JSON Patch or design a custom update shape.

Merge Patch Arrays Are Whole-Value Replacements

Merge Patch cannot append to an array, remove one array item, or insert at an index. This patch replaces the entire tags array:

{
  "tags": ["admin", "beta", "trial"]
}

That is fine for small arrays edited as a complete field, such as notification channels or selected UI preferences. It is risky for large or concurrently edited arrays, such as cart items, collaborators, access rules, or ordered workflow steps.

If two clients both read ["admin", "beta"], then one adds "trial" while another removes "beta", whole-array replacement can overwrite one of those edits unless you also enforce version checks.

JSON Patch: Ordered Operations With Paths

JSON Patch is more explicit. The request body is an array. Each item says what operation to run and where to run it.

[
  { "op": "replace", "path": "/displayName", "value": "Ada Lovelace" },
  { "op": "remove", "path": "/phone" },
  { "op": "replace", "path": "/settings/marketingEmail", "value": false },
  { "op": "add", "path": "/tags/-", "value": "trial" }
]

The path is a JSON Pointer. It is not a JavaScript property path and not a JSONPath expression.

JSON Pointer path Meaning
"" The whole document
/displayName Top-level displayName member
/settings/theme Nested theme member
/tags/0 First array item
/tags/- Append to the end of an array, valid for add
/a~1b Key named a/b
/tilde~0key Key named tilde~key

Pointer escaping matters:

  • ~ becomes ~0
  • / becomes ~1

So a key named billing/address is /billing~1address, not /billing/address.

JSON Patch Operations in Practice

JSON Patch has six operations.

Operation Required fields Practical note
add op, path, value Adds an object member, inserts into an array, or appends with /-.
remove op, path The target path must exist. Use it when deletion should fail on stale paths.
replace op, path, value The target path must exist. Use it for ordinary field updates.
move op, from, path Removes the value at from and places it at path. Useful but easy to restrict for public APIs.
copy op, from, path Copies a value without removing the source.
test op, path, value Asserts the current value. If it fails, the patch should be rejected.

A test operation is the part many comparison articles underplay. It lets the client say, "Only apply this if the resource still looks like what I saw."

[
  { "op": "test", "path": "/version", "value": 7 },
  { "op": "replace", "path": "/settings/marketingEmail", "value": false },
  { "op": "replace", "path": "/version", "value": 8 }
]

That does not replace ETags for every API, but it is useful when the relevant precondition is inside the JSON document itself. For general HTTP caching and concurrency, If-Match with an ETag is still the cleaner protocol-level guard.

Side-by-Side API Example

Suppose a user edits three things:

  • Change displayName.
  • Remove phone.
  • Add a new tag.

Merge Patch request:

PATCH /users/u_123 HTTP/1.1
Content-Type: application/merge-patch+json

{
  "displayName": "Ada Lovelace",
  "phone": null,
  "tags": ["admin", "beta", "trial"]
}

JSON Patch request:

PATCH /users/u_123 HTTP/1.1
Content-Type: application/json-patch+json

[
  { "op": "replace", "path": "/displayName", "value": "Ada Lovelace" },
  { "op": "remove", "path": "/phone" },
  { "op": "add", "path": "/tags/-", "value": "trial" }
]

The Merge Patch body is easier to read, but it has to send the whole final tags array. The JSON Patch body is noisier, but it describes the actual change: append one item.

That difference matters when arrays are long, ordered, collaborative, or security-sensitive.

Validation Checklist for a Server Endpoint

Whichever format you choose, never apply a patch as raw trustable data. Treat it as untrusted input that describes a mutation.

For JSON Merge Patch:

  • Require Content-Type: application/merge-patch+json.
  • Decide whether the patch body must be an object for this route.
  • Restrict which fields can be changed.
  • Treat null as deletion only for fields where deletion is allowed.
  • Recompute derived fields server-side instead of accepting them from the patch.
  • Validate the final resource after merge.
  • Protect concurrent writes with ETags, versions, or row locks.

For JSON Patch:

  • Require Content-Type: application/json-patch+json.
  • Require the body to be an array of operation objects.
  • Limit operation count and document size.
  • Allow only the operation types your API actually supports.
  • Validate every path and from pointer against an allowlist.
  • Reject paths into server-owned fields such as /id, /role, /createdAt, or /billing/plan unless the route explicitly owns those changes.
  • Apply the patch atomically in your persistence layer.
  • Validate the final resource after all operations succeed.

The allowlist step is not bureaucracy. Without it, a generic JSON Patch endpoint can become a generic privilege-editing endpoint.

Status Codes That Make Debugging Easier

You do not need a perfect status-code taxonomy, but consistent errors save client teams hours.

Problem Reasonable status
Unsupported patch media type 415 Unsupported Media Type
Body is not valid JSON 400 Bad Request
Patch shape is invalid for the media type 400 Bad Request
JSON Pointer path is not allowed by this API 403 Forbidden or 422 Unprocessable Content
Target path does not exist for remove or replace 409 Conflict or 422 Unprocessable Content
test operation fails 409 Conflict
If-Match ETag fails 412 Precondition Failed
Final resource violates business validation 422 Unprocessable Content

The most important part is returning a precise error body: operation index, path, reason, and a safe message. Do not echo sensitive field values.

Implementation Notes for JavaScript

For JSON Patch, use a library that implements RFC 6902 and gives you control over validation, mutation, and error handling. fast-json-patch is a common JavaScript option, but the library choice matters less than how you wrap it.

import { applyPatch } from 'fast-json-patch';

const allowedPaths = new Set([
  '/displayName',
  '/phone',
  '/settings/marketingEmail',
  '/tags/-',
]);

function assertAllowedPatch(operations) {
  if (!Array.isArray(operations)) {
    throw new Error('JSON Patch body must be an array');
  }

  for (const [index, operation] of operations.entries()) {
    if (!allowedPaths.has(operation.path)) {
      throw new Error(`Operation ${index} uses a path this endpoint does not allow`);
    }
  }
}

function applyUserPatch(document, operations) {
  assertAllowedPatch(operations);

  const clone = structuredClone(document);
  return applyPatch(clone, operations, true, false).newDocument;
}

For Merge Patch, the algorithm is small, but a tested package is still helpful if this endpoint is public. If you implement it yourself, be explicit about arrays and null:

function applyMergePatch(target, patch) {
  if (patch === null || typeof patch !== 'object' || Array.isArray(patch)) {
    return patch;
  }

  const output =
    target && typeof target === 'object' && !Array.isArray(target)
      ? { ...target }
      : {};

  for (const [key, value] of Object.entries(patch)) {
    if (value === null) {
      delete output[key];
    } else {
      output[key] = applyMergePatch(output[key], value);
    }
  }

  return output;
}

That sample follows the RFC shape, but production code still needs field allowlists, size limits, validation, and persistence-level concurrency checks.

Which One Should You Pick?

Pick JSON Merge Patch when:

  • Your resources are mostly objects with scalar fields.
  • Clients are humans or simple apps that benefit from readable request bodies.
  • null as deletion is acceptable for your data model.
  • Arrays can be replaced as a whole.
  • You do not need move, copy, or path-level preconditions.

Pick JSON Patch when:

  • Arrays need item-level edits.
  • null is a real value you need to store.
  • You need test operations for conditional updates.
  • You want a replayable operation log.
  • Clients already produce diffs between before and after documents.
  • You need to reject stale paths instead of silently overlaying fields.

Pick neither when:

  • The update is a business command, not a document edit.
  • The endpoint should expose only one action, such as POST /invoices/{id}/void.
  • The server must derive all changed fields from a narrow input.
  • Public clients should not see or target your internal JSON shape.

For example, "change my display name" can be Merge Patch. "Append a collaborator if the version is still 7" is JSON Patch. "Upgrade this workspace plan and bill the customer" should probably be a domain command, not a generic patch.

Use JSON Diff Before You Ship a Patch

When you are debugging a partial update endpoint, diff the before and after resource. It shows whether your patch changed only what you intended.

Frequently Asked Questions

What is the difference between JSON Patch and JSON Merge Patch?

JSON Merge Patch is a partial object overlay where object-member null means delete. JSON Patch is an ordered array of operations that target JSON Pointer paths. Merge Patch is easier to read; JSON Patch is more precise.

Which one should I use for a REST API?

Use JSON Merge Patch for simple object field updates such as profile settings. Use JSON Patch when clients need array edits, explicit remove operations, test preconditions, replayable change logs, or the ability to set a field to JSON null.

Why can JSON Merge Patch not set a field to null?

Inside an object Merge Patch, null is the deletion marker. Sending { "phone": null } removes phone; it does not store an explicit null value. If explicit null matters, use JSON Patch or a custom command body.

How do I edit one array element?

Use JSON Patch with a path such as /items/2, or append with /items/-. JSON Merge Patch can only replace the entire array value.

Are JSON Patch operations atomic?

For HTTP PATCH endpoints, the server should apply the patch atomically: either all operations succeed or the resource is not changed. Be careful with libraries that mutate an object while applying operations; clone or wrap the operation in a transaction.

Is PATCH idempotent?

The HTTP PATCH method is not automatically idempotent. A Merge Patch that sets fixed fields usually is idempotent, but a JSON Patch add to /items/- is not because every retry appends another item.

What Content-Type should I send?

Send application/merge-patch+json for JSON Merge Patch and application/json-patch+json for JSON Patch. Servers should reject the wrong media type instead of guessing.

Should public APIs expose every JSON Pointer path?

No. A JSON Patch endpoint should validate every path and from pointer against an allowlist. Otherwise clients may edit server-owned fields such as IDs, roles, timestamps, or billing state.

Related Tools & Reading

Sources

  • RFC 5789 - HTTP PATCH method semantics.
  • RFC 6901 - JSON Pointer path syntax and escaping.
  • RFC 6902 - JSON Patch operation format.
  • RFC 7396 - JSON Merge Patch processing rules.
  • fast-json-patch - one common JavaScript implementation of RFC 6902.

Last reviewed July 2026.