JSON Toolkit

JSON Pointer, Patch y Merge Patch

En resumen: Tres especificaciones breves forman un protocolo de cambio: Pointer localiza, Patch edita con precisión y Merge Patch fusiona parcialmente. Elegir mal duele en arrays y concurrencia.

Intermedio~12 min de lecturaPublicado el 2026-10-30Actualizado el 2026-10-30
Portada de JSON Pointer, Patch y Merge Patch

En resumen: usa JSON Pointer para localizar por posición y JSONPath para filtrar o recorrer de forma recursiva; deja la inserción y eliminación en arrays, las escrituras de null y las comprobaciones de versión a JSON Patch, y reserva Merge Patch para configuraciones planas y recursos de formulario.

Localizar y modificar son dos cosas distintas, y las especificaciones las dividen en tres documentos. El precio habitual de confundirlas es "el array entero acabó reemplazado" y nadie se da cuenta.

1. JSON Pointer: la sintaxis de localización más breve

RFC 6901 hace exactamente una cosa: apuntar a un nodo de un documento con una cadena.

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

Tres reglas:

  1. La cadena vacía apunta a la raíz misma del documento, no a un "nodo vacío";
  2. Cada / separa un token de referencia; los arrays usan índices decimales (no se permiten ceros a la izquierda, así que 01 no es válido);
  3. Dentro de un token, ~ se escribe como ~0 y / como ~1 — reemplaza ~ antes que /, de lo contrario acabas aplicando doble escape.
