Skip to content

Latest commit

 

History

4 Commits

Folders and files

NameName
Last commit message
Last commit date
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

deep-merge-patch

CI

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.

Why

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.

Install

npm install @ferrow/deep-merge-patch

Quickstart

import { 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" } });

API

applyMergePatch(target: unknown, patch: unknown): unknown

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.

generateMergePatch(source: unknown, target: unknown): unknown

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).

deepMerge(a: unknown, b: unknown, options?: DeepMergeOptions): unknown

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 by item[key]; entries with a matching key on both sides are themselves deep-merged (with the same array strategy), b's new entries are appended.

isPlainObject(value: unknown): value is Record<string, unknown>

deepEqual(a: unknown, b: unknown): boolean

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; }

Limits

  • The prototype-pollution guard rejects __proto__/constructor/prototype as 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.
  • generateMergePatch cannot represent "delete then re-add with null value" — RFC 7386 has no way to distinguish "set this key to null" from "delete this key" when the target's value is legitimately null; per spec, null in a patch always means delete.
  • Arrays are always atomic in the RFC merge-patch functions (applyMergePatch/ generateMergePatch) — element-wise array handling is only available via deepMerge's arrays option.
  • deepMerge's unionBy strategy 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

About

RFC 7386 JSON Merge Patch apply+generate, deep merge with array strategies, prototype-pollution guard, immutable

Topics

Resources

Stars

0 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages