deep-diff、jsondiffpatch、object-diff は、JavaScript オブジェクト間の変更点を検出するためのライブラリです。これらは、状態管理、データ同期、変更履歴の記録など、フロントエンドアーキテクチャにおいて重要な役割を果たします。deep-diff は変更内容を配列で詳細に出力し、jsondiffpatch は転送に適した圧縮形式のデルタを生成します。object-diff はシンプルな差分オブジェクトを返しますが、機能面で他より限定的です。
フロントエンド開発において、状態の変化を追跡することは一般的です。deep-diff、jsondiffpatch、object-diff はすべて JavaScript オブジェクトの差分を検出できますが、出力形式と設計思想が異なります。実務でどのライブラリを選ぶべきか、技術的な観点から比較します。
ライブラリごとに差分の表現方法が全く違います。これが後の処理コストに直結します。
deep-diff は変更内容を配列で返します。
kind)で編集、追加、削除を区別します。import { diff } from 'deep-diff';
const lhs = { name: 'Alice' };
const rhs = { name: 'Bob' };
const changes = diff(lhs, rhs);
// 出力: [ { kind: 'E', path: [ 'name' ], lhs: 'Alice', rhs: 'Bob' } ]
jsondiffpatch は構造化されたデルタオブジェクトを返します。
[旧値,新値] で編集を表すなど、独自ルールがあります。import * as jsondiffpatch from 'jsondiffpatch';
const lhs = { name: 'Alice' };
const rhs = { name: 'Bob' };
const delta = jsondiffpatch.diff(lhs, rhs);
// 出力: { name: ['Alice', 'Bob'] }
object-diff は差分のみを含む単純なオブジェクトを返します。
import diff from 'object-diff';
const lhs = { name: 'Alice' };
const rhs = { name: 'Bob' };
const result = diff(lhs, rhs);
// 出力: { name: 'Bob' }
差分を検出するだけでなく、それを適用できるかも重要です。
deep-diff は専用関数で変更を適用できます。
applyChange を使って元のオブジェクトを更新します。import { applyChange } from 'deep-diff';
// changes は上記の diff 結果
applyChange(target, source, changes[0]);
jsondiffpatch はパッチ適用が主要機能です。
patch 関数でデルタを適用します。unpatch)もサポートしており、undo 機能に役立ちます。import * as jsondiffpatch from 'jsondiffpatch';
// delta は上記の diff 結果
jsondiffpatch.patch(lhs, delta);
object-diff はパッチ機能を標準で提供しません。
Object.assign で代用可能です。import diff from 'object-diff';
const changes = diff(lhs, rhs);
// 手动で適用
Object.assign(lhs, changes);
配列が含まれる場合、どのライブラリも苦労しますが、アプローチが違います。
deep-diff は配列をインデックスベースで扱います。
// 先頭に要素を挿入した場合
// deep-diff は全要素が変更されたように見なす可能性があります
jsondiffpatch は配列を値ベースで扱おうとします。
// jsondiffpatch は要素の移動を検出し、無駄な更新を防ぎます
object-diff は配列処理が単純です。
// 配列の一部変更でも、配列全体が差分として返されることがあります
ライブラリのメンテナンス状況は、長期プロジェクトで重要です。
deep-diff は長年維持されており安定しています。
jsondiffpatch も活発に開発されています。
object-diff は更新頻度が低いです。
| 機能 | deep-diff | jsondiffpatch | object-diff |
|---|---|---|---|
| 出力形式 | 変更イベントの配列 | 圧縮デルタオブジェクト | 差分のみオブジェクト |
| パッチ適用 | ✅ 専用関数あり | ✅ 専用関数あり | ❌ 手动マージ必要 |
| 配列検出 | インデックスベース | 値ベース(スマート) | 単純比較 |
| 保守状況 | 🟢 安定 | 🟢 活発 | 🟡 更新少ない |
| 主な用途 | 監査ログ、履歴管理 | 状態同期、通信節約 | 簡易スクリプト |
deep-diff は、変更の履歴を詳細に記録したい場合に最適です。 — 監査ログや、ユーザー操作の記録を残すシステムに向いています。
jsondiffpatch は、効率と機能性のバランスが良い選択です。 — 状態をサーバーと同期したり、複雑な_undo_機能が必要な場合に推奨します。
object-diff は、限定的な用途に留めるべきです。 — 保守リスクを考慮すると、重要なプロダクションコードでは deep-diff または jsondiffpatch を選ぶのが賢明です。
最終的には、差分データを「どう使うか」で決まります。 — 人間が読むなら deep-diff、機械が処理するなら jsondiffpatch が基本的な基準になります。
ネットワーク経由で差分を送信したり、保存容量を節約したい場合に選択します。出力形式がコンパクトで、フィルタリング機能も充実しているため、大規模な状態同期システムに向いています。
変更履歴を配列として保持し、人間が読みやすいログや監査証跡が必要な場合に選択します。変更の種類(追加、削除、編集)が明確に区別されるため、デバッグや_undo/redo_機能の実装に適しています。
非常に単純なオブジェクト比較しか必要なく、依存パッケージを最小限に抑えたい場合に選択します。ただし、保守頻度が他より低いため、重要な基幹システムでの使用は避けるべきです。
jsondiffpatch.com
Diff & patch JavaScript objects
objectHash function (this is how objects are matched, otherwise a dumb match by position is used). For more details, check Array diff documentation./node_modules/.bin/jsondiffpatch left.json right.jsonjsondiffpatch.clone(obj) (deep clone)on your terminal:
npx jsondiffpatch --help

