Wybór narzędzia do diffu JSON: diff tekstowy, strukturalny i wizualizacja
Podsumowanie: Diff tekstowy znajduje, gdzie pliki się różnią; diff strukturalny rozstrzyga, czy treść jest równoważna. Wybór właściwej warstwy chroni przed braniem kolejności kluczy za incydent.

Najważniejsze: diff tekstowy odkrywa zmianę (najpierw posortuj klucze przez jq -S), diff strukturalny rozstrzyga równoważność (wyrównaj tablice po kluczu głównym), a wizualizacja tylko wyjaśnia lub ułatwia przegląd; w CI utrzymuj trzy warstwy osobno.
Dwa dokumenty JSON wyglądające inaczej nie znaczą, że ich treść się różni. Najpierw wybierz właściwą warstwę porównania, a dopiero potem narzędzie.
1. Trzy rodzaje „diffu” to nie to samo
Wiele zespołów traktuje „porównanie JSON” jako jedną czynność, a potem wpada na te same problemy w różnych scenariuszach. Warto rozdzielić trzy warstwy:
| Warstwa | Co jest porównywane | Co ujawnia | Typowe użycie |
|---|---|---|---|
| Diff tekstowy | Surowe bajty i wiersze | Wcięcia, kolejność kluczy, spacje, nowe linie | Dryf konfiguracji, przegląd kodu |
| Diff strukturalny | Sparsowane obiekty i tablice | Dodane lub usunięte klucze, zmienione wartości, zmiany elementów tablicy | Regresja API, migracja danych |
| Diff wizualny | Sposób przedstawienia różnic | Do którego poziomu należy zmiana, jak duży jest jej zasięg | Przegląd ludzki, dyskusja zespołowa |
Diff tekstowy jest wolny od zależności, szybki i naturalnie integruje się z Git. Jego słabością jest brak wiedzy o semantyce JSON. Dwa zupełnie równoważne dokumenty zaświecą się na czerwono i zielono, jeśli różnią się kolejnością kluczy lub wcięciami. Diff strukturalny jest przeciwieństwem: najpierw parsuje, potem porównuje, więc rozpoznaje, że wartość tylko się przesunęła. Ale potrzebuje właściwej strategii wyrównywania tablic; inaczej pojedyncze wstawienie zostanie odczytane jako pełna przebudowa.
Wizualizacja nie jest trzecim algorytmem. Renderuje wynik pozostałych dwóch warstw, aby człowiek mógł szybko potwierdzić, czy różnica jest oczekiwana. Jest dobra do ludzkiej oceny i słaba jako zamiennik automatycznych kontroli. Mieszanie warstw zwykle daje potok CI, który jest wiecznie czerwony, oraz zespół, który nauczył się go ignorować.
2. Diff tekstowy: najpierw uporządkuj kolejność kluczy przez jq -S
Do szybkiego spojrzenia na dwa pliki najtańszą kombinacją jest jq -S plus diff -u. Flaga -S rekurencyjnie sortuje klucze obiektów, normalizując dokumenty o identycznej treści, ale różnej kolejności kluczy:
jq -S . a.json > a.sorted.json
jq -S . b.json > b.sorted.json
diff -u a.sorted.json b.sorted.json
To usuwa najczęstszy fałszywy alarm. RFC 8259 definiuje obiekty jako zbiory nieuporządkowane, więc {"a":1,"b":2} i {"b":2,"a":1} to ta sama wartość. Diff tekstowy oznacza jednak każdy wiersz jako zmieniony. Sortowanie najpierw zbliża czerwone bloki znacznie bardziej do rzeczywistych problemów.
Zauważ, że jq -S sortuje tylko klucze; nie sortuje tablic. Kolejność tablicy ma w JSON-ie znaczenie, a [1,2] to nie ta sama wartość co [2,1], więc narzędzie nie podejmie tej decyzji za ciebie. Jeśli tablica jest pojęciowo nieuporządkowanym zbiorem, posortuj ją sam po kluczu głównym przed porównaniem.
Jest jeszcze jeden szczegół wart zapamiętania: diff kończy się kodem 1, gdy znajdzie różnice, ale brakujący plik lub błąd uprawnień również dają niezerowy kod wyjścia. W CI nie traktuj każdego niezerowego kodu jako „treść się zmieniła”, inaczej źle wpisana ścieżka zostanie zgłoszona jako zmiana danych. Bezpieczniejszy wzorzec najpierw weryfikuje pliki, a potem obsługuje różnicę:
if ! diff -u a.sorted.json b.sorted.json; then
echo "JSON content differs" >&2
exit 1
fi
delta czyni diff tekstowy czytelnym
Surowe wyjście diff -u działa, ale długie wiersze i głęboko zagnieżdżony JSON męczą przy czytaniu. delta to filtr diffu, który dodaje podświetlanie składni, numery wierszy i wyraźniejsze kolory tła:
diff -u a.sorted.json b.sorted.json | delta
Zaczynał jako pager Git, więc potrafi też przejąć git diff. Dla osób czytających dziesięć diffów konfiguracji dziennie takie narzędzia poprawiają szybkość czytania, a nie dokładność algorytmu. Innym częstym wyborem jest git diff --no-index --word-diff, który porównuje na poziomie słów i dobrze sprawdza się przy skompresowanym JSON-ie przechowywanym w bardzo długich wierszach.
Jedno ostrzeżenie: upiększanie nie usuwa fałszywych różnic. Jeśli kolejność kluczy nie została znormalizowana, delta tylko czyni szum ładniejszym. Właściwa kolejność to zawsze najpierw normalizacja, potem dekoracja.
3. Diff strukturalny: jsondiffpatch rozumie semantykę
Gdy pytanie brzmi, czy dwa fragmenty danych są semantycznie równoważne, użyj diffu strukturalnego. Najczęstszą opcją w ekosystemie JavaScript jest jsondiffpatch, który zwraca obiekt delta opisujący zmiany, a nie wiersze tekstu ze znacznikami + i -:
import { create } from 'jsondiffpatch';
const instance = create();
const delta = instance.diff(
{ id: 1, tags: ['a', 'b'], enabled: true },
{ id: 1, tags: ['a', 'c'], enabled: false },
);
console.log(JSON.stringify(delta, null, 2));
Delta rozróżnia dodane klucze, usunięte klucze, zastąpione wartości i przesunięte elementy tablicy. Dzięki temu możesz przejść po delcie bezpośrednio w asercjach: undefined, gdy nie ma różnicy, albo opis ścieżka po ścieżce, gdy jest. W przeciwieństwie do diffu tekstowego nie przeszkadzają jej wcięcia, kolejność kluczy ani pisownia liczb, taka jak 1e3 versus 1000.
Równie ważne jest to, że wyjście diffu strukturalnego może zasilać proces łatania. jsondiffpatch udostępnia patch i reverse, więc deltę można zastosować do oryginalnego obiektu i później cofnąć. Jedno zastrzeżenie: automatycznie wygenerowana delta nie ma tego samego formatu co ręcznie pisany JSON Patch, a konwersja między nimi wymaga pracy. Szersze podejście do porównań opisano w Diff i porównywanie JSON.
4. Jak wyeliminować fałszywe różnice spowodowane kolejnością kluczy
Kolejność kluczy nie tylko irytuje; może prowadzić do błędnych wniosków. Podczas śledztwa wykonaj trzy kroki:
- Przed porównaniem znormalizuj rekurencyjnie kolejność kluczy przez
jq -Slub równoważne narzędzie. - Usuń pola zmieniające się przy każdym uruchomieniu, takie jak
timestamp,requestIditraceId. - Potwierdź, że nie ma zduplikowanych kluczy. Zduplikowane klucze są rozstrzygane przez „ostatni wygrywa” podczas parsowania, więc diff tekstowy je widzi, a strukturalny nie.
jq -S 'del(.meta.timestamp, .meta.requestId)' a.json > /tmp/a.json
jq -S 'del(.meta.timestamp, .meta.requestId)' b.json > /tmp/b.json
diff -u /tmp/a.json /tmp/b.json
Ten przepływ dobrze pasuje do dryfu konfiguracji. Przepuść staging i produkcję przez tę samą normalizację, a cokolwiek zostanie, jest różnicą, która naprawdę wymaga wyjaśnienia. Trzymaj reguły filtrowania w wersjonowanym skrypcie, zamiast przepisywać je z pamięci. Stronę edycji i skryptów łańcucha narzędzi opisano w Edytory, wiersz poleceń i automatyzacja.
5. Tablice: wyrównywać po indeksie czy po id?
Tablice to najbardziej podatna na błędy część porównania JSON. Przy domyślnym wyrównywaniu po indeksie wstawienie jednego rekordu na początku przesuwa każdy późniejszy element i narzędzie oznacza je wszystkie jako zmienione. Narzędzie nie jest zepsute; strategia wyrównywania jest zła.
| Wyrównanie | Działa, gdy | Mocna strona | Ryzyko |
|---|---|---|---|
| Po indeksie | Kolejność tablicy niesie znaczenie biznesowe | Proste, nie wymaga dodatkowych pól | Jedno wstawienie powoduje kaskadowe fałszywe alarmy |
| Po kluczu głównym | Elementy są zbiorem z unikalnym id | Wykrywa wstawienie, usunięcie i przesunięcia | Wymaga stabilnego, unikalnego klucza |
| Najdłuższy wspólny podciąg | Kolejność ma znaczenie, ale wstawienia są dozwolone | Odporniejsze niż czysty indeks | Trudniejsze do wyjaśnienia, wyższy koszt |
jsondiffpatch używa objectHash do zadeklarowania klucza głównego, indeksując elementy tablicy przed porównaniem:
const instance = create({
objectHash: (item: { id?: string; sku?: string }) => item.id ?? item.sku,
});
W zwykłym jq możesz najpierw posortować, a potem użyć diffu tekstowego:
jq -S 'sort_by(.id)' items.json > items.sorted.json
Kompromis z wyrównywaniem po id polega na tym, że zmiany pozycji nie liczą się już jako różnice. Jeśli biznes musi wiedzieć, że rekord przesunął się z pozycji 2 na pozycję 5, porównaj także jawne pole kolejności; nie polegaj tylko na zawartości zbioru.
6. Diff wizualny: warstwa zbudowana dla ludzi
Wyjście diffu strukturalnego jest zaprojektowane dla maszyn, nie dla spotkań. Gdy musisz komuś wyjaśnić zmianę, wizualizacja jest skuteczniejsza:
- Edytor diff w VS Code jest dobry do przeglądu plik po pliku, z panelami obok siebie i podświetleniami w wierszu.
- Przeglądarki drzewa, takie jak JSON Crack i JSON Hero, pokazują, do którego poziomu należy zmiana.
jsondiffpatchdostarcza formater HTML, który renderuje deltę jako zwijany, bogaty widok.
Wartością wizualizacji jest kontekst. Diff tekstowy mówi, że wiersz 88 się zmienił; widok drzewa mówi, że wiersz 88 należy do data.items[3].price. Ale wizualizacja zwykle ładuje cały dokument do przeglądarki lub aplikacji desktopowej. Przed otwarciem produkcyjnego ładunku potwierdź, czy narzędzie liczy lokalnie, czy wysyła na serwer.
7. Trzy zasady wprowadzania diffu do CI
Po pierwsze, normalizuj przed porównaniem. Ustabilizuj wcięcia, posortuj rekurencyjnie klucze i odfiltruj zmienne pola, zanim diff się uruchomi. Inaczej CI wielokrotnie zawodzi na szumie.
Po drugie, wybieraj wyrównanie według semantyki. Jeśli tablica jest zbiorem, wyrównuj po kluczu głównym. Jeśli jest sekwencją, zachowaj wyrównanie po indeksie. Nie stosuj jednej reguły do każdego API.
Po trzecie, asercuj wartości, nie tekst. Porównywanie sparsowanych obiektów w testach jest znacznie stabilniejsze niż porównywanie wyjścia JSON.stringify. Gdy kontrola zawiedzie, wypisz ścieżkę różnicy oraz wartości przed i po, zamiast zrzucać cały ładunek do logu.
# Print only the differing paths and values, not the entire document
jq -S 'del(.timestamp)' actual.json > /tmp/actual.json
jq -S 'del(.timestamp)' expected.json > /tmp/expected.json
diff -u /tmp/expected.json /tmp/actual.json | head -n 80
Jeśli różnica wymaga długoterminowego śledzenia, zatwierdź skrypt normalizacji razem z plikiem oczekiwanym. Jeśli to jednorazowe śledztwo, usuń skrypt tymczasowy później, aby po cichu nie stał się drugą implementacją logiki biznesowej.
8. Tabela doboru narzędzi
| Scenariusz | Pierwszy wybór | Alternatywa | Kluczowe przypomnienie |
|---|---|---|---|
| Sporadyczne porównanie plików | Diff w VS Code | diff -u |
Najpierw sformatuj, aby usunąć szum |
| Dryf konfiguracji | jq -S + diff |
delta |
Odfiltruj zmienne pola |
| Regresja odpowiedzi API | jsondiffpatch |
deepdiff |
Wyrównaj tablice po kluczu głównym |
| Wyjaśnienie zmiany zespołowi | JSON Crack / JSON Hero | Formater HTML | Sprawdź, czy dane są wysyłane |
| Bardzo duże pliki | Wyciągnięcie poddrzewa przez jq |
Przeglądanie w jless |
Nie ładuj całego drzewa do pamięci |
| Kontrola pre-commit | git diff --no-index |
pre-commit | Obsłuż kody wyjścia jawnie |
Nie ma jednego najlepszego narzędzia do diffu JSON; jest tylko właściwe narzędzie dla aktualnej warstwy. Diff tekstowy odkrywa zmianę, diff strukturalny ocenia równoważność, a wizualizacja wyjaśnia zmianę. Utrzymywanie trzech warstw osobno jest znacznie bardziej niezawodne niż szukanie jednego narzędzia, które robi wszystko. Obsługę konfliktów między gałęziami omówiono w JSON i scalanie w Git.
FAQ
Czy jq -S zmienia także kolejność tablic?
Nie. -S sortuje tylko klucze obiektów i zostawia tablice w oryginalnej kolejności. Tablice w JSON-ie mają kolejność, więc narzędzie nie zamieni [1,2] na [2,1]. Jeśli tablica jest pojęciowo nieuporządkowanym zbiorem, posortuj ją jawnie przez sort_by.
Czy 1 i 1.0 to ta sama wartość w diffie strukturalnym?
Większość parserów zamienia oba na tę samą liczbę, więc diff strukturalny zwykle uznaje je za równe, podczas gdy diff tekstowy oznacza je jako różne. Użyj diffu strukturalnego do rozstrzygania równoważności, a diffu tekstowego do ustalania, czy pliki są identyczne bajt w bajt.
Czy wyjście diffu można użyć bezpośrednio jako JSON Patch?
Nie bezpośrednio. Narzędzie diffu zgłasza, co się różni, podczas gdy JSON Patch opisuje uporządkowaną listę operacji; formaty i semantyka się różnią. Automatycznie generowane patche odwołują się też zwykle do indeksów tablic, które stają się nieaktualne przy współbieżności. Dodaj operację test przed ich zastosowaniem.
Jak porównywać pliki JSON o rozmiarze kilkudziesięciu megabajtów?
Najpierw wyciągnij interesujące cię poddrzewo, na przykład przez jq '.data.items', a potem posortuj po kluczu głównym i porównaj. Nie ładuj całego drzewa do pamięci na potrzeby rekurencyjnego diffu. Bardzo duże pliki lepiej obsługiwać przez strumieniowanie lub porównanie w fragmentach, a koszt pamięci wymaga osobnej oceny.
Czy narzędzia diffu online mogą wyciekać moje dane?
Zależy od implementacji. Narzędzia czysto frontendowe liczą w przeglądarce i nie wysyłają danych; narzędzia renderowane po stronie serwera wysyłają treść do backendu, gdzie może trafić do logów. Przed porównaniem produkcyjnej konfiguracji lub ładunków klientów potwierdź, że strona liczy lokalnie, a w razie potrzeby przetestuj raz z odłączoną siecią.
Jak zdecydować między diffem tekstowym a strukturalnym?
Do szybkiego spojrzenia, gdzie dwa pliki się różnią, posortuj klucze przez jq -S i uruchom diff -u. Do ustalenia, czy dwa dokumenty są semantycznie równoważne, użyj diffu strukturalnego, takiego jak jsondiffpatch, i wyrównaj tablice po kluczu głównym. Diff tekstowy jest wolny od zależności i integruje się z Git, a strukturalny nie przeszkadza wcięć, kolejności kluczy ani pisowni liczb.
Dlaczego CI nie powinno traktować każdego niezerowego kodu wyjścia diff jako różnicy?
diff kończy się kodem 1, gdy znajdzie różnice, ale brakujący plik lub błąd uprawnień również zwraca niezerowy kod. Traktowanie każdego niezerowego kodu jako „treść się zmieniła” zgłasza źle wpisaną ścieżkę jako zmianę danych. Bezpieczniejszy wzorzec najpierw weryfikuje pliki, a potem osobno obsługuje różnicę.
Czy porównywać w przeglądarce pliki o rozmiarze kilkudziesięciu megabajtów?
Nie. Najpierw wyciągnij interesujące cię poddrzewo przez jq, a potem posortuj po kluczu głównym i porównaj. Ładowanie całego drzewa do pamięci na rekurencyjny diff kosztuje znacznie więcej pamięci i czasu. Bardzo duże pliki lepiej obsługiwać przez strumieniowanie lub porównanie w fragmentach.