JSON Toolkit

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.

Začátečník~6 min čteníPublikováno 2026-10-30Aktualizováno 2026-10-30
Titulní obrázek článku Srovnání dvou dokumentů JSON

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:

  • jq neumí 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;
  • -r odstraň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 modify a 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:

  1. V testech tvrďte parsované hodnoty, nikdy výstup JSON.stringify. V Jestu použijte toEqual; v Pythonu assert a == b po json.loads. Pořadí klíčů a odsazení se neúčastní.
  2. Whitelistujte dynamická pole. requestId, timestamp, traceId se liší při každém běhu; bez vyloučení je test trvale červený. propertyFilter v jsondiffpatch a exclude_paths v deepdiff existují právě proto.
  3. Při selhání vypisujte cesty, nikoli celý dokument. Nikdo nečte 5 MB payloadu v logu; $.data.items[id=ORD-042].qty: 2 → 3 lokalizuje 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é.