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,'&amp;').replace(/</g,'&lt;').replace(/>/g,'&gt;'); }