JSON Toolkit

JSON Pointer, Patch i Merge Patch

Podsumowanie: Trzy krótkie specyfikacje tworzą protokół zmian: Pointer lokalizuje, Patch edytuje precyzyjnie, Merge Patch scala częściowo. Zły wybór boli przy tablicach i współbieżności.

Średniozaawansowany~12 min czytaniaOpublikowano 2026-10-30Zaktualizowano 2026-10-30
Okładka artykułu o JSON Pointer, Patch i Merge Patch

Najważniejsze: używaj JSON Pointer do lokalizowania po pozycji, a JSONPath do filtrowania lub rekurencji; wstawianie i usuwanie elementów tablicy, zapis null oraz asercje wersji oddaj JSON Patch, a Merge Patch zarezerwuj dla płaskiej konfiguracji i zasobów formularzy.

Lokalizowanie i modyfikowanie to dwie różne rzeczy, a specyfikacje dzielą je na trzy dokumenty. Typową ceną ich mylenia jest „cała tablica została zastąpiona” i nikt tego nie zauważa.

1. JSON Pointer: najkrótsza składnia lokalizowania

RFC 6901 robi dokładnie jedną rzecz: wskazuje węzeł w dokumencie za pomocą łańcucha.

""             → the whole document
"/store"       → the store member under the root
"/store/book"  → one level deeper
"/store/book/0/title" → item 0 of the array

Trzy reguły:

  1. Pusty łańcuch wskazuje sam węzeł główny, a nie „pusty węzeł”;
  2. Każdy / rozdziela jeden token referencyjny; tablice używają indeksów dziesiętnych (zera wiodące są niedozwolone, więc 01 jest niepoprawne);
  3. Wewnątrz tokenu ~ zapisuje się jako ~0, a / jako ~1 — zastąp ~ przed /, inaczej skończysz z podwójną ucieczką.
/**
 * Escapes a single reference token into a JSON Pointer segment.
 * @param token Original key name or index
 * @returns Escaped token
 */
