JSON Toolkit

JSON Pointer, Patch a Merge Patch

Shrnutí: Tři krátké specifikace tvoří protokol změn: Pointer lokalizuje, Patch upravuje přesně, Merge Patch slučuje částečně. Špatná volba škodí u polí a souběžnosti.

Středně pokročilý~12 min čteníPublikováno 2026-10-30Aktualizováno 2026-10-30
Titulní obrázek článku JSON Pointer, Patch a Merge Patch

Stručně: JSON Pointer použijte k lokalizaci podle pozice a JSONPath k filtrování nebo rekurzi; vkládání a odebírání v polích, zápisy null a verifikační asserty předejte JSON Patch a Merge Patch si nechte pro plochou konfiguraci a formulářové zdroje.

Lokalizace a úprava jsou dvě různé věci a specifikace je rozdělují do tří dokumentů. Obvyklá cena jejich záměny je „celé pole se nahradilo“ a nikdo si toho nevšimne.

1. JSON Pointer: nejkratší lokalizační syntaxe

RFC 6901 dělá přesně jednu věc: ukazuje řetězcem na uzel v dokumentu.

""             → celý dokument
"/store"       → člen store pod kořenem
"/store/book"  → o úroveň hlouběji
"/store/book/0/title" → položka 0 pole

Tři pravidla:

  1. Prázdný řetězec ukazuje na samotný kořenový uzel, nikoli na „prázdný uzel“;
  2. Každé / odděluje jeden referenční token; pole používají desetinné indexy (úvodní nuly nejsou povoleny, takže 01 je neplatné);
  3. Uvnitř tokenu se ~ zapisuje jako ~0 a / jako ~1 – nahradte ~ před /, jinak skončíte s dvojím escapováním.
/**
 * Escapuje jeden referenční token do segmentu JSON Pointeru.
 * @param token Původní název klíče nebo index
 * @returns Escapovaný token
 */
function escapePointerToken(token: string): string {
  return token.replace(/~/g, '~0').replace(/\//g, '~1');
}

/**
 * Spojí pole názvů klíčů do JSON Pointeru.
 * @param tokens Pole názvů klíčů nebo indexů
 * @returns Řetězec pointeru začínající /
 */
function buildPointer(tokens: Array<string | number>): string {
  return tokens.map((token) => `/${escapePointerToken(String(token))}`).join('');
}

Hodnota se ukáže nejlépe, když klíč sám obsahuje /: pointer pro {"a/b": 1} je /a~1b, nikoli /a/b (to druhé znamená b pod a).

Při použití uvnitř URL je nutné i percent-encoding, protože # a ~ mají ve fragmentu zvláštní význam.

2. JSON Patch: atomická posloupnost šesti operací

RFC 6902 popisuje změny pomocí pole operací, mediálního typu 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" }
]
Operace Sémantika Hranice
add Přidá člen nebo vloží prvek pole Index pole může být roven délce (připojení); - znamená konec
remove Odebere Cesta musí existovat
replace Nahradí existující hodnotu Cesta musí existovat (ekvivalent remove + add, ale vyžaduje, aby cesta nejprve existovala)
move Přesune Uzel nelze přesunout pod jeho vlastní podcestu
copy Zkopíruje Zdroj musí existovat
test Tvrzení rovnosti Pokud selže, celý patch se neuplatní; používá se pro optimistické zamykání

Klíčovou vlastností je atomicita: pokud kterákoli operace selže, celý patch se vrátí, takže neexistuje mezistav „první tři se uplatnily a čtvrtá selhala“. To ho činí skvělým tělem požadavku PATCH.

2.1 Použití test pro optimistickou souběžnost

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

Klient nejprve GETem získá etag a zapíše ho do operace test. Při souběžných zápisech test druhého požadavku selže a server vrátí 409 Conflict nebo 412 Precondition Failed. To je mnohem bezpečnější než last-write-wins a nevyžaduje distribuovaný zámek.

2.2 Operace s poli jsou nejnáchylnější k chybám

// Chceme vložit na index 1
{ "op": "add", "path": "/items/1", "value": {...} }   // Správně: vloží; původní index 1 a vše za ním se posune zpět

// Chceme nahradit index 1
{ "op": "replace", "path": "/items/1", "value": {...} } // Správně: nahradí

add na index pole n je vložení, nikoli přepsání; index mimo rozsah (n > length) musí vyhodit chybu. Některé implementace jsou v tomto benevolentní, což způsobuje, že se stejný patch chová na různých serverech odlišně.

3. Merge Patch: fragment, který vypadá jako cíl

RFC 7386 má jen čtyři pravidla:

  1. Cíl je objekt a patch je objekt → rekurzivní sloučení;
  2. Hodnota klíče v patch je null → smaž ten klíč z cíle;
  3. Hodnota klíče v patch není objekt → nahrad přímo;
  4. Patch sám není objekt → celý cíl se nahradí.