or as a library:
// sample data
const country = {
name: 'Argentina',
capital: 'Buenos Aires',
independence: new Date(1816, 6, 9),
};
// clone country, using dateReviver for Date objects
const country2 = JSON.parse(JSON.stringify(country), jsondiffpatch.dateReviver);
// make some changes
country2.name = 'Republica Argentina';
country2.population = 41324992;
delete country2.capital;
const delta = jsondiffpatch.diff(country, country2);
assertSame(delta, {
name: ['Argentina', 'Republica Argentina'], // old value, new value
population: ['41324992'], // new value
capital: ['Buenos Aires', 0, 0], // deleted
});
// patch original
jsondiffpatch.patch(country, delta);
// reverse diff
const reverseDelta = jsondiffpatch.reverse(delta);
// also country2 can be return to original value with: jsondiffpatch.unpatch(country2, delta);
const delta2 = jsondiffpatch.diff(country, country2);
assert(delta2 === undefined);
// undefined => no difference
Array diffing:
// sample data
const country = {
name: 'Argentina',
cities: [
{
name: 'Buenos Aires',
population: 13028000,
},
{
name: 'Cordoba',
population: 1430023,
},
{
name: 'Rosario',
population: 1136286,
},
{
name: 'Mendoza',
population: 901126,
},
{
name: 'San Miguel de Tucuman',
population: 800000,
},
],
};
// clone country
const country2 = JSON.parse(JSON.stringify(country));
// delete Cordoba
country.cities.splice(1, 1);
// add La Plata
country.cities.splice(4, 0, {
name: 'La Plata',
});
// modify Rosario, and move it
const rosario = country.cities.splice(1, 1)[0];
rosario.population += 1234;
country.cities.push(rosario);
// create a configured instance, match objects by name
const diffpatcher = jsondiffpatch.create({
objectHash: function (obj) {
return obj.name;
},
});
const delta = diffpatcher.diff(country, country2);
assertSame(delta, {
cities: {
_t: 'a', // indicates this node is an array (not an object)
1: [
// inserted at index 1
{
name: 'Cordoba',
population: 1430023,
},
],
2: {
// population modified at index 2 (Rosario)
population: [1137520, 1136286],
},
_3: [
// removed from index 3
{
name: 'La Plata',
},
0,
0,
],
_4: [
// move from index 4 to index 2
'',
2,
3,
],
},
});
For more example cases (nested objects or arrays, long text diffs) check packages/jsondiffpatch/test/examples/
If you want to understand deltas, see delta format documentation
This works for node, or in browsers if you already do bundling on your app
npm install jsondiffpatch
import {* as jsondiffpatch} from 'jsondiffpatch';
const jsondiffpatchInstance = jsondiffpatch.create(options);
In a browser, you can load a bundle using a tool like esm.sh or Skypack.
import * as jsondiffpatch from 'jsondiffpatch';
// Only import if you want text diffs using diff-match-patch
import { diff_match_patch } from '@dmsnell/diff-match-patch';
const jsondiffpatchInstance = jsondiffpatch.create({
// used to match objects when diffing arrays, by default only === operator is used
objectHash: function (obj) {
// this function is used only to when objects are not equal by ref
return obj._id || obj.id;
},
arrays: {
// default true, detect items moved inside the array (otherwise they will be registered as remove+add)
detectMove: true,
// default false, the value of items moved is not included in deltas
includeValueOnMove: false,
},
textDiff: {
// If using text diffs, it's required to pass in the diff-match-patch library in through this proprty.
// Alternatively, you can import jsondiffpatch using `jsondiffpatch/with-text-diffs` to avoid having to pass in diff-match-patch through the options.
diffMatchPatch: diff_match_patch,
// default 60, minimum string length (left and right sides) to use text diff algorithm: google-diff-match-patch
minLength: 60,
},
propertyFilter: function (name, context) {
/*
this optional function can be specified to ignore object properties (eg. volatile data)
name: property name, present in either context.left or context.right objects
context: the diff context (has context.left and context.right objects)
*/
return name.slice(0, 1) !== '$';
},
cloneDiffValues: false /* default false. if true, values in the obtained delta will be cloned
(using jsondiffpatch.clone by default), to ensure delta keeps no references to left or right objects. this becomes useful if you're diffing and patching the same objects multiple times without serializing deltas.
instead of true, a function can be specified here to provide a custom clone(value).
*/
omitRemovedValues: false /* if you don't need to unpatch (reverse deltas),
"old"/"left" values (removed or replaced) are not included in the delta.
you can set this to true to get more compact deltas.
*/,
});
<!doctype html>
<html>
<head>
<link rel="stylesheet" href="./style.css" type="text/css" />
<link
rel="stylesheet"
href="https://esm.sh/jsondiffpatch@0.6.0/lib/formatters/styles/html.css"
type="text/css"
/>
<link
rel="stylesheet"
href="https://esm.sh/jsondiffpatch@0.6.0/lib/formatters/styles/annotated.css"
type="text/css"
/>
</head>
<body>
<div id="visual"></div>
<hr />
<div id="annotated"></div>
<script type="module">
import * as jsondiffpatch from 'https://esm.sh/jsondiffpatch@0.6.0';
import * as annotatedFormatter from 'https://esm.sh/jsondiffpatch@0.6.0/formatters/annotated';
import * as htmlFormatter from 'https://esm.sh/jsondiffpatch@0.6.0/formatters/html';
const left = { a: 3, b: 4 };
const right = { a: 5, c: 9 };
const delta = jsondiffpatch.diff(left, right);
// beautiful html diff
document.getElementById('visual').innerHTML = htmlFormatter.format(
delta,
left,
);
// self-explained json
document.getElementById('annotated').innerHTML =
annotatedFormatter.format(delta, left);
</script>
</body>
</html>
To see formatters in action check the Live Demo.
For more details check Formatters documentation
diff(), patch() and reverse() functions are implemented using Pipes & Filters pattern, making it extremely customizable by adding or replacing filters on a pipe.
Check Plugins documentation for details.