X Xerobit

JSON Deep Equal — Testing Structural Equality of JSON Objects

Deep equality checks if two JSON objects have identical structure and values, regardless of object reference. Here's how to implement deep equal for JSON in JavaScript and...

Mian Ali Khalid · · 4 min read
Use the tool
JSON Diff
Compare two JSON objects structurally with field-by-field diff.
Open JSON Diff →

Deep equality checks whether two JSON values have the same structure and data — not whether they’re the same object in memory. This is what you need when validating API responses, comparing config files, or writing tests.

Use the JSON Diff Tool to compare JSON documents and see exactly what differs.

The problem with shallow equality

// Primitive values: reference equality works
42 === 42      // true
'hello' === 'hello'  // true

// Objects: reference equality fails
{ a: 1 } === { a: 1 }  // false (different references)
[1, 2, 3] === [1, 2, 3]  // false

// After JSON roundtrip: always different references
const parsed = JSON.parse('{"a":1}');
parsed === { a: 1 }  // false

Deep equality compares values, not references.

What JSON deep equality must handle

// 1. Primitives: value comparison
deepEqual(1, 1)       // true
deepEqual('a', 'a')   // true
deepEqual(null, null) // true
deepEqual(1, '1')     // false (different types)

// 2. Arrays: ordered, element-by-element
deepEqual([1, 2, 3], [1, 2, 3])  // true
deepEqual([1, 2, 3], [1, 3, 2])  // false (order matters)

// 3. Objects: all keys equal, regardless of key order
deepEqual({ a: 1, b: 2 }, { b: 2, a: 1 })  // true

// 4. Nested: recursive
deepEqual({ x: { y: { z: 1 } } }, { x: { y: { z: 1 } } })  // true

// 5. Edge cases
deepEqual(null, undefined)  // false
deepEqual(0, -0)            // depends on your requirements
deepEqual(NaN, NaN)         // NaN !== NaN in JS, but may want true

JavaScript implementation

function deepEqual(a, b) {
  // Same reference or same primitive value
  if (a === b) return true;
  
  // Null checks
  if (a === null || b === null) return a === b;
  
  // Type mismatch
  if (typeof a !== typeof b) return false;
  
  // Arrays
  if (Array.isArray(a)) {
    if (!Array.isArray(b) || a.length !== b.length) return false;
    return a.every((item, i) => deepEqual(item, b[i]));
  }
  
  // Objects (but not arrays)
  if (typeof a === 'object') {
    if (Array.isArray(b)) return false;
    
    const keysA = Object.keys(a);
    const keysB = Object.keys(b);
    
    if (keysA.length !== keysB.length) return false;
    
    return keysA.every(key => key in b && deepEqual(a[key], b[key]));
  }
  
  // Primitives that didn't pass === (e.g., NaN)
  return false;
}

// Tests:
deepEqual({ a: 1, b: [1, 2] }, { a: 1, b: [1, 2] });  // true
deepEqual({ a: 1, b: [1, 2] }, { a: 1, b: [2, 1] });  // false
deepEqual(null, null);   // true
deepEqual(null, 0);      // false
deepEqual({}, {});       // true
deepEqual([], []);       // true
deepEqual([], {});       // false

Using libraries in JavaScript

// Option 1: Lodash (battle-tested, handles edge cases like NaN, Date, RegExp)
import { isEqual } from 'lodash';

isEqual({ a: 1 }, { a: 1 });     // true
isEqual([1, 2], [1, 2]);          // true
isEqual(NaN, NaN);                // true (lodash handles this)

// Option 2: fast-deep-equal (faster, JSON-safe values)
import equal from 'fast-deep-equal';

equal({ a: 1, b: { c: 2 } }, { a: 1, b: { c: 2 } });  // true

// Option 3: JSON.stringify (simplest, but key-order sensitive)
JSON.stringify({ a: 1, b: 2 }) === JSON.stringify({ b: 2, a: 1 });
// false — same content, different key order

Python deep equality

Python’s == operator on dicts and lists already does structural comparison:

# Python dicts: structural equality by default
{"a": 1, "b": 2} == {"b": 2, "a": 1}  # True — key order doesn't matter
[1, 2, 3] == [1, 2, 3]                  # True
[1, 2, 3] == [1, 3, 2]                  # False — list order matters

# Nested:
{"x": {"y": [1, 2]}} == {"x": {"y": [1, 2]}}  # True

# Type differences:
1 == 1.0    # True (int and float)
1 == "1"    # False
None == None  # True

For more control, use deepdiff:

from deepdiff import DeepDiff

a = {"name": "Alice", "scores": [100, 95, 87]}
b = {"name": "Alice", "scores": [100, 95, 87]}

# No differences:
DeepDiff(a, b) == {}  # True

# Check with type strictness:
DeepDiff({"n": 1}, {"n": 1.0}, ignore_numeric_type_changes=True)
# {} (treated as equal)

DeepDiff({"n": 1}, {"n": 1.0})
# {'type_changes': {"root['n']": {'old_type': int, 'new_type': float, ...}}}

Deep equal in tests

Jest (JavaScript)

// Jest uses deep equality for toEqual:
expect({ a: 1, b: [2, 3] }).toEqual({ a: 1, b: [2, 3] });  // passes

// vs toBe (reference equality):
const obj = { a: 1 };
expect(obj).toBe(obj);  // passes
expect({ a: 1 }).toBe({ a: 1 });  // FAILS — different references

// For API responses:
const response = await fetch('/api/user/1');
const data = await response.json();

expect(data).toEqual({
  id: 1,
  name: 'Alice',
  email: 'alice@example.com',
});

// Partial matching:
expect(data).toMatchObject({ id: 1, name: 'Alice' });
// Passes even if data has extra keys

Pytest (Python)

def test_api_response():
    response = client.get('/api/user/1')
    data = response.json()
    
    assert data == {
        "id": 1,
        "name": "Alice",
        "email": "alice@example.com",
    }
    
    # Partial: check subset
    assert data["id"] == 1
    assert data["name"] == "Alice"

Order-insensitive array comparison

When array order doesn’t matter (e.g., comparing tag lists):

function deepEqualUnordered(a, b) {
  if (!Array.isArray(a) || !Array.isArray(b)) {
    return deepEqual(a, b);
  }
  if (a.length !== b.length) return false;
  
  const sorted_a = [...a].sort();
  const sorted_b = [...b].sort();
  return deepEqual(sorted_a, sorted_b);
}

deepEqualUnordered(['b', 'a', 'c'], ['c', 'a', 'b']);  // true
# Python:
sorted(["b", "a", "c"]) == sorted(["c", "a", "b"])  # True

# With deepdiff:
from deepdiff import DeepDiff
DeepDiff(["b", "a"], ["a", "b"], ignore_order=True)  # {}

Related posts

Related tool

JSON Diff

Compare two JSON objects structurally with field-by-field diff.

Written by Mian Ali Khalid. Part of the Data & Format pillar.