function escapePointerToken(token: string): string {
  return token.replace(/~/g, '~0').replace(/\//g, '~1');
}

/**
 * Joins an array of key names into a JSON Pointer.
 * @param tokens Array of key names or indices
 * @returns Pointer string beginning with /
 */
function buildPointer(tokens: Array<string | number>): string {
  return tokens.map((token) => `/${escapePointerToken(String(token))}`).join('');
}

Wartość widać najlepiej, gdy sam klucz zawiera /: wskaźnik dla {"a/b": 1} to /a~1b, a nie /a/b (to drugie znaczy b pod a).

Używany wewnątrz URL wymaga też kodowania procentowego, bo # i ~ mają znaczenie specjalne w fragmencie.

2. JSON Patch: atomowa sekwencja sześciu operacji

RFC 6902 opisuje zmiany tablicą operacji, typ nośny application/json-patch+json:

[
  { "op": "test", "path": "/version", "value": 7 },
  { "op": "replace", "path": "/name", "value": "新的名字" },
  { "op": "add", "path": "/tags/-", "value": "json" },
  { "op": "remove", "path": "/temp" },
  { "op": "copy", "from": "/name", "path": "/alias" },
  { "op": "move", "from": "/tags/0", "path": "/tags/1" }
]
Operacja Semantyka Granica
add Dodaje składową lub wstawia element tablicy Indeks tablicy może równać się długości (dopisanie); - oznacza koniec
remove Usuwa Ścieżka musi istnieć
replace Zastępuje istniejącą wartość Ścieżka musi istnieć (równoważne remove + add, ale wymaga najpierw istnienia ścieżki)
move Przesuwa Nie można przenieść węzła pod jego własną podścieżkę
copy Kopiuje Źródło musi istnieć
test Asercja równości Jeśli zawiedzie, cały patch nie zostaje zastosowany; używane do optymistycznego blokowania

Kluczową właściwością jest atomowość: jeśli dowolna operacja zawiedzie, cały patch się cofa, więc nie ma stanu pośredniego, w którym „pierwsze trzy się zastosowały, a czwarta zawiodła”. To czyni go świetnym ciałem żądania PATCH.

2.1 Używanie test do optymistycznej współbieżności

[
  { "op": "test", "path": "/etag", "value": "a3f9c1" },
  { "op": "replace", "path": "/status", "value": "paid" }
]

Klient najpierw GET-uje etag i wpisuje go do operacji test. Przy współbieżnym zapisie test drugiego żądania zawodzi i serwer zwraca 409 Conflict lub 412 Precondition Failed. To znacznie bezpieczniejsze niż last-write-wins i nie wymaga rozproszonej blokady.

2.2 Operacje na tablicach są najbardziej podatne na błędy

// Want to insert at index 1
{ "op": "add", "path": "/items/1", "value": {...} }   // Correct: insert; the original index 1 and everything after shift back

// Want to replace index 1
{ "op": "replace", "path": "/items/1", "value": {...} } // Correct: replace

add pod indeksem tablicy n to wstawienie, a nie nadpisanie; indeks poza zakresem (n > length) musi zgłosić błąd. Niektóre implementacje są w tym łagodne, przez co ten sam patch zachowuje się różnie na różnych serwerach.

3. Merge Patch: fragment, który wygląda jak cel

RFC 7386 ma tylko cztery reguły:

  1. Cel jest obiektem i patch jest obiektem → scalaj rekurencyjnie;
  2. Wartość klucza w patchu to null → usuń ten klucz z celu;
  3. Wartość klucza w patchu nie jest obiektem → zastąp bezpośrednio;
  4. Sam patch nie jest obiektem → cały cel zostaje zastąpiony.
// Target
{ "a": "b", "c": { "d": "e", "f": "g" } }

// Patch
{ "a": "z", "c": { "f": null } }

// Result
{ "a": "z", "c": { "d": "e" } }

Typ nośny to application/merge-patch+json. Czyta się jak „częściowa aktualizacja”, co czyni go szczególnie łatwym do niewłaściwego użycia:

  • Tablice można tylko zastępować w całości. Chcesz usunąć list[2]? Merge Patch tego nie potrafi;
  • Nie można zapisać null. null jest już zajęte przez znaczenie usunięcia;
  • Nie potrafi wyrazić trzeciego stanu między „zostaw to pole w spokoju” a „usuń to pole” (na przykład jawnego ustawienia go na puste).

4. Macierz możliwości: który wybrać

Wymaganie JSON Patch Merge Patch
Precyzyjne zastąpienie konkretnego elementu tablicy ✓ ✗
Usunięcie jednego elementu z tablicy ✓ ✗
Częściowa aktualizacja kilku pól ✓ (wiele replace) ✓ (najzwięźlej)
Zapis wartości null ✓ ✗
Kontrola współbieżności (asercja wersji) ✓ (test) ✗ (tylko przez If-Match)
Czytelność patcha Średnia Dobra (wygląda jak same dane)
Złożoność implementacji Wysoka Niska
Pasujące kształty zasobów Głęboko zagnieżdżone, listowe Płaska konfiguracja, formularze

Zasady praktyczne:

  • Formularze, strony ustawień, zasoby konfiguracji → Merge Patch; frontend wysyła tylko zmienione pola;
  • Dodawanie i usuwanie list, przejścia maszyny stanów, cokolwiek wymagającego ochrony współbieżności → JSON Patch;
  • Oba mogą współistnieć: różne punkty końcowe deklarują różne Content-Type, a serwer kieruje według typu.

5. Uwagi implementacyjne

import { applyPatch, compare } from 'fast-json-patch';

const document = { version: 7, items: [{ id: 1 }, { id: 2 }] };
const patch = compare(document, { version: 8, items: [{ id: 1 }] });

// test operations are validated by default; setting mutateDocument to true modifies in place, use with caution
const result = applyPatch(document, patch, true, false);
console.log(result.newDocument);

Cztery łatwe do przeoczenia punkty:

  1. Nie mutuj współdzielonych obiektów w miejscu; zawiedziony patch zostawia na wpół zmieniony stan (chyba że implementacja gwarantuje cofnięcie);
  2. Walidacja ścieżek: patche z zewnątrz muszą walidować każdy path/from i odrzucać klucze takie jak __proto__ i constructor;
  3. Limity rozmiaru: ogranicz liczbę wpisów patcha, aby pojedynczy patch nie wykonywał setek tysięcy operacji;
  4. Audytowanie: patche naturalnie pasują do logów audytu — są kompletnym opisem „co się zmieniło”.

6. Semantyka błędów

Sytuacja Sugerowany kod statusu Wyjaśnienie
Patch nie jest tablicą / jest strukturalnie niepoprawny 400 Bad Request Format treści żądania jest zły
Węzeł wskazany przez path nie istnieje 422 Unprocessable Entity Składniowo poprawne, ale semantycznie niewykonalne
Operacja test zawiodła 409 lub 412 Wskazuje konflikt wersji
Brak uprawnień do modyfikacji tej ścieżki 403 Forbidden Nie maskuj tego przez 404, chyba że świadomie ukrywasz istnienie zasobu

Rozróżnianie 400 i 422 ma znaczenie: pierwsze znaczy „nie rozumiem twojego patcha”, drugie „rozumiem go, ale te dane nie pozwalają na taką zmianę”.

7. Lista kontrolna

  • [ ] Punkt końcowy deklaruje właściwy Content-Type (json-patch+json / merge-patch+json);
  • [ ] Stosowanie patcha jest atomowe, a porażka nie zostawia częściowego wyniku;
  • [ ] Zewnętrzne patche są walidowane względem białej listy ścieżek i limitu liczby wpisów;
  • [ ] Kontrola współbieżności używa test lub If-Match;
  • [ ] Patche są zapisywane do logu audytu;
  • [ ] Scenariusze tablicowe jawnie wybierają JSON Patch; nie licz na Merge Patch.

FAQ

Jak zdecydować między JSON Patch a Merge Patch?

Użyj JSON Patch, gdy musisz zastąpić lub usunąć konkretny element tablicy, zapisać wartość null albo asercować wersję; użyj Merge Patch dla formularzy i zasobów konfiguracji, które aktualizują tylko kilka pól. Merge Patch nie potrafi usunąć pojedynczego elementu tablicy ani zapisać null, ponieważ null już znaczy usunięcie.

Dlaczego add pod indeksem tablicy n nie jest nadpisaniem?

Bo add wstawia do tablicy: element pod indeksem n i wszystko za nim przesuwa się w tył. Aby zastąpić istniejący element, musisz użyć replace, a ścieżka musi najpierw istnieć. Indeks poza zakresem (n > length) powinien zgłosić błąd; łagodne implementacje sprawiają, że ten sam patch zachowuje się różnie na różnych serwerach.

Czy modyfikować oryginalny obiekt w miejscu?

Nie. Zawiedziony patch zostawia na wpół zmieniony stan, chyba że implementacja gwarantuje cofnięcie. Zastosuj patch do głęboko skopiowanej lub sklonowanej strukturalnie kopii, a po sukcesie podmien referencję.