Compare, track, and patch JSON like a boss — TypeScript-first and zero dependencies.
A blazing-fast TypeScript utility that diffs any two JSON values, gives you a clean list of changes (added, removed, modified), and lets you apply those changes back — with full support for nested objects and arrays.
- ✅ Deep diffing — recursively compare nested objects and arrays
- ✅ Type-safe — full TypeScript support with strict mode
- ✅ Patch application — apply changes back to transform JSON
- ✅ Array support — diff arrays by index or identity keys
- ✅ Zero deps — no external packages, just pure TypeScript
- ✅ ESM-first — modern module system with CJS compatibility
npm install @adametherzlab/json-diff-tsOr with Bun:
bun add @adametherzlab/json-diff-ts// REMOVED external import: import { diff, patch } from "@adametherzlab/json-diff-ts";
const before = { name: "Alice", age: 30, city: "NYC" };
const after = { name: "Alice", age: 31, city: "LA", country: "USA" };
const result = diff(before, after);
console.log(result.changes);
// [
// { path: ["age"], type: "changed", oldValue: 30, newValue: 31 },
// { path: ["city"], type: "changed", oldValue: "NYC", newValue: "LA" },
// { path: ["country"], type: "added", newValue: "USA" }
// ]
// Apply changes to get the new state
const patched = patch(before, result.patches);
console.log(patched);
// { name: "Alice", age: 31, city: "LA", country: "USA" }enum ChangeType {
Added = "added",
Removed = "removed",
Changed = "changed"
}type Change = AddedChange | RemovedChange | ChangedChange;interface AddedChange {
readonly type: "added";
readonly path: readonly string[];
readonly newValue: JsonValue;
}interface RemovedChange {
readonly type: "removed";
readonly path: readonly string[];
readonly oldValue: JsonValue;
}interface ChangedChange {
readonly type: "changed";
readonly path: readonly string[];
readonly oldValue: JsonValue;
readonly newValue: JsonValue;
}type JsonValue = JsonPrimitive | JsonArray | JsonObject;JSON primitive types.
type JsonPrimitive = string | number | boolean | null;JSON array type — array of JsonValue.
interface JsonArray extends ReadonlyArray<JsonValue> {}JSON object type — record with string keys and JsonValue values.
interface JsonObject extends Readonly<Record<string, JsonValue>> {}interface DiffOptions {
/** Compare arrays by identity key instead of index. Default: false */
readonly arrayIdentityKey?: string;
/** Treat arrays as ordered (index-based). Default: true */
readonly orderedArrays?: boolean;
/** Max depth for comparison. Default: unlimited */
readonly maxDepth?: number;
}interface PatchOptions {
/** Mutate the original object instead of returning a copy. Default: false */
readonly mutate?: boolean;
}interface DiffResult {
/** List of all changes detected */
readonly changes: readonly Change[];
/** List of patch operations derived from changes */
readonly patches: readonly Patch[];
/** Number of additions */
readonly added: number;
/** Number of removals */
readonly removed: number;
/** Number of modifications */
readonly changed: number;
/** Total number of changes */
readonly total: number;
}A patch operation that can be applied to transform JSON.
type Patch = AddPatch | RemovePatch | ReplacePatch;interface AddPatch {
readonly op: "add";
readonly path: readonly string[];
readonly value: JsonValue;
}interface RemovePatch {
readonly op: "remove";
readonly path: readonly string[];
}interface ReplacePatch {
readonly op: "replace";
readonly path: readonly string[];
readonly value: JsonValue;
}function diff(oldVal: JsonValue, newVal: JsonValue, options?: DiffOptions): DiffResultParameters:
oldVal— The original JSON valuenewVal— The new JSON value to compare againstoptions— Optional configuration for diff behavior
Returns: DiffResult containing all changes and statistics
Example:
const result = diff({ a: 1 }, { a: 2 });
console.log(result.changes); // [{ path: ["a"], type: "changed", oldValue: 1, newValue: 2 }]Diff two objects, detecting added, removed, and changed properties.
function diffObjects(oldObj: JsonObject, newObj: JsonObject, options?: DiffOptions): DiffResultExample:
const result = diffObjects({ x: 1 }, { x: 2, y: 3 });function diffArrays(oldArr: JsonArray, newArr: JsonArray, options?: DiffOptions): DiffResultExample:
const result = diffArrays([1, 2], [1, 3]);function parsePath(pathString: string): readonly string[]Example:
parsePath("a.b.c") // ["a", "b", "c"]
parsePath("a[0].b") // ["a", "0", "b"]
parsePath('a["0"].b') // ["a", "0", "b"]function buildPath(segments: readonly string[]): stringExample:
buildPath(["a", "b", "c"]) // "a.b.c"
buildPath(["a", "0", "b"]) // "a[0].b"function deepEqual(a: JsonValue, b: JsonValue): booleanExample:
deepEqual({ a: 1 }, { a: 1 }) // true
deepEqual([1, 2], [1, 2]) // true
deepEqual({ a: 1 }, { a: 2 }) // falsefunction patch(value: JsonValue, patches: readonly Patch[], options?: PatchOptions): JsonValueParameters:
value— The JsonValue to patchpatches— Array of patches to applyoptions— Patch options
Returns: Patched JsonValue (new copy if mutate is false, same reference if true)
Example:
const original = { a: 1 };
const patched = patch(original, [{ path: ["a"], op: "replace", value: 2 }]);
// original is unchanged, patched is { a: 2 }function applyChanges(value: JsonValue, changes: readonly Change[], options?: PatchOptions): JsonValueExample:
const original = { a: 1 };
const changes = diff(original, { a: 2 });
const patched = applyChanges(original, changes);// REMOVED external import: import { diff } from "@adametherzlab/json-diff-ts";
const usersBefore = [
{ id: 1, name: "Alice" },
{ id: 2, name: "Bob" }
];
const usersAfter = [
{ id: 1, name: "Alice Updated" },
{ id: 3, name: "Charlie" },
{ id: 2, name: "Bob" }
];
const result = diff(usersBefore, usersAfter, { arrayIdentityKey: "id" });
console.log(result.changes);
// Detects: name change for id=1, addition of id=3, keeps id=2 in same position// REMOVED external import: import { parsePath, buildPath, patch } from "@adametherzlab/json-diff-ts";
const pathStr = "users[0].profile.settings.notifications";
const segments = parsePath(pathStr);
// ["users", "0", "profile", "settings", "notifications"]
const restored = buildPath(segments);
// "users[0].profile.settings.notifications"
// Use segments directly in patches
const data = { users: [{ profile: { settings: { notifications: true } } }] };
const patched = patch(data, [
{ op: "replace", path: segments, value: false }
]);For performance-critical scenarios where memory allocation matters:
// REMOVED external import: import { diff, patch } from "@adametherzlab/json-diff-ts";
const largeObject = { /* big data */ };
const updates = { /* changes */ };
const result = diff(largeObject, updates);
patch(largeObject, result.patches, { mutate: true });
// Modifies largeObject directly, no copy allocationSee CONTRIBUTING.md
MIT (c) AdametherzLab