RFC 7386 JSON Merge Patch — apply and generate, the direction most merge-patch libraries skip — plus a general-purpose deep merge with configurable array strategies. Zero runtime dependencies, immutable (never mutates its inputs), with a built-in prototype-pollution guard.
Diffing two JSON documents into a patch, not just applying one, is the
harder and less-supported half of RFC 7386. This package does both,
round-trips cleanly, and adds a general deepMerge for the common case of
combining two config-shaped objects (with a choice of array-combination
strategy) — all without touching __proto__/constructor/prototype keys.
npm install @ferrow/deep-merge-patchimport { generateMergePatch, applyMergePatch, deepMerge } from "deep-merge-patch";
const patch = generateMergePatch(v1, v2); // RFC 7386 patch: v1 -> v2
const result = applyMergePatch(v1, patch); // deep-equals v2
deepMerge(a, b, { arrays: "concat" });
deepMerge(a, b, { arrays: { mode: "unionBy", key: "id" } });Applies an RFC 7386 JSON Merge Patch. null in the patch deletes a key;
non-object patch values replace wholesale; object values merge recursively.
Returns a new value — target and patch are never mutated.
Generates the RFC 7386 patch that transforms source into target.
applyMergePatch(source, generateMergePatch(source, target)) deep-equals
target for JSON-shaped values (arrays are replaced wholesale per RFC 7386
— they are never diffed element-wise, same as the spec).
General deep merge, b into a. Plain objects merge key-by-key
recursively; arrays follow options.arrays:
"replace"(default) —b's array wins wholesale."concat"—[...a, ...b].{ mode: "unionBy", key }— arrays of objects are unioned byitem[key]; entries with a matching key on both sides are themselves deep-merged (with the same array strategy),b's new entries are appended.
Exported utilities used internally; useful for callers building their own patch/merge logic on top.
type ArrayStrategy = "replace" | "concat" | { mode: "unionBy"; key: string };
interface DeepMergeOptions { arrays?: ArrayStrategy; }- The prototype-pollution guard rejects
__proto__/constructor/prototypeas own enumerable keys encountered during merge/patch (the realistic vector for JSON.parse'd input). It does not intercept a JS literal like{__proto__: x}, which sets the prototype at object-literal-evaluation time, before this library ever sees the object. generateMergePatchcannot represent "delete then re-add withnullvalue" — RFC 7386 has no way to distinguish "set this key tonull" from "delete this key" when the target's value is legitimatelynull; per spec,nullin a patch always means delete.- Arrays are always atomic in the RFC merge-patch functions (
applyMergePatch/generateMergePatch) — element-wise array handling is only available viadeepMerge'sarraysoption. deepMerge'sunionBystrategy treats a missing/undefined key value as a single shared bucket — items without the union key will collapse together.
Part of the ferrow-toolkit collection · Sponsored by Ferrow