JSON Toolkit

JSON Patch en producción: atomicidad, idempotencia y conflictos concurrentes

En resumen: JSON Patch es solo un formato de cambio; la atomicidad, la idempotencia y la concurrencia siguen siendo tu responsabilidad. Cubre precondiciones con test, escape de rutas, rollback y coordinación con ETag.

Avanzado~10 min de lecturaPublicado el 2026-10-31Actualizado el 2026-10-31
Portada de JSON Patch en producción

En resumen: valida primero las rutas objetivo y los permisos del parche, luego añade una comprobación de concurrencia con test o If-Match; aplica el parche sobre una copia para que un fallo no deje nada a medio escribir.

El peligro real de una petición PATCH no es escribir el campo equivocado. Es creer que la escritura tuvo éxito mientras se sobrescribe en silencio el cambio de otra persona. JSON Patch te da las herramientas, pero el diseño de seguridad sigue siendo tuyo.

1. Repaso rápido del conjunto de operaciones de RFC 6902

JSON Patch describe cambios como un array de operaciones, con el tipo de medio application/json-patch+json. Seis operaciones se ejecutan en el orden del array:

Operación Efecto Restricción clave
add Añadir un miembro o insertar un elemento de array Un índice de array puede ser igual a la longitud; - significa el final
remove Eliminar un miembro o elemento La ruta objetivo debe existir
replace Reemplazar un valor existente La ruta objetivo debe existir
move Mover un nodo No se puede mover un nodo a una ruta hija de sí mismo
copy Copiar un nodo La ruta de origen debe existir
test Comprobar igualdad de valores Si falla, el parche completo no se aplica

Mucha gente solo recuerda las tres primeras y olvida que test es lo que convierte una escritura a ciegas en una escritura condicional. La semántica completa de las operaciones y los límites de los arrays se tratan en JSON Pointer, Patch y Merge Patch.

2. Usar test para convertir un parche en una actualización condicional

Un parche típico de bloqueo optimista no empieza con una escritura, sino con una comprobación de versión:

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

Inclúyelo en un parche completo:

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

La lógica es: confirmar que la versión del documento es actualmente 1 y luego cambiarla a 2. Si otra petición ya ha avanzado la versión a 2, el primer test falla, el parche completo no se aplica y el servidor devuelve un conflicto. Esto evita actualizaciones perdidas sin introducir un bloqueo distribuido.

Algunos puntos de diseño:

  • Coloca test antes de cada escritura. Si una escritura anterior ya se aplicó, la comprobación no tiene sentido;
  • Comprueba un número de versión o un digest generado por el servidor, no un campo normal que el cliente pueda cambiar libremente;
  • Incrementa la versión en cada escritura correcta; de lo contrario, dos peticiones que usen la misma versión antigua pasarán ambas;
  • El value de test debe tener exactamente el mismo tipo JSON que el valor del documento. 1 y "1" no son iguales.

3. Escape de rutas y contaminación de prototipos

Las reglas de escape de JSON Pointer son simples: escribe ~ como ~0 y / como ~1, y reemplaza ~ antes que /, de lo contrario obtienes un doble escape.

Key "a/b"    → /a~1b
Key "m~n"    → /m~0n
Key "a~/b"   → /a~0~1b

El riesgo con parches externos normalmente no es el escape, sino las claves peligrosas. En JavaScript, claves como __proto__, constructor y prototype pueden desencadenar contaminación de prototipos, incluso mediante un add normal:

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

Usa una lista de permitidos en lugar de una lista de bloqueados:

  1. Permite que los parches modifiquen solo campos de negocio, como /status o /items/0/quantity;
  2. Valida cada path y from contra una expresión regular o un prefijo de ruta, rechazando niveles inesperados distintos de los índices de array;
  3. Procesa el JSON en contenedores Object.create(null) o Map para reducir el impacto de una cadena de prototipos contaminada;
  4. Limita el número de operaciones y el tamaño en bytes de cada value.

4. Aplicación atómica y rollback

RFC 6902 exige ejecución atómica, pero la atomicidad es un requisito semántico, no un valor por defecto en todas las bibliotecas. Bibliotecas como fast-json-patch modifican el documento en su lugar cuando mutateDocument es true, y un fallo puede dejar cambios parciales.

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

/**
 * Apply a patch immutably and return the original document on failure.
 * @param document The original document, which callers must not mutate
 * @param patch The JSON Patch operation array to apply
 * @returns The result, including a success flag and the new document
 */
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 };
  }
}

Tres recomendaciones de implementación:

  • Copia en profundidad o structured-clone primero, aplica sobre la copia e intercambia la referencia solo tras el éxito;
  • No dependas del rollback automático de una biblioteca; el comportamiento ante excepciones varía entre implementaciones;
  • Valida antes de ejecutar: validate solo comprueba la estructura, mientras que los conflictos semánticos siguen requiriendo test; ninguno sustituye al otro.

Si un parche abarca varios recursos, como cambiar un pedido y su inventario a la vez, la atomicidad de JSON Patch solo se cumple dentro de un único documento. Entre documentos necesitas una transacción de base de datos o acciones compensatorias.

5. Idempotencia: PATCH no es idempotente por naturaleza

La semántica HTTP define PUT y DELETE como idempotentes, pero PATCH no tiene por qué serlo. Repetir el mismo JSON Patch puede producir resultados completamente distintos:

  • add /items/- añade al final del array cada vez, así que los reintentos crean duplicados;
  • replace /version cambia la versión 1 a 2 y vuelve a tener éxito en un reintento, aunque la operación ya se haya aplicado;
  • remove /temp falla en la segunda ejecución porque la ruta ya no existe.