// Cíl
{ "a": "b", "c": { "d": "e", "f": "g" } }

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

// Výsledek
{ "a": "z", "c": { "d": "e" } }

Mediální typ je application/merge-patch+json. Čte se jako „částečná aktualizace“, což činí jeho zneužití obzvlášť snadným:

  • Pole lze jen nahradit celá. Chcete smazat list[2]? Merge Patch to neumí;
  • Nelze zapsat null. null už znamená smazání;
  • Nelze vyjádřit třetí stav mezi „nech toto pole být“ a „smaž toto pole“ (například explicitní nastavení na prázdno).

4. Matice schopností: který zvolit

Požadavek JSON Patch Merge Patch
Přesně nahradit konkrétní prvek pole ✓ ✗
Smazat jednu položku z pole ✓ ✗
Částečně aktualizovat několik polí ✓ (více replace) ✓ (nejstručnější)
Zapsat hodnotu null ✓ ✗
Řízení souběžnosti (verifikační assert) ✓ (test) ✗ (jen přes If-Match)
Čitelnost patchu Střední Dobrá (vypadá jako data samotná)
Složitost implementace Vysoká Nízká
Vhodné tvary zdrojů Hluboce vnořené, seznamové Plochá konfigurace, formuláře

Pravidla palce:

  • Formuláře, stránky nastavení, konfigurační zdroje → Merge Patch; frontend posílá jen změněná pole;
  • Přidávání a odebírání v seznamech, přechody stavových automatů, cokoli vyžadující ochranu souběžnosti → JSON Patch;
  • Oba mohou koexistovat: různé endpointy deklarují různé Content-Type a server dispatchuje podle typu.

5. Poznámky k implementaci

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 }] });

// Operace test se ve výchozím nastavení validují; nastavení mutateDocument na true upravuje na místě, používejte opatrně
const result = applyPatch(document, patch, true, false);
console.log(result.newDocument);

Čtyři snadno přehlédnutelné body:

  1. Nemodifikujte sdílené objekty na místě; selhaný patch nechává polovičně upravený stav (pokud implementace nezaručuje rollback);
  2. Validace cest: patche zvenčí musí validovat každý path/from a odmítnout klíče jako __proto__ a constructor;
  3. Limity velikosti: ohraničte počet položek patchu, aby jeden patch neprováděl stovky tisíc operací;
  4. Audit: patche se přirozeně hodí pro auditní logy – jsou úplným popisem „co se změnilo“.

6. Sémantika chyb

Situace Navržený stavový kód Vysvětlení
Patch není pole / je strukturálně neplatný 400 Bad Request Formát těla požadavku je špatný
Uzel odkazovaný path neexistuje 422 Unprocessable Entity Syntakticky platné, ale sémanticky nevykonatelné
Operace test selhala 409 nebo 412 Označuje konflikt verze
Žádné oprávnění upravit tu cestu 403 Forbidden Neskrývejte to 404, pokud záměrně neskrýváte existenci zdroje

Rozlišovat 400 a 422 má smysl: první znamená „nerozumím vašemu patchu“, druhé „rozumím, ale tato data tu změnu nedovolují“.

7. Kontrolní seznam

  • [ ] Endpoint deklaruje správný Content-Type (json-patch+json / merge-patch+json);
  • [ ] Aplikace patchu je atomická a selhání nezanechá částečný výsledek;
  • [ ] Externí patche jsou validovány proti whitelistu cest a limitu počtu položek;
  • [ ] Řízení souběžnosti používá test nebo If-Match;
  • [ ] Patche se zapisují do auditního logu;
  • [ ] Scénáře s poli výslovně volí JSON Patch; nepočítejte s Merge Patch.

Časté dotazy

Jak se rozhodnout mezi JSON Patch a Merge Patch?

JSON Patch použijte, když potřebujete nahradit nebo odebrat konkrétní prvek pole, zapsat hodnotu null nebo tvrdit verzi; Merge Patch pro formuláře a konfigurační zdroje, které aktualizují jen několik polí. Merge Patch neumí smazat jednu položku pole a neumí zapsat null, protože null už znamená smazání.

Proč add na index pole n není přepsání?

Protože add vkládá do pole: prvek na indexu n a všechno za ním se posune zpět. Pro nahrazení existujícího prvku musíte použít replace a cesta musí nejprve existovat. Index mimo rozsah (n > length) má vyhodit chybu; benevolentní implementace činí stejný patch na různých serverech odlišný.

Mám upravovat původní objekt na místě?

Ne. Selhaný patch nechává polovičně upravený stav, pokud implementace nezaručuje rollback. Aplikujte patch na hluboce zkopírovanou nebo strukturovaně klonovanou kopii a po úspěchu prohodte referenci.