Když se dva dokumenty JSON liší: poziční diff, diff podle ID, nebo nejprve normalizace
Shrnutí: Padesát záznamů se stane padesáti jedna a diff hlásí padesát jedna změn. Pokrývá falešné poplachy textového diffu, párování polí podle id, normalizaci v jq a aplikovatelné patche.

Stručně: nejprve normalizujte, poté srovnávejte – pomocí jq -S nebo stabilní serializace vymažte pořadí klíčů, odsazení a zápis čísel, seřaďte nebo indexujte pole podle obchodního klíče, když je pořadí bezvýznamné, a teprve poté přeměňte diff na patch.
Rozdíl JSON není rozdíl textu. Srovnejte soubory jako řetězce a přeřazení klíčů rozsvítí celou stránku; srovnejte pole podle indexu a vložení jednoho záznamu označí ostatních padesát jako změněné.
Do služby objednávek se nasadil patch a máte jen dvě odpovědi: padesát záznamů předtím, padesát jedna potom, s jedním novým záznamem vloženým doprostřed. Nástroj diffu hlásí padesát jedna změn, zeď červené, a stále nedokážete poznat, který záznam se vlastně pohnul.
Nástroj není rozbitý. „Srovnávat“ znamená na JSON tři různé věci a většina lidí sáhne ve výchozím nastavení po té nejméně užitečné.
Reprodukce falešného poplachu: jedno vložení, padesát jedna změn
Třicet řádků Node 22: postavte padesát objednávek ({id, sku, qty}), poté jednu připojte na konec a jednu vložte na začátek a počítejte rozdíly se zarovnáním index po indexu.
| Operace | Záznamy | Změny hlášené podle pozice |
|---|---|---|
| Připojení jedné na konec | 50 → 51 | 1 |
| Vložení jedné na začátek | 50 → 51 | 51 |
Stejné „jeden záznam navíc“, padesátkrát více šumu. Důvod je všední: když srovnáváte podle indexu, nový prvek přistane na pozici 0 a každý existující záznam se posune o jeden slot dolů. Pozice 0 versus pozice 0, pozice 1 versus pozice 1 – žádná dvojice není shodná.
Postavte místo toho Map klíčovanou podle id a stejná data hlásí přesně jednu položku: $[id=ORD-999] added.
Ten experiment vám dává pravidlo důležitější než jakákoli doporučení nástrojů: zda je pole uspořádaná posloupnost, nebo neuspořádaná množina, rozhoduje o tom, které srovnání máte použít.
Tři způsoby, jak textový diff lže o JSON
diff a.json b.json srovnává znaky. JSON nese datovou strukturu. Tento nesoulad produkuje tři spolehlivé druhy falešných poplachů.
1. Přeřazené klíče objektu
Část 4 RFC 8259 je výslovná: objekt je „neuspořádaná kolekce dvojic název/hodnota“. Tyto dva dokumenty jsou stejná hodnota:
{ "id": 42, "name": "Ada", "active": true }
{ "active": true, "name": "Ada", "id": 42 }
Řádkový diff označí všechny tři řádky jako změněné. Obrátil jsem pořadí klíčů objektu s deseti klíči, oba jsem pěkně naformátoval a srovnal řádek po řádku: označeno všech deset řádků, zatímco strukturální srovnání říká, že jsou shodné. Poměr šumu 10:0.
Proč se pořadí klíčů vůbec mění? Přepnutí serializátoru, spuštění formátovače se seřazenými klíči, přestavba objektu ze slovníku nebo hash mapy, přesun mezi běhovými prostředími jazyků – cokoli z toho stačí, aby identická data na disku vypadala odlišně.
2. Prázdné znaky a odsazení
{"a":1} a {\n "a": 1\n} se parsují do stejné hodnoty. Textový diff vidí zalomení řádků a mezery a označí všechno.
3. Různý zápis čísel a escape sekvencí
1.0 a 1, 1e2 a 100, "\u00e9" a "é" – po parsování stejná hodnota, po serializaci jiný text. Tento případ je mírnější, protože zasahuje málo řádků, ale v revizích konfigurace stačí k tomu, abyste si mysleli, že se číslo změnilo.
Opačný případ je to, co text nevidí a struktura ano: 5 versus "5". Jeden pár uvozovek navíc, ale typ driftoval – obvykle chyba, nikoli aktualizace. Strukturální srovnání to bez váhání ohlásí jako modify.
Tři režimy srovnání, tři úlohy
| Režim | Co se srovnává | Odkud šum | Výstup | Hodí se pro |
|---|---|---|---|---|
| Textový diff | Řádky znaků | Pořadí klíčů, odsazení, zápis čísel | Čísla řádků, červená/zelená | Lokalizační balíčky, fixtures se stabilním pořadím klíčů |
| Strukturální diff | Parsované stromy, zarovnané podle cesty | Pole zarovnaná podle indexu | Cesty s add/remove/modify | Odpovědi API, konfigurace, webhooky |
| Sémantický diff | Stromy plus obchodní klíče | Jen špatná volba klíče | Cesty s detekcí move/insert | Seznamy záznamů, objednávky, inventář |
Strukturální diff řeší vrstvu „pořadí klíčů a formátování“. Sémantický diff jde o krok dál a řeší „identitu prvků pole“. Rozdíl není v implementační složitosti; je v tom, zda jste ochotni nástroji říci, které pole je pasem záznamu.
Pole jsou dělicí čárou
Položte si jednu otázku: je pořadí tohoto pole samo datem?
- Pořadí je data: migrační kroky, pořadí ve vyhledávání, body grafu, schvalovací řetězce. Změna pozice je změna chování, takže poziční srovnání je správné.
- Pořadí je bezvýznamné: tagy, množiny ID oprávnění, položky objednávky, seznamy inventáře. Vložení nemá ovlivnit, jak se posuzují ostatní položky.
Když je pořadí bezvýznamné, indexujte záznamy podle klíče, místo abyste je mlátili podle pozice:
// Párování podle id: vložení nebo smazání hlásí jednu položku, ostatní položky zůstávají
function diffById(before, after, path = '$') {
const out = [];
const beforeMap = new Map(before.map((item) => [item.id, item]));
const afterMap = new Map(after.map((item) => [item.id, item]));
for (const [id, value] of afterMap) {
if (!beforeMap.has(id)) out.push({ path: `${path}[id=${id}]`, type: 'added' });
}
for (const [id, value] of beforeMap) {
if (!afterMap.has(id)) out.push({ path: `${path}[id=${id}]`, type: 'removed' });
else if (JSON.stringify(value) !== JSON.stringify(afterMap.get(id))) {
out.push({ path: `${path}[id=${id}]`, type: 'changed' });
}
}
return out;
}
Vyspělé knihovny z toho dělají volbu. jsondiffpatch používá LCS k nalezení nejdelší společné podposloupnosti, ale objektové prvky uvnitř polí se ve výchozím nastavení párují podle pozice – oficiální dokumentace zdůrazňuje, že musíte dodat funkci objectHash, aby se párovaly podle obsahu. Uživatelé na první pokus často usoudí, že knihovna je hloupá; ve skutečnosti nikdy nepředali klíč.
Volba klíče také záleží. id je bezpečná volba. sku funguje jen tehdy, když jsou SKU jedinečné. Hashing celého objektu mění „změnilo se jedno pole“ na „jedno odebrané plus jedno přidané“, což je horší než srovnání podle pozice.
Nejrychlejší trik v terminálu: normalizace pomocí jq
Pokud nechcete knihovnu a potřebujete se jen podívat, nejprve normalizujte a poté diffejte. jq -S rekurzivně seřadí klíče objektů a použije konzistentní odsazení:
# Normalizujte, poté srovnejte řádek po řádku; šum pořadí klíčů a odsazení zmizí
diff -u <(jq -S . before.json) <(jq -S . after.json)
# Když je pole skutečně neuspořádané, před srovnáním seřaďte podle klíče
diff -u <(jq -S '.items |= sort_by(.id)' before.json) \
<(jq -S '.items |= sort_by(.id)' after.json)
Spustil jsem printf '{"b":2,"a":1,"arr":[1,2]}' | jq -S . a dostal {"a":1,"arr":[1,2],"b":2} – klíče seřazené, arr nedotčené. Pamatujte: -S řadí jen klíče objektů, nikdy prvky pole. Buď pole seřaďte explicitně pomocí sort / sort_by, nebo přijměte poziční šum.
Dvě pasti:
jqneumí upravovat na místě; zapisuje na standardní výstup. Použijte process substitution (<(...)), nebo zapište do dočasného souboru, ověřte a poté nahraďte;-rodstraňuje uvozovky z řetězců, takže výstup může obsahovat zalomení řádků nebo znaky významné pro shell. Nedůvěryhodná data před dalším předáním obalte uvozovkami.
Co dělá /diff na tomto webu a co ne
Vestavěný nástroj pro srovnání JSON sleduje stejný strukturální přístup: obě strany se parsují do stromů a rekurzivně se zarovnávají podle cesty, přičemž vzniká cesta + typ (add/remove/modify) + před/po. Pořadí klíčů objektu výsledek neovlivňuje a vše běží v prohlížeči – vložené payloady se nikdy nenahrávají.
Jeho pole jsou však zarovnána podle indexu. To je úmyslný kompromis, nikoli vada:
- Připojení na konec, úprava stejné pozice nebo úplná náhrada dává čistý výstup;
- Vložení záznamu na začátek nebo doprostřed označí všechno za ním jako
modifya vy musíte postřehnout, že „ve skutečnosti je nový jen jeden záznam“.
Když tedy srovnáváte seznamy objednávek, správný krok je seřadit obě strany podle id před vložením. Seřazený vstup zafixuje bod vložení a srazí šum na jednu položku. Není třeba nic instalovat a pokrývá většinu každodenních případů.
Pokud potřebujete „vygenerovat aplikovatelný patch“ nebo „detekovat přesunuté prvky“, nástroj na stránce to neumí. To je další část.
Přeměna diffu na patch: dva formáty, nemíchejte je
Lidé čtou diffy; programy aplikují patche. JSON má dva formáty patchů se zcela odlišnou sémantikou.
| Dimenze | JSON Patch (RFC 6902) | JSON Merge Patch (RFC 7396) |
|---|---|---|
| Tvar | Pole operací s op, path, value |
JSON objekt ve tvaru cílového dokumentu |
| Pole | Ano, podle indexu, /items/0, /- pro připojení |
Ne, pole se nahrazují celá |
| Smazání klíče | remove |
Jen pomocí null, což neumí smazat pole, jehož hodnota je legitimně null |
| Vyjádření přesunu | move / copy |
Ne |
| Hodí se pro | Zdroje obsahující pole, auditní logy, atomické aktualizace | Částečné aktualizace obyčejných objektů, doručování konfigurace |
JSON Patch má jednu operaci, kterou lidé přeskakují: test.
[
{ "op": "test", "path": "/version", "value": 3 },
{ "op": "replace", "path": "/status", "value": "shipped" }
]
Pokud test selže, celý patch se odmítne. To je optimistické souběžné řízení zabudované do protokolu – není třeba extra hlavička If-Match k zaručení „zapsal jsem jen proto, že jsem viděl verzi 3“. Pro endpoint PATCH to má větší cenu než jakákoli dokumentační konvence.
Cesty používají JSON Pointer (RFC 6901): ~0 escapuje ~, ~1 escapuje /. Klíče obsahující lomítka (například MIME typy jako klíče) musí být escapované, jinak pointer přistane jinde. Podrobnosti najdete v JSON Pointer a Patch.
Zůstává jeden praktický problém: patche generované automaticky z diffu obvykle adresují pole podle indexu. Indexy jsou v produkci křehké – jedno souběžné vložení a /items/3 už neukazuje na stejný záznam. Buď omezte generované patche na pole objektů, nebo kritická pole před aplikací validujte pomocí test.
Zařaďte srovnání do CI: tvrďte hodnoty, nikoli text
Stačí tři pravidla:
- V testech tvrďte parsované hodnoty, nikdy výstup
JSON.stringify. V Jestu použijtetoEqual; v Pythonuassert a == bpojson.loads. Pořadí klíčů a odsazení se neúčastní. - Whitelistujte dynamická pole.
requestId,timestamp,traceIdse liší při každém běhu; bez vyloučení je test trvale červený.propertyFilterv jsondiffpatch aexclude_pathsv deepdiff existují právě proto. - Při selhání vypisujte cesty, nikoli celý dokument. Nikdo nečte 5 MB payloadu v logu;
$.data.items[id=ORD-042].qty: 2 → 3lokalizuje problém okamžitě.
Drift konfigurace funguje stejně: prožeňte staging i produkční konfiguraci jq -S, seřaďte pole podle obchodního klíče a co zůstane, je skutečný rozdíl. Tato třída problémů – stejná data vypadající napříč prostředími odlišně – je obvykle nedefinovaná smlouva, což je téma Návrh smlouvy API.
Časté dotazy
Počítá se pořadí klíčů jako rozdíl?
Ne. RFC 8259 definuje objekty jako neuspořádané kolekce, takže {"a":1,"b":2} a {"b":2,"a":1} jsou stejná hodnota. Každý nástroj, který hlásí přeřazení jako změnu, dělá textové srovnání, nikoli srovnání JSON.
Proč vložení jednoho záznamu označí každou položku jako změněnou?
Protože nástroj zarovnává pole index po indexu. Nový prvek sedí na pozici 0, každý existující záznam se posune o jeden dolů a žádné dvě pozice se neshodují. Místo pozičního srovnání pole před srovnáním seřaďte nebo indexujte podle klíče jako id.
Nahrávají online nástroje pro srovnání moje data?
Záleží na implementaci. Serverově vykreslované nástroje posílají payload na backend, kde se může zalogovat; klientské nástroje parsují v prohlížeči a nic nenahrávají. Než srovnáte produkční konfigurace nebo uživatelské payloady, ověřte, že stránka počítá lokálně, nebo odpojte síť a zkuste to znovu.
Lze výsledek diffu použít přímo jako patch?
Ne přímo. Diff říká, kde se věci liší; JSON Patch říká, které operace v jakém pořadí spustit. Mezi nimi je převodní vrstva. Generované patche navíc obvykle adresují pole podle indexu, což se při souběžnosti rozbije – před aplikací přidejte operace test.
Jak srovnávat soubory o desítkách MB?
Nejprve streamujte (jq -c zapisuje jeden záznam na řádek), nebo extrahujte jen podstrom, který vás zajímá, například jq '.data.items'. Úplné rekurzivní srovnání desítek MB se ještě vejde do paměti; nad sto MB ho rozdělte nebo indexujte podle klíče. Konkrétní údaje o paměti a čase najdete v Výkon u velkého JSON.
Mám pole před srovnáním seřadit podle id?
Ano. Vestavěný nástroj pro srovnání JSON zarovnává pole podle indexu, takže vložení jednoho záznamu na začátek nebo doprostřed označí všechno za ním jako modify; seřazení obou stran podle id před vložením zafixuje bod vložení a srazí šum na jednu položku. Výjimkou je případ, kdy pořadí pole je samo datem – migrační kroky, pořadí ve vyhledávání, body grafu – kde je poziční srovnání správné a řazení špatné.