Developer
JSON DIFF
Paste two JSON objects to see exactly what changed — added, removed, and modified keys highlighted recursively. Nothing leaves your browser.
Added in B
Removed in B
Changed
Unchanged
Paste two JSON objects above to see the diff.
Developer Reference
Core Algorithm & Standalone Script
Standalone, zero-dependency JavaScript implementation powering this tool. Free to inspect, copy, and build upon.
let diffTimer = null;
function autoDiff() {
clearTimeout(diffTimer);
diffTimer = setTimeout(runDiff, 200);
}
function parseJSON(id, errId) {
const raw = document.getElementById(id).value.trim();
const errEl = document.getElementById(errId);
const ta = document.getElementById(id);
if (!raw) { errEl.textContent = ''; ta.classList.remove('err'); return null; }
try {
const val = JSON.parse(raw);
errEl.textContent = ''; ta.classList.remove('err');
return val;
} catch(e) {
errEl.textContent = '⚠ ' + e.message;
ta.classList.add('err');
return undefined;
}
}
function runDiff() {
const a = parseJSON('jsonA', 'errA');
const b = parseJSON('jsonB', 'errB');
if (a === undefined || b === undefined) {
document.getElementById('diffOutput').innerHTML = '<div class="empty-state">Fix JSON errors above.</div>';
document.getElementById('summaryRow').style.display = 'none';
return;
}
if (a === null && b === null) {
document.getElementById('diffOutput').innerHTML = '<div class="empty-state">Paste two JSON objects above to see the diff.</div>';
document.getElementById('summaryRow').style.display = 'none';
return;
}
const lines = [];
const stats = { added: 0, removed: 0, changed: 0, same: 0 };
diffValues(a, b, '', lines, stats);
document.getElementById('summaryRow').innerHTML =
`<div class="sum-badge sum-added">+${stats.added} added</div>` +
`<div class="sum-badge sum-removed">−${stats.removed} removed</div>` +
`<div class="sum-badge sum-changed">~ ${stats.changed} changed</div>` +
`<div class="sum-badge sum-same">${stats.same} same</div>`;
document.getElementById('summaryRow').style.display = 'flex';
document.getElementById('diffOutput').innerHTML = lines.map(l =>
`<span class="diff-line ${l.type}">${esc(l.text)}</span>`
).join('');
}
function diffValues(a, b, path, lines, stats, indent) {
indent = indent || 0;
const pad = ' '.repeat(indent);
if (a === null && b === null) { stats.same++; return; }
// Both objects
if (isObj(a) && isObj(b)) {
const keysA = Object.keys(a);
const keysB = Object.keys(b);
const allKeys = [...new Set([...keysA, ...keysB])];
if (path) lines.push({ type:'header', text: pad + path + ' {' });
allKeys.forEach(k => {
const subPath = path ? k : k;
const hasA = Object.prototype.hasOwnProperty.call(a, k);
const hasB = Object.prototype.hasOwnProperty.call(b, k);
if (hasA && !hasB) {
stats.removed++;
lines.push({ type:'removed', text: pad + ' − ' + k + ': ' + JSON.stringify(a[k]) });
} else if (!hasA && hasB) {
stats.added++;
lines.push({ type:'added', text: pad + ' + ' + k + ': ' + JSON.stringify(b[k]) });
} else if (deepEqual(a[k], b[k])) {
stats.same++;
lines.push({ type:'same', text: pad + ' ' + k + ': ' + JSON.stringify(a[k]) });
} else if (isObj(a[k]) && isObj(b[k])) {
diffValues(a[k], b[k], k, lines, stats, indent + 1);
} else if (Array.isArray(a[k]) && Array.isArray(b[k])) {
diffArrays(a[k], b[k], k, lines, stats, indent + 1);
} else {
stats.changed++;
lines.push({ type:'changed', text: pad + ' ~ ' + k + ': ' + JSON.stringify(a[k]) + ' → ' + JSON.stringify(b[k]) });
}
});
if (path) lines.push({ type:'header', text: pad + '}' });
} else if (Array.isArray(a) && Array.isArray(b)) {
diffArrays(a, b, path, lines, stats, indent);
} else {
// primitive or type mismatch
if (deepEqual(a, b)) {
stats.same++;
lines.push({ type:'same', text: pad + ' ' + path + ': ' + JSON.stringify(a) });
} else {
stats.changed++;
lines.push({ type:'changed', text: pad + ' ~ ' + path + ': ' + JSON.stringify(a) + ' → ' + JSON.stringify(b) });
}
}
}
function diffArrays(a, b, path, lines, stats, indent) {
indent = indent || 0;
const pad = ' '.repeat(indent);
const maxLen = Math.max(a.length, b.length);
lines.push({ type:'header', text: pad + (path ? path + ' [' : '[') + ' (array, ' + a.length + ' → ' + b.length + ' items)' });
for (let i = 0; i < maxLen; i++) {
if (i >= a.length) {
stats.added++;
lines.push({ type:'added', text: pad + ' + [' + i + ']: ' + JSON.stringify(b[i]) });
} else if (i >= b.length) {
stats.removed++;
lines.push({ type:'removed', text: pad + ' − [' + i + ']: ' + JSON.stringify(a[i]) });
} else if (deepEqual(a[i], b[i])) {
stats.same++;
lines.push({ type:'same', text: pad + ' [' + i + ']: ' + JSON.stringify(a[i]) });
} else {
stats.changed++;
lines.push({ type:'changed', text: pad + ' ~ [' + i + ']: ' + JSON.stringify(a[i]) + ' → ' + JSON.stringify(b[i]) });
}
}
lines.push({ type:'header', text: pad + ']' });
}
function isObj(v) { return v !== null && typeof v === 'object' && !Array.isArray(v); }
function deepEqual(a, b) {
if (a === b) return true;
if (typeof a !== typeof b) return false;
if (a === null || b === null) return a === b;
if (Array.isArray(a) && Array.isArray(b)) {
if (a.length !== b.length) return false;
return a.every((v,i) => deepEqual(v, b[i]));
}
if (isObj(a) && isObj(b)) {
const keysA = Object.keys(a), keysB = Object.keys(b);
if (keysA.length !== keysB.length) return false;
return keysA.every(k => deepEqual(a[k], b[k]));
}
return false;
}
function esc(s) { return String(s).replace(/&/g,'&').replace(/</g,'<').replace(/>/g,'>'); }