JSON Toolkit

JSON Patch v produkci: atomicita, idempotence a souběžné konflikty

Shrnutí: JSON Patch je jen formát změn; atomicita, idempotence a souběžnost jsou stále váš úkol. Pokrývá předpoklady `test`, escapování cest, rollback a koordinaci ETag.

Pokročilý~10 min čteníPublikováno 2026-10-31Aktualizováno 2026-10-31
Titulní obrázek článku JSON Patch v produkci

Stručně: nejprve validujte cílové cesty a oprávnění patchu, poté přidejte souběžný assert pomocí test nebo If-Match; aplikujte patch na kopii, aby selhání nezanechalo nic napůl zapsaného.

Skutečné nebezpečí požadavku PATCH není zápis špatného pole. Je to víra, že zápis uspěl, zatímco tiše přepisujete změnu někoho jiného. JSON Patch vám dává nástroje, ale návrh bezpečnosti je stále váš.

1. Rychlá rekapitulace sady operací RFC 6902

JSON Patch popisuje změny jako pole operací s mediálním typem application/json-patch+json. Šest operací se provádí v pořadí pole:

Operace Účinek Klíčové omezení
add Přidá člen nebo vloží prvek pole Index pole může být roven délce; - znamená konec
remove Odebere člen nebo prvek Cílová cesta musí existovat
replace Nahradí existující hodnotu Cílová cesta musí existovat
move Přesune uzel Nelze přesunout uzel do jeho vlastní podcesty
copy Zkopíruje uzel Zdrojová cesta musí existovat
test Tvrzení rovnosti hodnot Pokud selže, celý patch se neuplatní

Mnoho lidí si pamatuje jen první tři a zapomíná, že test je to, co mění slepý zápis v podmíněný zápis. Úplné sémantice operací a hranicím polí se věnuje JSON Pointer, Patch a Merge Patch.

2. Použití test k přeměně patchu v podmíněnou aktualizaci

Typický patch pro optimistické zamykání nezačíná zápisem, ale verzovacím assertem:

{"op":"test","path":"/version","value":1}

Vložte ho do kompletního patchu:

[
  { "op": "test", "path": "/version", "value": 1 },
  { "op": "replace", "path": "/version", "value": 2 },
  { "op": "replace", "path": "/status", "value": "paid" }
]

Logika je: potvrď, že verze dokumentu je aktuálně 1, poté ji změň na 2. Pokud jiný požadavek už verzi posunul na 2, první test selže, celý patch se neuplatní a server vrátí konflikt. Tím se zabrání ztraceným aktualizacím, aniž by zaváděl distribuovaný zámek.

Několik designových bodů:

  • Dejte test před každý zápis. Pokud už dřívější zápis účinkoval, assert je bezvýznamný;
  • Assertujte na číslo verze nebo digest generovaný serverem, nikoli na obyčejné pole, které klient může volně měnit;
  • Pri každém úspěšném zápisu verzi zvyšte; jinak dva požadavky se stejnou starou verzí projdou oba;
  • value v test musí mít přesně stejný JSON typ jako hodnota v dokumentu. 1 a "1" nejsou rovné.

3. Escapování cest a prototype pollution

Pravidla escapování JSON Pointer jsou jednoduchá: ~ jako ~0, / jako ~1 a nahradte ~ před /, jinak dostanete dvojí escapování.

Klíč "a/b"    → /a~1b
Klíč "m~n"    → /m~0n
Klíč "a~/b"   → /a~0~1b

Riziko u externích patchů obvykle není escapování, ale nebezpečné klíče. V JavaScriptu mohou klíče jako __proto__, constructor a prototype vyvolat prototype pollution, a to i přes obyčejný add:

[
  { "op": "add", "path": "/__proto__/polluted", "value": true }
]

Použijte allowlist, nikoli denylist:

  1. Povolte patchům upravovat jen obchodní pole, například /status nebo /items/0/quantity;
  2. Validujte každý path a from proti regulárnímu výrazu nebo prefixu cesty a odmítněte nečekané úrovně jiné než indexy pole;
  3. Parsujte JSON do kontejnerů Object.create(null) nebo Map, abyste snížili dopad znečištěného řetězce prototypů;
  4. Ohraničte počet operací a bajtovou velikost každého value.

