X Xerobit

JSON Merge Patch — Partial Updates to JSON Documents (RFC 7396)

JSON Merge Patch (RFC 7396) is a simple format for partial JSON updates. A null value removes a key; other values replace or add. Here's how JSON Merge Patch works, how it...

Mian Ali Khalid · · 4 min read
Use the tool
JSON Diff
Compare two JSON objects structurally with field-by-field diff.
Open JSON Diff →

JSON Merge Patch (RFC 7396) defines a simple way to describe partial changes to a JSON document. You send only the keys you want to change — a null value removes a key, any other value replaces or adds it.

Use the JSON Diff Tool to compare JSON documents and visualize differences.

How JSON Merge Patch works

The merge patch is itself a JSON object. When applied to a target document:

  • If a key in the patch is null — remove that key from the target
  • If a key exists in the patch — set that key in the target to the patch’s value
  • If a key doesn’t exist in the patch — leave the target unchanged

Example

Original document:

{
  "title": "Hello World",
  "author": "Alice",
  "tags": ["intro", "tutorial"],
  "metadata": {
    "views": 100,
    "published": true
  }
}

Merge patch:

{
  "title": "Hello World Updated",
  "author": null,
  "metadata": {
    "views": 250
  }
}

Result:

{
  "title": "Hello World Updated",
  "tags": ["intro", "tutorial"],
  "metadata": {
    "views": 250
  }
}

Changes made:

  • title updated to new value
  • author removed (null)
  • tags unchanged (not in patch)
  • metadata.views updated — but metadata.published is GONE (see caveat below)

The nested object caveat

This is the biggest gotcha: merge patch replaces nested objects entirely, it doesn’t merge them recursively:

// Original:
{
  "settings": {
    "theme": "dark",
    "language": "en",
    "notifications": true
  }
}

// Patch:
{
  "settings": {
    "theme": "light"
  }
}

// Result — language and notifications are GONE:
{
  "settings": {
    "theme": "light"
  }
}

To update a single nested key while preserving others, your patch must include all existing keys you want to keep, or restructure your data.

Implementing JSON Merge Patch

JavaScript

function mergePatch(target, patch) {
  if (typeof patch !== 'object' || patch === null) {
    return patch;
  }
  
  const result = typeof target === 'object' && target !== null
    ? { ...target }
    : {};
  
  for (const [key, value] of Object.entries(patch)) {
    if (value === null) {
      delete result[key];
    } else {
      result[key] = mergePatch(result[key], value);
    }
  }
  
  return result;
}

const original = { a: 1, b: 2, c: { d: 3, e: 4 } };
const patch = { b: null, c: { d: 10 } };

mergePatch(original, patch);
// { a: 1, c: { d: 10 } }
// Note: c.e is gone because c was replaced, not merged

Python

import copy

def merge_patch(target, patch):
    if not isinstance(patch, dict):
        return patch
    
    result = copy.deepcopy(target) if isinstance(target, dict) else {}
    
    for key, value in patch.items():
        if value is None:
            result.pop(key, None)
        else:
            result[key] = merge_patch(result.get(key), value)
    
    return result

original = {"a": 1, "b": {"c": 2, "d": 3}}
patch = {"b": {"c": 10}, "e": 5}

merge_patch(original, patch)
# {"a": 1, "b": {"c": 10}, "e": 5}
# b.d is gone

Node.js with json-merge-patch library

import { apply } from 'json-merge-patch';

const original = { a: 1, b: 2, c: 3 };
const patch = { b: null, d: 4 };

apply(original, patch);
// { a: 1, c: 3, d: 4 }

Using JSON Merge Patch in REST APIs

HTTP PATCH requests with Content-Type application/merge-patch+json:

PATCH /articles/123 HTTP/1.1
Content-Type: application/merge-patch+json

{
  "title": "Updated Title",
  "draft": null
}

Express.js handler:

app.patch('/articles/:id', async (req, res) => {
  const patch = req.body;  // JSON Merge Patch
  const existing = await db.articles.findById(req.params.id);
  const updated = mergePatch(existing, patch);
  await db.articles.update(req.params.id, updated);
  res.json(updated);
});

JSON Merge Patch vs JSON Patch (RFC 6902)

FeatureMerge Patch (RFC 7396)JSON Patch (RFC 6902)
FormatJSON objectArray of operations
OperationsReplace, delete, add (implicit)add, remove, replace, move, copy, test
Can’t remove from arraysYes — replacing array removes itNo — can target array indices
Nested objectsReplaces, not mergesCan target specific paths
ComplexitySimpleMore complex
Use caseSimple partial updatesFine-grained operations
// JSON Patch equivalent of setting title and removing author:
[
  { "op": "replace", "path": "/title", "value": "New Title" },
  { "op": "remove",  "path": "/author" }
]

When to use each

Use JSON Merge Patch when:

  • Your document is mostly flat (shallow nesting)
  • You want a simple, readable diff format
  • You’re updating a handful of top-level fields
  • You need something easy to implement

Use JSON Patch when:

  • You need to add/remove array items by index
  • You need atomic operations (test before apply)
  • You need move/copy operations
  • You need precise control over deep nested paths

Generating a merge patch

To generate the patch needed to transform document A into document B:

function generateMergePatch(original, updated) {
  if (typeof original !== 'object' || typeof updated !== 'object') {
    return original === updated ? undefined : updated;
  }
  
  const patch = {};
  
  // Keys to remove (in original but not in updated):
  for (const key of Object.keys(original)) {
    if (!(key in updated)) {
      patch[key] = null;
    }
  }
  
  // Keys to add or update:
  for (const [key, value] of Object.entries(updated)) {
    if (JSON.stringify(original[key]) !== JSON.stringify(value)) {
      patch[key] = value;
    }
  }
  
  return Object.keys(patch).length > 0 ? patch : undefined;
}

const a = { x: 1, y: 2, z: { a: 3 } };
const b = { x: 1, z: { a: 4 }, w: 5 };

generateMergePatch(a, b);
// { y: null, z: { a: 4 }, w: 5 }

Related posts

Related tool

JSON Diff

Compare two JSON objects structurally with field-by-field diff.

Written by Mian Ali Khalid. Part of the Data & Format pillar.