JSON Toolkit

JSON Pointer, Patch et Merge Patch

L’essentiel : Trois courtes spécifications forment un protocole de modification : Pointer localise, Patch édite précisément, Merge Patch fusionne partiellement. Un mauvais choix nuit aux tableaux et à la concurrence.

Intermédiaire~12 min de lecturePublié le 2026-10-30Mis à jour le 2026-10-30
Illustration de l'article JSON Pointer, Patch et Merge Patch

L'essentiel : utilisez JSON Pointer pour localiser par position et JSONPath pour filtrer ou récurser ; confiez l'insertion/retrait dans un tableau, les écritures de null et les assertions de version à JSON Patch, et réservez Merge Patch aux configurations plates et aux ressources de type formulaire.

Localiser et modifier sont deux choses différentes, et les spécifications les répartissent en trois documents. Le prix habituel de leur confusion est « tout le tableau a été remplacé » sans que personne ne le remarque.

1. JSON Pointer : la syntaxe de localisation la plus courte

La RFC 6901 fait exactement une chose : pointer un nœud d'un document avec une chaîne.

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

Trois règles :

  1. La chaîne vide pointe le nœud racine lui-même, pas un « nœud vide » ;
  2. Chaque / sépare un jeton de référence ; les tableaux utilisent des indices décimaux (les zéros initiaux ne sont pas autorisés, 01 est donc invalide) ;
  3. Dans un jeton, ~ s'écrit ~0 et / s'écrit ~1 — remplacez ~ avant /, sinon vous finissez par doubler l'échappement.