Hay tres formas habituales de lograr idempotencia:

Enfoque Cómo funciona Ideal para
Precondición de versión La primera operación es test /version y luego se incrementa Recursos con una versión explícita
Clave de idempotencia El cliente envía Idempotency-Key; el servidor cachea el resultado Pagos, pedidos y otras escrituras
Parche de estado objetivo Usar replace en lugar de add, de modo que el resultado dependa solo del valor final Campos de configuración y estado

El peor patrón es un parche que mezcla add y remove mientras asume que los reintentos son seguros. Si un parche añade o elimina algo, debe ir acompañado de una comprobación de versión o de una clave de idempotencia.

6. Conflictos concurrentes: ETag e If-Match

Además del test dentro del parche, HTTP ofrece un mecanismo estándar: el servidor devuelve un ETag y el cliente envía If-Match en la siguiente petición.

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"}]

Si el recurso ha cambiado, el servidor devuelve 412 Precondition Failed y el cliente vuelve a obtenerlo antes de reintentar. If-Match: * significa "ejecutar siempre que el recurso exista", lo que no es adecuado cuando necesitas evitar sobrescrituras.

La diferencia entre test e If-Match:

  • If-Match se comprueba en la capa HTTP antes de analizar el cuerpo, así que es más barato;
  • test se comprueba en la capa semántica del parche y puede comprobar cualquier campo sin depender del soporte de ETag del servidor;
  • Se pueden combinar, pero no implementes solo uno de ellos y afirmes que el endpoint es seguro bajo concurrencia.

7. Diferencias entre bibliotecas y selección

Implementación Lenguaje Atomicidad Compatibilidad con test Notas
fast-json-patch JavaScript Opcional (devuelve un nuevo documento por defecto) Sí Ecosistema amplio; cuidado con mutateDocument
jsonpatch Python Revierte ante excepciones Sí Cercana al estándar, con errores claros
json-patch Go Opera sobre una copia Sí A menudo se usa junto a JSON Merge Patch
zjsonpatch Java Sí Sí Integración cómoda con Jackson

No elijas una biblioteca solo por "compatibilidad con las seis operaciones". Confirma tres cosas: si un fallo revierte, si test compara de forma estricta y si un add con índice de array fuera de rango lanza un error. Las implementaciones laxas hacen que el mismo parche se comporte de forma distinta según el entorno.

8. Lista de comprobación antes del lanzamiento

  • [ ] El endpoint declara application/json-patch+json;
  • [ ] Los parches se aplican sobre una copia y un fallo nunca contamina el documento original;
  • [ ] Cada path y from de los parches externos pasa una comprobación de lista de permitidos;
  • [ ] Se rechazan claves peligrosas como __proto__, constructor y prototype;
  • [ ] Se limitan el número de operaciones y el tamaño de un valor individual;
  • [ ] test o If-Match proporcionan control de concurrencia;
  • [ ] Los parches se registran en un log de auditoría con actor, marca de tiempo y el array completo de operaciones;
  • [ ] Los límites de transacción son explícitos cuando un parche abarca varios documentos.

Un parche es, por naturaleza, una descripción completa de "qué cambió", lo que lo convierte en un registro de auditoría natural. Para los códigos de error y la idempotencia en la capa de API, continúa con Diseño de API JSON. Si un parche proviene de fusionar dos ramas, también necesitas semántica de conflictos, tratada en Fusión de Git para archivos JSON.

Preguntas frecuentes

¿Qué código de estado debería devolver una operación test fallida?

Si el conflicto proviene de una comprobación de versión, tanto 409 Conflict como 412 Precondition Failed son defendibles. Prefiere 412 para mantener la coherencia con If-Match, e incluye la versión actual en el cuerpo de la respuesta para que el cliente pueda reintentar.

¿Se puede reintentar con seguridad un parche que contiene add y remove?

No directamente. add al final de un array duplica datos, y remove falla en la segunda ejecución porque la ruta ya no existe. Añade una precondición de versión o una clave de idempotencia para que los reintentos se reconozcan como la misma operación.

¿Puede JSON Patch garantizar transacciones entre documentos?

No. La atomicidad de RFC 6902 se aplica a un único documento JSON. Cuando intervienen pedidos, inventario u otros recursos, usa transacciones de base de datos, compensación Saga o una cola de mensajes para la consistencia.

¿Por qué un parche nunca debe escribir en __proto__?

Porque la mayoría de implementaciones de JavaScript almacenan el documento en objetos normales, y escribir __proto__ puede cambiar la cadena de prototipos y afectar a todos los objetos posteriores. Una lista de permitidos de rutas es la defensa más fiable; no confíes solo en el filtrado interno de una biblioteca.

¿Cómo hago idempotente una petición PATCH?

Hay tres enfoques habituales: empezar el parche con test /version e incrementar la versión al tener éxito, hacer que el servidor cachee el resultado para una Idempotency-Key, o usar replace en lugar de add para que el resultado dependa solo del valor final. Si un parche añade o elimina algo, debe ir acompañado de una comprobación de versión o de una clave de idempotencia; no se puede reintentar sin más.

¿Debería usar test e If-Match a la vez?

No tienes que usar ambos, pero implementar solo uno de ellos no hace que un endpoint sea seguro bajo concurrencia. If-Match se comprueba en la capa HTTP antes de analizar el cuerpo, así que es más barato; test se comprueba en la capa semántica del parche y puede comprobar cualquier campo sin depender del soporte de ETag del servidor.