JSON Toolkit

JSON Pointer, Patch e Merge Patch

In sintesi: Tre specifiche brevi formano un protocollo di cambiamento: Pointer localizza, Patch modifica con precisione, Merge Patch unisce parzialmente. Scegliere male danneggia array e concorrenza.

Intermedio~12 min di letturaPubblicato il 2026-10-30Aggiornato il 2026-10-30
Copertina di JSON Pointer, Patch e Merge Patch

In sintesi: usa JSON Pointer per localizzare per posizione e JSONPath per filtrare o ricorrere; affida inserimento/rimozione di array, scritture null e asserzioni di versione a JSON Patch, e riserva Merge Patch per configurazione piatta e risorse di form.

Localizzare e modificare sono due cose diverse, e le specifiche le dividono in tre documenti. Il prezzo usuale del confonderle è "l'intero array è stato sostituito" e nessuno se ne accorge.

1. JSON Pointer: la sintassi di localizzazione più corta

RFC 6901 fa esattamente una cosa: puntare a un nodo in un documento con una stringa.

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

Tre regole:

  1. La stringa vuota punta al nodo radice stesso, non a un "nodo vuoto";
  2. Ogni / separa un token di riferimento; gli array usano indici decimali (gli zeri iniziali non sono ammessi, quindi 01 è invalido);
  3. Dentro un token, ~ si scrive come ~0 e / come ~1 — sostituisci ~ prima di /, altrimenti finisci per fare doppio escaping.
