python
41 lines · 8 steps
Recursively diffing two JSON structures
A recursive walk over dicts, lists, and scalars that reports every added, removed, and changed value with a path.
Explained by
highlit
1from typing import Any
2
3_MISSING = object()
4
5
6def diff_json(old: Any, new: Any, path: str = "") -> dict[str, list[dict]]:
7 result = {"added": [], "removed": [], "changed": []}
8
9 if isinstance(old, dict) and isinstance(new, dict):
10 for key in old.keys() | new.keys():
11 child_path = f"{path}.{key}" if path else key
12 old_val = old.get(key, _MISSING)
13 new_val = new.get(key, _MISSING)
14
15 if old_val is _MISSING:
16 result["added"].append({"path": child_path, "value": new_val})
17 elif new_val is _MISSING:
18 result["removed"].append({"path": child_path, "value": old_val})
19 else:
20 _merge(result, diff_json(old_val, new_val, child_path))
21 elif isinstance(old, list) and isinstance(new, list):
22 for index in range(max(len(old), len(new))):
23 child_path = f"{path}[{index}]"
24 old_val = old[index] if index < len(old) else _MISSING
25 new_val = new[index] if index < len(new) else _MISSING
26
27 if old_val is _MISSING:
28 result["added"].append({"path": child_path, "value": new_val})
29 elif new_val is _MISSING:
30 result["removed"].append({"path": child_path, "value": old_val})
31 else:
32 _merge(result, diff_json(old_val, new_val, child_path))
33 elif old != new:
34 result["changed"].append({"path": path, "from": old, "to": new})
35
36 return result
37
38
39def _merge(target: dict[str, list], source: dict[str, list]) -> None:
40 for kind, entries in source.items():
41 target[kind].extend(entries)
01 / 01
STEP 01
‹ swipe to step through ›
Walkthrough
Space play
←→ step
click any line
Three takeaways
- 1A unique sentinel object distinguishes 'key absent' from a legitimate None or falsy value.
- 2Recursion mirrors the shape of nested data, threading a path string down to describe each leaf.
- 3Comparing the union of keys catches additions and removals symmetrically in one pass.
Related explainers
python
from fastapi import FastAPI, WebSocket, WebSocketDisconnect app = FastAPI()
Building a WebSocket chat with FastAPI
websockets
broadcast
connection-management
Intermediate
9 steps
python
import time import uuid from django.utils.deprecation import MiddlewareMixin
Attaching per-request context in Django
middleware
request lifecycle
multi-tenancy
Intermediate
7 steps
javascript
function evaluate(expression) { const tokens = tokenize(expression); let pos = 0;
Building a recursive descent calculator
parsing
recursion
operator-precedence
Intermediate
8 steps
python
import random from typing import Iterator, List
How reservoir sampling picks k items
reservoir-sampling
streaming
randomness
Intermediate
5 steps
python
import secrets from django.contrib.auth import authenticate, login from django.core.cache import cache
Two-factor login with OTP in Django
two-factor-auth
one-time-passwords
caching
Intermediate
9 steps
python
import re from functools import total_ordering from typing import Optional
Parsing and comparing semantic versions
regex
operator-overloading
sorting
Intermediate
7 steps
Share this explainer
Here's the card — post it anywhere.
Made with highlit — turn any snippet into a walkthrough like this in about a minute.
Explain your code
Embed this explainer
Drop the interactive walkthrough into a blog or docs. Views never cost a credit.
<iframe src="https://highlit.co/explainers/recursively-diffing-two-json-structures-explained-python-fc8f/embed?autoplay=1" width="100%" height="520" loading="lazy" style="border:0"></iframe>
Autoplay is on by default — add ?autoplay=0 to start paused.