/**
 * 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('');
}

Su valor se aprecia mejor cuando la propia clave contiene /: el pointer de {"a/b": 1} es /a~1b, no /a/b (este último significa b dentro de a).

Cuando se usa dentro de una URL, también hace falta codificación por ciento, porque # y ~ tienen significados especiales en un fragmento.

2. JSON Patch: una secuencia atómica de seis operaciones

RFC 6902 describe cambios con un array de operaciones, tipo de medio 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" }
]
Operación Semántica Límite
add Añade un miembro o inserta un elemento de array El índice de array puede ser igual a la longitud (añadir al final); - significa el final
remove Elimina La ruta debe existir
replace Reemplaza un valor existente La ruta debe existir (equivale a remove + add, pero exige que la ruta exista primero)
move Mueve No se puede mover un nodo bajo su propia subruta
copy Copia El origen debe existir
test Comprueba igualdad Si falla, el parche completo no se aplica; se usa para bloqueo optimista

La propiedad clave es la atomicidad: si cualquier operación falla, el parche completo se revierte, así que no existe un estado intermedio en el que "las tres primeras se aplicaron y la cuarta falló". Esto lo convierte en un cuerpo de petición PATCH excelente.

2.1 Usar test para concurrencia optimista

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

El cliente primero hace GET del etag y lo escribe en la operación test. En escrituras concurrentes, el test de la segunda petición falla y el servidor devuelve 409 Conflict o 412 Precondition Failed. Esto es mucho más seguro que "gana la última escritura" y no requiere un bloqueo distribuido.

2.2 Las operaciones sobre arrays son las más propensas a errores

// 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 en el índice de array n es una inserción, no una sobrescritura; un índice fuera de rango (n > length) debe lanzar un error. Algunas implementaciones son laxas con esto, lo que hace que el mismo parche se comporte de forma distinta según el servidor.

3. Merge Patch: un fragmento que se parece al objetivo

RFC 7386 tiene solo cuatro reglas:

  1. El objetivo es un objeto y el parche es un objeto → fusionar de forma recursiva;
  2. El valor de una clave en el parche es null → borrar esa clave del objetivo;
  3. El valor de una clave en el parche no es un objeto → reemplazar directamente;
  4. El parche en sí no es un objeto → se reemplaza todo el objetivo.
// Target
{ "a": "b", "c": { "d": "e", "f": "g" } }

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

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

El tipo de medio es application/merge-patch+json. Se lee como una "actualización parcial", lo que lo hace especialmente fácil de usar mal:

  • Los arrays solo se pueden reemplazar por completo. ¿Quieres borrar list[2]? Merge Patch no puede;
  • No se puede escribir null. null ya está reservado para significar eliminación;
  • No puede expresar un tercer estado entre "dejar este campo intacto" y "borrar este campo" (por ejemplo, establecerlo explícitamente como vacío).

4. Matriz de capacidades: cuál elegir

Necesidad JSON Patch Merge Patch
Reemplazar con precisión un elemento concreto de un array ✓ ✗
Borrar un elemento de un array ✓ ✗
Actualizar parcialmente varios campos ✓ (varios replace) ✓ (lo más conciso)
Escribir un valor null ✓ ✗
Control de concurrencia (comprobación de versión) ✓ (test) ✗ (solo mediante If-Match)
Legibilidad del parche Moderada Buena (parece los propios datos)
Complejidad de implementación Alta Baja
Formas de recurso adecuadas Muy anidadas, tipo lista Configuración plana, formularios

Reglas prácticas:

  • Formularios, páginas de ajustes, recursos de configuración → Merge Patch; el frontend envía solo los campos modificados;
  • Altas y bajas en listas, transiciones de máquina de estados, cualquier cosa que necesite protección de concurrencia → JSON Patch;
  • Ambos pueden convivir: distintos endpoints declaran distintos Content-Type, y el servidor despacha según el tipo.

5. Notas de implementación

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

Cuatro puntos que se pasan por alto con facilidad:

  1. No modifiques objetos compartidos en su lugar; un parche fallido deja un estado a medio modificar (salvo que la implementación garantice rollback);
  2. Validación de rutas: los parches que vienen de fuera deben validar cada path/from y rechazar claves como __proto__ y constructor;
  3. Límites de tamaño: limita el número de entradas del parche para evitar que un solo parche realice cientos de miles de operaciones;
  4. Auditoría: los parches encajan de forma natural con los logs de auditoría — son una descripción completa de "qué cambió".

6. Semántica de errores

Situación Código de estado sugerido Explicación
El parche no es un array o no es estructuralmente válido 400 Bad Request El formato del cuerpo de la petición es incorrecto
El nodo al que apunta path no existe 422 Unprocessable Entity Sintácticamente válido pero semánticamente no ejecutable
Una operación test ha fallado 409 o 412 Indica un conflicto de versión
Sin permiso para modificar esa ruta 403 Forbidden No lo ocultes con 404 salvo que quieras esconder deliberadamente la existencia del recurso

Distinguir 400 de 422 importa: el primero significa "no entiendo tu parche", el segundo significa "lo entiendo, pero estos datos no permiten ese cambio".

7. Lista de comprobación

  • [ ] El endpoint declara el Content-Type correcto (json-patch+json / merge-patch+json);
  • [ ] La aplicación del parche es atómica y un fallo no deja ningún resultado parcial;
  • [ ] Los parches externos se validan contra una lista blanca de rutas y un límite de entradas;
  • [ ] El control de concurrencia usa test o If-Match;
  • [ ] Los parches se registran en el log de auditoría;
  • [ ] Los escenarios con arrays eligen explícitamente JSON Patch; no cuentes con Merge Patch.

Preguntas frecuentes

¿Cómo decido entre JSON Patch y Merge Patch?

Usa JSON Patch cuando necesites reemplazar o eliminar un elemento concreto de un array, escribir un valor null o comprobar una versión; usa Merge Patch para formularios y recursos de configuración que solo actualizan unos pocos campos. Merge Patch no puede borrar un elemento individual de un array, ni puede escribir null, porque null ya significa eliminación.

¿Por qué add en el índice de array n no es una sobrescritura?

Porque add inserta en un array: el elemento en el índice n y todos los siguientes se desplazan hacia atrás. Para reemplazar un elemento existente debes usar replace, y la ruta tiene que existir primero. Un índice fuera de rango (n > length) debería lanzar un error; las implementaciones laxas hacen que el mismo parche se comporte de forma distinta según el servidor.

¿Debería modificar el objeto original en su lugar?

No. Un parche fallido deja un estado a medio modificar salvo que la implementación garantice rollback. Aplica el parche sobre una copia profunda o clonada estructuralmente, y luego intercambia la referencia tras el éxito.