Get the App

Generate an RFC 6902 JSON Patch from Large Files

A diff report tells a human what changed. A JSON Patch tells a machine how to make the change — and that difference decides which one you need. This guide covers what RFC 6902 actually is, the honest small-scale generators, and how to produce a patch from files that in-memory tools cannot even open.

What RFC 6902 Is, and When You Want One

An RFC 6902 JSON Patch is a JSON array of operations — add, remove, replace, move, copy, test — each addressing its target with an RFC 6901 JSON Pointer:

[
  { "op": "replace", "path": "/users/3/email", "value": "new@example.com" },
  { "op": "add",     "path": "/users/17",      "value": { "id": 9432, "name": "Ada" } },
  { "op": "remove",  "path": "/legacy" }
]

Applying the operations in order to document A produces document B. That machine contract is the point, and it is what makes JSON Patch the right format in three recurring situations:

  • API PATCH bodies. HTTP PATCH with the application/json-patch+json media type sends exactly the operations above — the standard way to update a resource without resending it.
  • Config sync. Ship the delta between two versions of a large configuration instead of the whole file, and apply it mechanically on the other side.
  • Audit trails. A patch is a precise, replayable record of what changed between two snapshots — better evidence than a prose summary.

Its simpler sibling, JSON Merge Patch (RFC 7386), is a partial document merged over the target. Merge Patch is easier to write by hand but cannot address individual array elements — touching any part of an array means shipping the whole array — so for array-heavy data, RFC 6902 is the expressive format.

The Small-Scale Generators, Honestly

At config-file and API-response sizes, two established libraries generate RFC 6902 patches well, and if your files are small they are the pragmatic choice.

python-jsonpatch

The jsonpatch package generates and applies patches, both as a library and as bundled command-line tools:

pip install jsonpatch
jsondiff a.json b.json > changes.json      # generate a patch
jsonpatch a.json changes.json              # apply: prints the patched document

Or in code:

import json, jsonpatch
a = json.load(open("a.json"))
b = json.load(open("b.json"))
patch = jsonpatch.make_patch(a, b)
print(patch)

Correct, standard, everywhere Python is. Its scale limit is the universal one: both documents are parsed fully into Python objects in memory, so the size ceiling is set by your RAM and the parsed trees' overhead, exactly as described in JSON Diff Out of Memory.

jsondiffpatch

jsondiffpatch computes deltas in its own compact format, and ships a formatter that converts a delta into RFC 6902 operations. An illustrative sketch, per the jsondiffpatch documentation:

import * as jsondiffpatch from 'jsondiffpatch';
import * as jsonpatchFormatter from 'jsondiffpatch/formatters/jsonpatch';

// a and b are already-parsed documents, e.g.:
// const a = JSON.parse(fs.readFileSync('a.json', 'utf8'));
const differ = jsondiffpatch.create({ objectHash: (o) => o.id });
const delta = differ.diff(a, b);
const ops = jsonpatchFormatter.format(delta);   // RFC 6902 operations

It is a genuinely strong tool in its range. On our benchmark machine — all numbers in this article: an 8 GiB, 4-core Linux container, SATA SSD, cold page cache, single first runs, August 2026 — jsondiffpatch 0.7.6 diffed 83 MB per side in 2.5 seconds (at 1.3 GB of RAM) and 165 MB per side in 8 seconds (at 3.9 GB). The 83 MB NDJSON benchmark pair was supplied to it as a JSON-array twin holding the identical records at the same size — jsondiffpatch, like the other Node diff tools, cannot read NDJSON. The hard stop comes at roughly 512 MiB per file: V8 caps a single string there, readFileSync in utf8 cannot return a larger file, and the run fails with ERR_STRING_TOO_LONG — on the 837 MB test file (also supplied as a JSON-array twin), in under a second. More RAM does not move that wall.

gjxdiff --patch: Patches from Files Bigger Than RAM

gjxdiff is a structural JSON and NDJSON diff CLI for Linux x86-64 whose engine memory-maps its inputs and keeps working state on disk under a fixed memory budget. --patch makes that engine emit an RFC 6902 patch:

gjxdiff --patch changes.json a.json b.json      # diff report plus patch file
gjxdiff --patch - a.json b.json > changes.json  # patch to stdout instead of the report

The contract is strict, and every part of it is deliberate:

  • The patch transforms A into B, or there is no patch. When a correct patch cannot be proven, gjxdiff refuses with exit code 2 and leaves no file — never a silently partial patch that would apply cleanly and produce a document that is neither A nor B.
  • Two concrete refusal cases. A needed record has no concrete RFC 6901 address; or keyed record pairing absorbed a reorder — an order difference the patch would have to express, but which keyed matching deliberately erased. For the reorder case, rerun with --key none so records align by position and the patch expresses the reorder explicitly.
  • An empty diff yields []. And representation-only differences — 1.0 versus 1.00, formatting, key order — give exit 0 with a [] patch: zero operations already turn A into a document equal to B.
  • The write is atomic. The patch file appears complete or not at all; an interrupted run cannot leave a half-written patch behind.
  • No partial patches by construction. --patch conflicts with the flags that truncate or filter output — --max-diffs, --only, --profile quick — each combination exits 2 with a message.
