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.

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:
- Prázdný řetězec ukazuje na samotný kořenový uzel, nikoli na „prázdný uzel“;
- Každé
/odděluje jeden referenční token; pole používají desetinné indexy (úvodní nuly nejsou povoleny, takže01je neplatné); - Uvnitř tokenu se
~zapisuje jako~0a/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:
- Cíl je objekt a patch je objekt → rekurzivní sloučení;
- Hodnota klíče v patch je
null→ smaž ten klíč z cíle; - Hodnota klíče v patch není objekt → nahrad přímo;
- 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.nulluž 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-Typea 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:
- Nemodifikujte sdílené objekty na místě; selhaný patch nechává polovičně upravený stav (pokud implementace nezaručuje rollback);
- Validace cest: patche zvenčí musí validovat každý
path/froma odmítnout klíče jako__proto__aconstructor; - Limity velikosti: ohraničte počet položek patchu, aby jeden patch neprováděl stovky tisíc operací;
- 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á
testneboIf-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.