https://github.com/marcolink/generate-json-patch
A simple function to diff any object and generate a JSON Patch
https://github.com/marcolink/generate-json-patch
generator javascript json jsonpatch typescript
Last synced: 12 days ago
JSON representation
A simple function to diff any object and generate a JSON Patch
- Host: GitHub
- URL: https://github.com/marcolink/generate-json-patch
- Owner: marcolink
- License: mit
- Created: 2023-07-14T07:25:29.000Z (over 2 years ago)
- Default Branch: main
- Last Pushed: 2026-02-12T11:10:51.000Z (29 days ago)
- Last Synced: 2026-02-12T18:13:07.538Z (29 days ago)
- Topics: generator, javascript, json, jsonpatch, typescript
- Language: TypeScript
- Homepage:
- Size: 273 KB
- Stars: 13
- Watchers: 1
- Forks: 3
- Open Issues: 1
-
Metadata Files:
- Readme: README.md
- License: LICENSE
Awesome Lists containing this project
README
# generate-json-patch
Create [RFC 6902](https://datatracker.ietf.org/doc/html/rfc6902/) compliant JSON Patch objects based on two given [JSON](https://www.ecma-international.org/publications-and-standards/standards/ecma-404/) objects with a configurable interface.
[](https://npmjs.org/package/generate-json-patch)
[](https://npmjs.org/package/generate-json-patch)
[](https://npmjs.org/package/generate-json-patch)
[](https://github.com/marcolink/generate-json-patch/actions/workflows/test.yml)
[](https://github.com/marcoxlink/generate-json-patch/blob/main/package.json)
[](http://www.typescriptlang.org/)
# TL;DR
- Can diff any two [JSON](https://www.ecma-international.org/publications-and-standards/standards/ecma-404/) compliant objects - returns differences as [JSON Patch](http://jsonpatch.com/).
- Elegant array diffing by providing an `objectHash` to match array elements
- Ignore specific keys by providing a `propertyFilter`
- LCS-based move detection (or disable moves with `array.ignoreMove`)
- Limit traversal with `maxDepth` to collapse deep trees into a single replace
- :paw_prints: ***Is it small?*** Zero dependencies - it's ~**3 KB** (minified).
- Ships ESM + CJS builds with types
- :crystal_ball: ***Is it fast?*** I haven't done any performance comparison yet.
- The interface is inspired by [jsondiffpatch](https://github.com/benjamine/jsondiffpatch)
- **100%** Typescript
# Installation
Works on node and browser environments.
```bash
npm install generate-json-patch
```
# Usage
```typescript
import { generateJSONPatch } from 'generate-json-patch';
const before = {
manufacturer: 'Ford',
model: 'Granada',
year: 1972,
colors: ['red', 'silver', 'yellow'],
engine: [
{ name: 'Cologne V6 2.6', hp: 125 },
{ name: 'Cologne V6 2.0', hp: 90 },
{ name: 'Cologne V6 2.3', hp: 108 },
{ name: 'Essex V6 3.0', hp: 138 },
],
};
const after = {
manufacturer: 'Ford',
model: 'Granada',
year: 1974,
colors: ['red', 'silver', 'yellow'],
engine: [
{ name: 'Essex V6 3.0', hp: 138 },
{ name: 'Cologne V6 2.6', hp: 125 },
{ name: 'Cologne V6 2.3', hp: 108 },
{ name: 'Cologne V6 2.0', hp: 90 },
],
};
const patch = generateJSONPatch(before, after);
console.log(patch);
// [
// { op: 'replace', path: '/year', value: 1974 },
// { op: 'move', from: '/engine/3', path: '/engine/0' },
// ]
```
## Configuration
`generateJSONPatch(before, after, config?)` accepts the options below. The examples reuse the same payload shown in the Usage section.
### `objectHash`
Match array elements by a stable hash instead of position. Useful to detect moves and edits for arrays of objects.
```typescript
import { generateJSONPatch, type JsonValue, type ObjectHashContext, pathInfo } from 'generate-json-patch';
const before = {
manufacturer: 'Ford',
model: 'Granada',
year: 1972,
colors: ['red', 'silver', 'yellow'],
engine: [
{ name: 'Cologne V6 2.6', hp: 125 },
{ name: 'Cologne V6 2.0', hp: 90 },
{ name: 'Cologne V6 2.3', hp: 108 },
{ name: 'Essex V6 3.0', hp: 138 },
],
};
const after = {
manufacturer: 'Ford',
model: 'Granada',
year: 1974,
colors: ['red', 'silver', 'yellow'],
engine: [
{ name: 'Essex V6 3.0', hp: 138 },
{ name: 'Cologne V6 2.6', hp: 125 },
{ name: 'Cologne V6 2.3', hp: 108 },
{ name: 'Cologne V6 2.0', hp: 90 },
],
};
const patch = generateJSONPatch(before, after, {
objectHash(value: JsonValue, context: ObjectHashContext) {
const { length, last } = pathInfo(context.path);
if (length === 2 && last === 'engine') {
// keep engine comparisons stable by model name
// @ts-expect-error JsonValue does not guarantee shape
return value?.name;
}
// default to position for other arrays
return context.index.toString();
},
});
console.log(patch);
// [
// { op: 'replace', path: '/year', value: 1974 },
// { op: 'move', from: '/engine/3', path: '/engine/0' },
// ]
```
### `propertyFilter`
Skip properties when diffing. Return `false` to ignore a field.
```typescript
const before = {
manufacturer: 'Ford',
model: 'Granada',
year: 1972,
vin: 'secret-123',
};
const after = {
manufacturer: 'Ford',
model: 'Granada',
year: 1974,
vin: 'secret-456',
};
const patch = generateJSONPatch(before, after, {
propertyFilter(propertyName) {
return propertyName !== 'vin';
},
});
console.log(patch);
// [
// { op: 'replace', path: '/year', value: 1974 }
// ]
```
### `array.ignoreMove`
Prevent move operations if order does not matter to you. The resulting patch will not reorder arrays.
```typescript
const before = {
manufacturer: 'Ford',
model: 'Granada',
year: 1972,
engine: [
{ name: 'Cologne V6 2.6', hp: 125 },
{ name: 'Cologne V6 2.0', hp: 90 },
{ name: 'Cologne V6 2.3', hp: 108 },
{ name: 'Essex V6 3.0', hp: 138 },
],
};
const after = {
manufacturer: 'Ford',
model: 'Granada',
year: 1972,
engine: [
{ name: 'Essex V6 3.0', hp: 138 },
{ name: 'Cologne V6 2.6', hp: 125 },
{ name: 'Cologne V6 2.3', hp: 108 },
{ name: 'Cologne V6 2.0', hp: 90 },
],
};
const unorderedPatch = generateJSONPatch(before, after, {
objectHash: (value: any) => value.name,
array: { ignoreMove: true },
});
console.log(unorderedPatch);
// []
```
### `maxDepth`
Stop descending deeper than a given path depth. When the limit is reached, a `replace` is emitted for that subtree.
```typescript
const before = {
manufacturer: 'Ford',
model: 'Granada',
year: 1972,
specs: {
trim: 'Base',
colorOptions: ['red', 'silver', 'yellow'],
},
};
const after = {
manufacturer: 'Ford',
model: 'Granada',
year: 1974,
specs: {
trim: 'Ghia',
colorOptions: ['red', 'silver', 'yellow'],
},
};
const patch = generateJSONPatch(before, after, { maxDepth: 2 });
console.log(patch);
// [
// {
// op: 'replace',
// path: '/specs',
// value: { trim: 'Ghia', colorOptions: ['red', 'silver', 'yellow'] },
// },
// { op: 'replace', path: '/year', value: 1974 },
// ]
```
### Patch Context
Both config functions (`objectHash`, `propertyFilter`) receive a context as the second parameter to drive fine-grained decisions:
- `side`: `'left' | 'right'` indicating the value being inspected
- `path`: JSON Pointer-style path to the current value
- `index`: only on `objectHash`, giving the array index being processed
See the `objectHash` example above for how `pathInfo` can be combined with the context to scope hashing logic.
### How moves are found (Longest Common Subsequence)
When `ignoreMove` is `false`, array reorders emit move operations instead of delete/add pairs. We minimize moves by:
1. Hashing array elements with `objectHash` to get stable identifiers.
2. Computing the **[Longest Common Subsequence (LCS)](https://en.wikipedia.org/wiki/Longest_common_subsequence)** between the current order and the target order. The LCS represents items that stay in place.
3. Walking the target order and moving only the out-of-place items, keeping LCS items anchored. This yields the smallest set of `{ op: 'move', from, path }` operations needed to reach the target sequence.
This is implemented in `move-operations.ts` (`longestCommonSequence` + `moveOperations`) and is exercised in the tests in `src/move-operations.spec.ts`.
> For more examples, check out the [tests](./src/index.spec.ts)