/**
 * 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('');
}

La valeur se montre le mieux lorsque la clé elle-même contient / : le pointeur de {"a/b": 1} est /a~1b, pas /a/b (ce dernier signifie b sous a).

Utilisé dans une URL, l'encodage en pourcentage est également nécessaire, car # et ~ ont des significations particulières dans un fragment.

2. JSON Patch : une séquence atomique de six opérations

La RFC 6902 décrit les modifications avec un tableau d'opérations, de type de média 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" }
]
Opération Sémantique Frontière
add Ajoute un membre ou insère un élément de tableau L'index de tableau peut égaler la longueur (ajout en fin) ; - signifie la fin
remove Supprime Le chemin doit exister
replace Remplace une valeur existante Le chemin doit exister (équivalent à remove + add, mais exige que le chemin existe d'abord)
move Déplace Vous ne pouvez pas déplacer un nœud sous son propre sous-chemin
copy Copie La source doit exister
test Affirme une égalité En cas d'échec, tout le patch n'est pas appliqué ; utilisé pour le verrouillage optimiste

La propriété clé est l'atomicité : si une opération échoue, tout le patch est annulé, il n'y a donc pas d'état intermédiaire où « les trois premières ont été appliquées et la quatrième a échoué ». Cela en fait un excellent corps de requête PATCH.

2.1 Utiliser test pour la concurrence optimiste

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

Le client fait d'abord un GET de l'etag et l'écrit dans l'opération test. En cas d'écritures concurrentes, le test de la seconde requête échoue et le serveur renvoie 409 Conflict ou 412 Precondition Failed. C'est bien plus sûr que le dernier-écrit-gagne et cela ne nécessite aucun verrou distribué.

2.2 Les opérations sur les tableaux sont les plus sujettes aux erreurs

// 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 à l'index de tableau n est une insertion, pas un écrasement ; un index hors limites (n > length) doit lever une erreur. Certaines implémentations se montrent laxistes sur ce point, ce qui fait que le même patch se comporte différemment selon les serveurs.

3. Merge Patch : un fragment qui ressemble à la cible

La RFC 7386 ne compte que quatre règles :

  1. La cible est un objet et le patch est un objet → fusion récursive ;
  2. La valeur d'une clé dans le patch est null → supprimer cette clé de la cible ;
  3. La valeur d'une clé dans le patch n'est pas un objet → remplacer directement ;
  4. Le patch lui-même n'est pas un objet → toute la cible est remplacée.
// Target
{ "a": "b", "c": { "d": "e", "f": "g" } }

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

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

Le type de média est application/merge-patch+json. Il se lit comme une « mise à jour partielle », ce qui le rend particulièrement facile à mal utiliser :

  • Les tableaux ne peuvent être remplacés qu'en bloc. Supprimer list[2] ? Merge Patch ne sait pas le faire ;
  • Vous ne pouvez pas écrire null. null est déjà pris pour signifier la suppression ;
  • Il ne peut pas exprimer un troisième état entre « laisser ce champ tranquille » et « supprimer ce champ » (par exemple, le mettre explicitement à vide).

4. Matrice de capacités : lequel choisir

Besoin JSON Patch Merge Patch
Remplacer précisément un élément de tableau donné ✓ ✗
Supprimer un élément d'un tableau ✓ ✗
Mettre à jour partiellement plusieurs champs ✓ (plusieurs replace) ✓ (le plus concis)
Écrire une valeur null ✓ ✗
Contrôle de concurrence (assertion de version) ✓ (test) ✗ (seulement via If-Match)
Lisibilité du patch Moyenne Bonne (ressemble aux données mêmes)
Complexité d'implémentation Élevée Faible
Formes de ressources adaptées Profondément imbriquées, de type liste Configuration plate, formulaires

Règles empiriques :

  • Formulaires, pages de paramètres, ressources de configuration → Merge Patch ; le frontend ne soumet que les champs modifiés ;
  • Ajouts et retraits dans des listes, transitions d'état, tout ce qui nécessite une protection de concurrence → JSON Patch ;
  • Les deux peuvent coexister : différents points de terminaison déclarent différents Content-Type, et le serveur dispatche selon le type.

5. Notes d'implémentation

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

Quatre points facilement négligés :

  1. Ne mutez pas des objets partagés sur place ; un patch échoué laisse un état à moitié modifié (sauf si l'implémentation garantit l'annulation) ;
  2. Validation des chemins : les patchs venus de l'extérieur doivent valider chaque path/from et rejeter des clés comme __proto__ et constructor ;
  3. Limites de taille : plafonnez le nombre d'entrées du patch pour empêcher qu'un seul patch exécute des centaines de milliers d'opérations ;
  4. Audit : les patchs se prêtent naturellement aux journaux d'audit — ils sont une description complète de « ce qui a changé ».

6. Sémantique des erreurs

Situation Code de statut suggéré Explication
Le patch n'est pas un tableau / est structurellement invalide 400 Bad Request Le format du corps de requête est faux
Le nœud référencé par path n'existe pas 422 Unprocessable Entity Syntaxiquement valide mais sémantiquement inexécutable
Une opération test a échoué 409 ou 412 Indique un conflit de version
Pas de permission pour modifier ce chemin 403 Forbidden Ne le masquez pas avec 404, sauf si vous voulez délibérément cacher l'existence de la ressource

Distinguer 400 de 422 compte : le premier signifie « je ne comprends pas votre patch », le second « je le comprends, mais ces données n'autorisent pas cette modification ».

7. Liste de vérification

  • [ ] Le point de terminaison déclare le bon Content-Type (json-patch+json / merge-patch+json) ;
  • [ ] L'application du patch est atomique, et un échec ne laisse aucun résultat partiel ;
  • [ ] Les patchs externes sont validés contre une liste blanche de chemins et une limite d'entrées ;
  • [ ] Le contrôle de concurrence utilise test ou If-Match ;
  • [ ] Les patchs sont écrits dans le journal d'audit ;
  • [ ] Les scénarios de tableaux choisissent explicitement JSON Patch ; ne comptez pas sur Merge Patch.

FAQ

Comment décider entre JSON Patch et Merge Patch ?

Utilisez JSON Patch lorsque vous devez remplacer ou supprimer un élément de tableau précis, écrire une valeur null ou affirmer une version ; utilisez Merge Patch pour les formulaires et ressources de configuration qui ne mettent à jour que quelques champs. Merge Patch ne peut pas supprimer un seul élément de tableau, et il ne peut pas écrire null, car null signifie déjà la suppression.

Pourquoi add à l'index de tableau n n'est-il pas un écrasement ?

Parce que add insère dans un tableau : l'élément à l'index n et tout ce qui suit se décalent vers l'arrière. Pour remplacer un élément existant, vous devez utiliser replace, et le chemin doit d'abord exister. Un index hors limites (n > length) doit lever une erreur ; les implémentations laxistes font que le même patch se comporte différemment selon les serveurs.

Faut-il modifier l'objet original sur place ?

Non. Un patch échoué laisse un état à moitié modifié, sauf si l'implémentation garantit l'annulation. Appliquez le patch à une copie profonde ou clonée structurellement, puis échangez la référence après succès.