4. Atomická aplikace a rollback

RFC 6902 vyžaduje atomické provedení, ale atomicita je sémantický požadavek, nikoli výchozí chování každé knihovny. Knihovny jako fast-json-patch modifikují dokument na místě, když je mutateDocument true, a selhání může zanechat částečné změny.

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

/**
 * Aplikuje patch imutabilně a při selhání vrátí původní dokument.
 * @param document Původní dokument, který volající nesmí modifikovat
 * @param patch Pole operací JSON Patch k aplikaci
 * @returns Výsledek včetně příznaku úspěchu a nového dokumentu
 */
function applySafely(document: unknown, patch: unknown[]) {
  const errors = validate(patch, document);

  if (errors) {
    return { ok: false, document };
  }

  try {
    const result = applyPatch(document, patch, false, false);
    return { ok: true, document: result.newDocument };
  } catch {
    return { ok: false, document };
  }
}

Tři implementační doporučení:

  • Nejprve hluboká kopie nebo strukturované klonování, aplikujte na kopii a referenci prohodte až po úspěchu;
  • Nespoléhejte na automatický rollback knihovny; chování při výjimkách se mezi implementacemi liší;
  • Validujte před provedením: validate kontroluje jen strukturu, zatímco sémantické konflikty stále vyžadují test; ani jedno nenahrazuje druhé.

Pokud patch zahrnuje více zdrojů, například mění objednávku i její inventář, atomicita JSON Patch platí jen uvnitř jediného dokumentu. Napříč dokumenty potřebujete databázovou transakci nebo kompenzační akce.

5. Idempotence: PATCH není přirozeně idempotentní

HTTP sémantika definuje PUT a DELETE jako idempotentní, ale PATCH nutně ne. Opakování stejného JSON Patch může produkovat zcela odlišné výsledky:

  • add /items/- připojuje na konec pole pokaždé, takže opakování vytváří duplikáty;
  • replace /version změní verzi 1 na 2 a při opakování uspěje znovu, i když už byla operace aplikována;
  • remove /temp selže na druhém běhu, protože cesta už neexistuje.

Tři běžné způsoby, jak dosáhnout idempotence:

Přístup Jak funguje Nejlepší pro
Verzovací předpoklad První operace je test /version, poté inkrementace Zdroje s explicitní verzí
Idempotency key Klient posílá Idempotency-Key; server cachuje výsledek Platby, objednávky a jiné zápisy
Patch cílového stavu Použijte replace místo add, aby výsledek závisel jen na konečné hodnotě Konfigurace a stavová pole

Nejhorší vzor je patch, který míchá add a remove a přitom předpokládá, že opakování jsou bezpečná. Pokud patch něco připojuje nebo maže, musí být spárován s verzovacím assertem nebo idempotency key.

6. Souběžné konflikty: ETag a If-Match

Kromě test uvnitř patchu HTTP nabízí standardní mechanismus: server vrací ETag a klient posílá If-Match v dalším požadavku.

GET /orders/42 HTTP/1.1
Host: api.example.com

HTTP/1.1 200 OK
ETag: "a3f9c1"
Content-Type: application/json
PATCH /orders/42 HTTP/1.1
Host: api.example.com
If-Match: "a3f9c1"
Content-Type: application/json-patch+json

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

Pokud se zdroj změnil, server vrátí 412 Precondition Failed a klient znovu načte data, než zopakuje pokus. If-Match: * znamená „proveď, dokud zdroj existuje“, což se nehodí, když potřebujete zabránit přepsání.

Rozdíl mezi test a If-Match:

  • If-Match se kontroluje na HTTP vrstvě před parsováním těla, takže je levnější;
  • test se kontroluje na sémantické vrstvě patchu a umí tvrdit jakékoli pole, aniž by závisel na podpoře ETag na serveru;
  • Lze je kombinovat, ale neimplementujte jen jedno a netvrďte, že endpoint je při souběžnosti bezpečný.