Why Refusal Is a Feature

A diff report that misses something wastes your time. A patch that misses something corrupts data downstream, because patches get applied, often unattended. That asymmetry is why gjxdiff fails loudly with exit 2 rather than writing its best guess: the exit code is the difference between a pipeline that stops and a pipeline that quietly ships a wrong document.

Measured Performance

The patch generator runs on the same engine, under the same fixed memory budget, as the diff itself — that is what lets it handle inputs in-memory generators cannot open. These are the engine's measured diff runs on the benchmark machine, gjxdiff 0.8.0, cold:

Input Time Peak RAM
2 MB pair 0.11 s 14 MB
83 MB per side, NDJSON, 310,000 records 2.8 s 478 MB
165 MB per side, real-world GeoJSON 5.0 s 920 MB
837 MB per side, NDJSON, 3.1 million records 26 s 3.9 GB
1.36 GB per side 61 s 4.7 GB (1.4 GB of it heap; the rest is reclaimable file-backed mmap pages the kernel counts)

Correctness on the same runs: on the three pairs with planted differences (83 MB, 165 MB, and 837 MB per side), gjxdiff found all 20 planted differences with zero false positives; a change-dense 156 MB pair in the same benchmark was verified by exact record accounting (348,685 report records for 174,342 changed records), and the 1.36 GB pair is a scale run with no planted ground truth. The full tool-by-tool story behind these numbers is in How to Diff Large JSON Files on Linux; the keyed pairing that decides how NDJSON records line up before a patch is generated is covered in How to Compare NDJSON Files When the Record Order Changed.

Applying the Patch

The output is standard RFC 6902, so applying it needs no gjxdiff at all. On the command line, python-jsonpatch's bundled tool does it:

jsonpatch a.json changes.json > b-rebuilt.json

Every major language has an RFC 6902 implementation, and servers accept the same array directly as an HTTP PATCH body with the application/json-patch+json media type. Generate with whichever tool fits your file size; the patch itself is portable.

Frequently Asked Questions

What is an RFC 6902 JSON Patch?

A standard format for describing changes between two JSON documents: a JSON array of operations — add, remove, replace, move, copy, test — each addressing its target with an RFC 6901 JSON Pointer such as /users/3/email. Applying the operations in order to document A produces document B. It is the payload format behind HTTP PATCH with the application/json-patch+json media type.

What is the difference between JSON Patch (RFC 6902) and JSON Merge Patch (RFC 7386)?

JSON Patch is an ordered list of explicit operations with pointer addresses; it can express array edits, moves, and tests precisely. JSON Merge Patch is a partial document that gets merged over the target; it is simpler but cannot address individual array elements — replacing any part of an array means shipping the whole array. For array-heavy data, RFC 6902 is the expressive one.

How do I generate a JSON Patch from two very large JSON files?

In-memory generators (python-jsonpatch, jsondiffpatch) parse both documents into RAM, which sets a size ceiling; Node-based ones cannot read files above V8's roughly 512 MiB string cap at all. gjxdiff --patch changes.json a.json b.json builds the patch under the same fixed memory budget as its diff, so it works on files bigger than RAM — the engine's measured diff runs reach 1.36 GB per side on an 8 GiB container, and --patch adds the patch-export step on that same engine.

Why does gjxdiff sometimes refuse to write a patch?

Because a patch it cannot prove correct is worse than no patch. gjxdiff refuses with exit code 2 and leaves no file when a needed record has no concrete RFC 6901 address, or when keyed record pairing absorbed a reorder — an order difference a patch would have to express but keyed matching deliberately erased. In the reorder case, rerun with --key none to align records by position; the patch then expresses the reorder explicitly.

What does an empty patch [] mean?

That no operations are needed: the documents are semantically identical. This includes representation-only differences — 1.0 versus 1.00, formatting, key order — which produce exit code 0 and a [] patch, because applying zero operations to A already yields a document equal to B.

How do I apply an RFC 6902 patch to a JSON file?

Any RFC 6902 implementation works; the patch format is the standard, not the tool. On the command line, python-jsonpatch installs a jsonpatch command: jsonpatch a.json changes.json prints the patched document. Libraries exist for every major language, and servers accept these patches directly as HTTP PATCH bodies with the application/json-patch+json media type.

Can I generate a patch for only some of the changes?

Not with gjxdiff, by design. A patch must transform A into B completely, so --patch conflicts with the flags that truncate or filter output (--max-diffs, --only, --profile quick) — each combination exits with code 2 and a message. A partial patch would apply cleanly and silently produce a document that is neither A nor B.

Conclusion

For small files, python-jsonpatch and jsondiffpatch generate correct RFC 6902 patches with minimal ceremony, and they are the right tools at that scale. Past their in-memory ceilings — the heap for Python, the roughly 512 MiB string cap for Node — the job needs an engine that never loads the documents at all. gjxdiff --patch is that engine's export: a patch built from files bigger than RAM, that transforms A into B or refuses loudly, and never anything in between. The binary and the full manual are at github.com/kotysoft/gjxdiff.

RFC 6902 Patches at Any File Size

gjxdiff is a single static binary for Linux x86-64 — free for individuals and organizations under 100 people.

Get gjxdiff on GitHub