/**
 * 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('');
}

Il valore si mostra meglio quando la chiave stessa contiene /: il pointer per {"a/b": 1} è /a~1b, non /a/b (quest'ultimo significa b sotto a).

Quando viene usato dentro un URL, è necessaria anche la codifica percentuale, perché # e ~ hanno significati speciali in un frammento.

2. JSON Patch: una sequenza atomica di sei operazioni

RFC 6902 descrive i cambiamenti con un array di operazioni, media type 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" }
]
Operazione Semantica Confine
add Aggiunge un membro o inserisce un elemento di array L'indice di array può eguagliare la lunghezza (append); - significa la fine
remove Rimuove Il percorso deve esistere
replace Sostituisce un valore esistente Il percorso deve esistere (equivalente a remove + add, ma richiede che il percorso esista prima)
move Sposta Non puoi spostare un nodo sotto il proprio sottopercorso
copy Copia La sorgente deve esistere
test Asserisce l'uguaglianza Se fallisce, l'intera patch non viene applicata; usato per l'optimistic locking

La proprietà chiave è l'atomicità: se qualsiasi operazione fallisce, l'intera patch fa rollback, quindi non esiste uno stato intermedio in cui "le prime tre sono state applicate e la quarta è fallita". Questo lo rende un ottimo corpo di richiesta PATCH.

2.1 Usare test per la concorrenza ottimistica

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

Il client fa prima GET dell'etag e lo scrive nell'operazione test. Su scritture concorrenti, il test della seconda richiesta fallisce e il server restituisce 409 Conflict o 412 Precondition Failed. Questo è molto più sicuro del last-write-wins e non richiede un lock distribuito.

2.2 Le operazioni sugli array sono le più soggette a errori

// 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 all'indice di array n è un inserimento, non una sovrascrittura; un indice fuori intervallo (n > length) deve sollevare un errore. Alcune implementazioni sono indulgenti su questo, il che fa comportare la stessa patch diversamente tra server.

3. Merge Patch: un frammento che assomiglia alla destinazione

RFC 7386 ha solo quattro regole:

  1. La destinazione è un oggetto e la patch è un oggetto → unisci ricorsivamente;
  2. Il valore di una chiave nella patch è null → elimina quella chiave dalla destinazione;
  3. Il valore di una chiave nella patch non è un oggetto → sostituisci direttamente;
  4. La patch stessa non è un oggetto → l'intera destinazione viene sostituita.
// Target
{ "a": "b", "c": { "d": "e", "f": "g" } }

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

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

Il media type è application/merge-patch+json. Si legge come un "aggiornamento parziale", il che lo rende particolarmente facile da usare male:

  • Gli array possono solo essere sostituiti in blocco. Vuoi eliminare list[2]? Merge Patch non può farlo;
  • Non puoi scrivere null. null è già preso per significare eliminazione;
  • Non può esprimere un terzo stato tra "lascia questo campo com'è" e "elimina questo campo" (per esempio, impostarlo esplicitamente a vuoto).

4. Matrice delle capacità: quale scegliere

Requisito JSON Patch Merge Patch
Sostituire con precisione un elemento specifico di un array ✓ ✗
Eliminare un elemento da un array ✓ ✗
Aggiornare parzialmente diversi campi ✓ (più replace) ✓ (il più conciso)
Scrivere un valore null ✓ ✗
Controllo di concorrenza (asserzione di versione) ✓ (test) ✗ (solo via If-Match)
Leggibilità della patch Media Buona (sembra i dati stessi)
Complessità implementativa Alta Bassa
Forme di risorse adatte Profondamente annidate, simili a liste Configurazione piatta, form

Regole pratiche:

  • Form, pagine di impostazioni, risorse di configurazione → Merge Patch; il frontend invia solo i campi modificati;
  • Aggiunte e rimozioni di liste, transizioni di macchine a stati, qualsiasi cosa che necessiti protezione dalla concorrenza → JSON Patch;
  • Entrambi possono coesistere: endpoint diversi dichiarano Content-Type diversi, e il server smista per tipo.

5. Note implementative

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

Quattro punti facilmente trascurati:

  1. Non modificare sul posto oggetti condivisi; una patch fallita lascia uno stato mezzo modificato (a meno che l'implementazione garantisca il rollback);
  2. Validazione dei percorsi: le patch dall'esterno devono validare ogni path/from e rifiutare chiavi come __proto__ e constructor;
  3. Limiti di dimensione: limita il numero di voci della patch per evitare che una singola patch esegua centinaia di migliaia di operazioni;
  4. Audit: le patch sono naturalmente adatte ai log di audit — sono una descrizione completa di "cosa è cambiato".

6. Semantica degli errori

Situazione Codice di stato suggerito Spiegazione
La patch non è un array / è strutturalmente invalida 400 Bad Request Il formato del corpo della richiesta è sbagliato
Il nodo referenziato da path non esiste 422 Unprocessable Entity Sintatticamente valido ma semanticamente non eseguibile
Un'operazione test è fallita 409 o 412 Indica un conflitto di versione
Nessun permesso per modificare quel percorso 403 Forbidden Non mascherarlo con 404 a meno che tu non voglia deliberatamente nascondere l'esistenza della risorsa

Distinguere 400 da 422 conta: il primo significa "non riesco a capire la tua patch", il secondo "la capisco, ma questi dati non permettono quel cambiamento".

7. Lista di controllo

  • [ ] L'endpoint dichiara il Content-Type corretto (json-patch+json / merge-patch+json);
  • [ ] L'applicazione della patch è atomica, e un fallimento non lascia risultati parziali;
  • [ ] Le patch esterne vengono validate contro una whitelist di percorsi e un limite di numero di voci;
  • [ ] Il controllo di concorrenza usa test o If-Match;
  • [ ] Le patch vengono scritte nel log di audit;
  • [ ] Gli scenari di array scelgono esplicitamente JSON Patch; non contare su Merge Patch.

FAQ

Come decido tra JSON Patch e Merge Patch?

Usa JSON Patch quando devi sostituire o rimuovere un elemento specifico di un array, scrivere un valore null o asserire una versione; usa Merge Patch per form e risorse di configurazione che aggiornano solo pochi campi. Merge Patch non può eliminare un singolo elemento di un array, e non può scrivere null, perché null significa già eliminazione.

Perché add all'indice di array n non è una sovrascrittura?

Perché add inserisce in un array: l'elemento all'indice n e tutto ciò che segue si sposta indietro. Per sostituire un elemento esistente devi usare replace, e il percorso deve esistere prima. Un indice fuori intervallo (n > length) dovrebbe sollevare un errore; le implementazioni indulgenti fanno comportare la stessa patch diversamente tra server.

Devo modificare l'oggetto originale sul posto?

No. Una patch fallita lascia uno stato mezzo modificato a meno che l'implementazione garantisca il rollback. Applica la patch a una copia deep-copied o structured-cloned, poi scambia il riferimento dopo il successo.