7. Rozdíly mezi knihovnami a výběr

Implementace Jazyk Atomicita Podpora test Poznámky
fast-json-patch JavaScript Volitelná (ve výchozím nastavení vrací nový dokument) Ano Široký ekosystém; pozor na mutateDocument
jsonpatch Python Vrací zpět při výjimkách Ano Blízko standardu, s jasnými chybami
json-patch Go Pracuje s kopií Ano Často používaný vedle JSON Merge Patch
zjsonpatch Java Ano Ano Pohodlná integrace s Jacksonem

Vybírejte knihovnu nejen podle „podporuje všech šest operací“. Potvrďte tři věci: zda selhání vrací zpět, zda test porovnává striktně a zda add s indexem mimo rozsah vyhazuje chybu. Volné implementace činí stejný patch v různých prostředích odlišný.

8. Bezpečnostní kontrolní seznam před nasazením

  • [ ] Endpoint deklaruje application/json-patch+json;
  • [ ] Patche se aplikují na kopii a selhání nikdy neznečistí původní dokument;
  • [ ] Každý path a from v externích patchech projde kontrolou allowlistu;
  • [ ] Nebezpečné klíče jako __proto__, constructor a prototype jsou odmítnuty;
  • [ ] Počet operací a velikost jedné hodnoty jsou ohraničeny;
  • [ ] test nebo If-Match poskytuje řízení souběžnosti;
  • [ ] Patche se zapisují do auditního logu s aktérem, časovým razítkem a úplným polem operací;
  • [ ] Transakční hranice jsou explicitní, když patch zahrnuje více dokumentů.

Patch je ze své podstaty úplným popisem „co se změnilo“, což z něj činí přirozený auditní záznam. Chybovým kódům a idempotenci na vrstvě API se věnuje Návrh JSON API. Pokud patch pochází ze sloučení dvou větví, potřebujete také sémantiku konfliktů, kterou pokrývá Slučování JSON souborů v Git.

Časté dotazy

Jaký stavový kód má vrátit selhaná operace test?

Pokud konflikt pochází z verzovacího assertu, 409 Conflict i 412 Precondition Failed jsou obhajitelné. Preferujte 412 pro soulad s If-Match a do těla odpovědi zahrňte aktuální verzi, aby klient mohl opakovat.

Lze patch obsahující add a remove bezpečně opakovat?

Ne přímo. add na konec pole duplikuje data a remove selže na druhém běhu, protože cesta je pryč. Přidejte verzovací předpoklad nebo idempotency key, aby opakování byla rozpoznána jako stejná operace.

Může JSON Patch zaručit transakce napříč dokumenty?

Ne. Atomicita RFC 6902 platí pro jediný dokument JSON. Když jsou zapojeny objednávky, inventář nebo jiné zdroje, použijte databázové transakce, Saga kompenzaci nebo frontu zpráv pro konzistenci.

Proč patch nesmí nikdy zapisovat do __proto__?

Protože většina JavaScriptových implementací ukládá dokument do obyčejných objektů a zápis __proto__ může změnit řetězec prototypů a ovlivnit každý objekt poté. Allowlist cest je nejspolehlivější obrana; nespoléhejte jen na interní filtrování knihovny.

Jak udělat požadavek PATCH idempotentní?

Existují tři běžné přístupy: začněte patch s test /version a při úspěchu verzi zvyšte, nechte server cachovat výsledek pro Idempotency-Key, nebo použijte replace místo add, aby výsledek závisel jen na konečné hodnotě. Pokud patch něco připojuje nebo maže, musí být spárován s verzovacím assertem nebo idempotency key; nelze ho prostě opakovat.

Mám použít test i If-Match současně?

Nemusíte používat obojí, ale implementace jen jednoho nečiní endpoint při souběžnosti bezpečným. If-Match se kontroluje na HTTP vrstvě před parsováním těla, takže je levnější; test se kontroluje na sémantické vrstvě patchu a umí tvrdit jakékoli pole, aniž by závisel na podpoře ETag na serveru.