Dev Tools

Cómo aplicar un JSON Patch en JavaScript, Python y Go

Qué significa realmente "aplicar" un JSON Patch

Un JSON Patch (RFC 6902) es un array JSON de operaciones que transforman un documento origen en un documento destino. "Aplicar" el patch significa ejecutar cada operación en orden sobre una copia en memoria del documento. Las reglas son estrictas: las operaciones se ejecutan secuencialmente y, si cualquiera falla, todo el patch se rechaza y el documento original debe quedar intacto. Este comportamiento atómico de todo-o-nada es la parte peor entendida de JSON Patch, y es la razón por la que siempre debes aplicar patches a través de una librería probada en lugar de programar el bucle a mano.

Esta guía muestra la forma exacta, lista para copiar, de aplicar un patch en tres ecosistemas — JavaScript, Python y Go — y luego cubre las tres cosas que más confunden: la operación test, el rollback atómico ante un fallo y la validación de rutas antes de confiar en la entrada del cliente. Si solo quieres ver qué aspecto tiene un patch entre dos documentos, genera uno primero con el comparador JSON y pega el resultado en los ejemplos siguientes.

JavaScript: fast-json-patch

La librería de facto en el mundo Node y navegador es fast-json-patch. Aplica patches, los valida y también puede generar un patch observando las mutaciones de un objeto.

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

const document = { user: { name: 'Ada', roles: ['reader'] } };

const patch = [
  { op: 'add', path: '/user/roles/-', value: 'editor' },
  { op: 'replace', path: '/user/name', value: 'Ada Lovelace' }
];

// validateOperation: true lanza un error si la op es inválida o falla
const result = applyPatch(deepClone(document), patch, true).newDocument;
// result.user.name === 'Ada Lovelace'
// result.user.roles === ['reader', 'editor']

Tres detalles importan aquí. Primero, applyPatch muta el documento que le pasas, así que clónalo (deepClone) si necesitas conservar el original — crítico cuando debes revertir ante un fallo. Segundo, la ruta /user/roles/- es el token JSON Pointer de "final del array": add con - añade al final, mientras que add con un índice numérico inserta antes de ese índice en lugar de sobrescribir. Tercero, pasar true como tercer argumento activa la validación para que una operación incorrecta lance un error en vez de corromper tus datos en silencio.

Python: la librería jsonpatch

En Python, el paquete jsonpatch refleja fielmente el RFC e integra con jsonpointer para resolver las rutas.

import jsonpatch

document = {"user": {"name": "Ada", "roles": ["reader"]}}

patch = jsonpatch.JsonPatch([
    {"op": "add", "path": "/user/roles/-", "value": "editor"},
    {"op": "replace", "path": "/user/name", "value": "Ada Lovelace"},
])

# apply() devuelve un documento NUEVO y nunca muta el original
result = patch.apply(document)
# result == {"user": {"name": "Ada Lovelace", "roles": ["reader", "editor"]}}

A diferencia de fast-json-patch, el método apply() de la librería de Python devuelve un objeto nuevo y deja la entrada intacta por defecto, lo que facilita el comportamiento atómico: si apply() lanza JsonPatchConflict o JsonPointerException, simplemente conservas el documento original. Usa in_place=True solo cuando ya hayas clonado el origen y quieras evitar la reserva de memoria.

Go: evanphx/json-patch

Los servicios en Go suelen usar github.com/evanphx/json-patch/v5, que opera sobre JSON en bruto ([]byte) en lugar de structs decodificados.

import jsonpatch "github.com/evanphx/json-patch/v5"

doc := []byte(`{"user":{"name":"Ada","roles":["reader"]}}`)
raw := []byte(`[
  {"op":"add","path":"/user/roles/-","value":"editor"},
  {"op":"replace","path":"/user/name","value":"Ada Lovelace"}
]`)

patch, err := jsonpatch.DecodePatch(raw)
if err != nil { /* patch malformado */ }

updated, err := patch.Apply(doc)
if err != nil { /* la operación falló — mantén `doc` sin cambios */ }

Como la librería trabaja sobre bytes, los errores aparecen en dos lugares distintos: DecodePatch falla si el patch en sí es JSON malformado, y Apply falla si una operación no se puede satisfacer (por ejemplo, un replace sobre una ruta que no existe). Mantén estos casos separados en tu manejo de errores — un 400 por un patch malformado es una respuesta distinta a un 409 por uno en conflicto.

La operación test: concurrencia optimista

La operación test es lo que convierte a JSON Patch de un escritor ciego en uno seguro. Afirma que el valor en una ruta es igual a un valor esperado y, si no lo es, todo el patch falla antes de escribir cualquier cambio. Esto te da control de concurrencia optimista sin necesidad de una columna de versión separada.

[
  { "op": "test",    "path": "/user/name", "value": "Ada" },
  { "op": "replace", "path": "/user/name", "value": "Ada Lovelace" }
]

Si otra petición ya cambió /user/name, el test falla, el replace nunca se ejecuta y el cliente descubre que el documento cambió bajo sus pies. Coloca las operaciones test al principio del patch para fallar pronto y no aplicarlo nunca parcialmente. Las tres librerías anteriores respetan test como parte del proceso estándar de aplicación.

Fallo atómico: por qué importan el orden y el rollback

RFC 6902 exige que un patch fallido deje el documento exactamente como estaba. Las librerías que devuelven un documento nuevo (Python, Go) te dan esto gratis — descartas el resultado y conservas el original. Las que mutan en el sitio (la vía rápida por defecto de fast-json-patch) no, y por eso el ejemplo de JavaScript clona primero. El antipatrón a evitar es iterar las operaciones tú mismo y aplicarlas una a una sobre el objeto vivo: en cuanto la tercera operación falla, la primera y la segunda ya han corrompido tus datos sin vuelta atrás limpia.

  • Clonar-y-aplicar: trabaja sobre una copia, sustitúyela solo en caso de éxito.
  • Fallar pronto: valida el patch completo (y ejecuta las ops test) antes de escribir nada duradero.
  • Mapea los errores a códigos de estado: patch malformado → 400, test fallido o ruta inexistente → 409, ruta no autorizada → 403.

Valida las rutas antes de confiar en ellas

Un patch que llega de un cliente es entrada no confiable. Un usuario que edita su perfil no debe poder enviar {"op":"replace","path":"/role","value":"admin"}. Mantén una lista blanca de rutas JSON Pointer modificables por tipo de recurso y rechaza cualquier operación cuya path (o from, en move/copy) quede fuera de ella. Esto se trata en profundidad en la guía RFC 6902, y combina de forma natural con la validación de esquemas — pasa el resultado del patch por tu validador de JSON Schema antes de persistirlo para que un patch técnicamente válido nunca produzca un documento estructuralmente inválido.

Depurar un patch que no se aplica

Cuando un patch falla y no sabes por qué, la forma más rápida de diagnosticarlo es comparar directamente el documento origen y el destino. Pega ambos en el comparador JSON para ver exactamente qué campos difieren, y luego confirma que las rutas de tu patch coinciden con esa estructura. Los culpables más habituales son: un replace apuntando a una ruta que aún no existe (usa add en su lugar), un índice de array que asume que add sobrescribe (en realidad inserta), y caracteres / o ~ sin escapar en las claves de objeto, que deben escribirse como ~1 y ~0 respectivamente bajo JSON Pointer (RFC 6901). Para repasar el modelo de documento subyacente, consulta Estructura y sintaxis de JSON.

Un ejemplo completo paso a paso: parchear un perfil de usuario

La teoría se entiende mejor cuando puedes seguirla de principio a fin. Supón que tu API guarda este documento de usuario:

{
  "id": 42,
  "name": "Ada",
  "email": "[email protected]",
  "roles": ["reader"],
  "settings": { "theme": "light", "notifications": true }
}

El cliente quiere renombrar al usuario, ascenderlo a editor, cambiar el tema a oscuro y eliminar por completo el indicador de notificaciones — pero solo si el email no ha cambiado desde la última lectura del registro. Expresado como un único JSON Patch, esa intención tiene este aspecto:

[
  { "op": "test",    "path": "/email",        "value": "[email protected]" },
  { "op": "replace", "path": "/name",         "value": "Ada Lovelace" },
  { "op": "add",     "path": "/roles/-",      "value": "editor" },
  { "op": "replace", "path": "/settings/theme", "value": "dark" },
  { "op": "remove",  "path": "/settings/notifications" }
]

Repasándolo: el test protege toda la operación — si alguien cambió el email mientras tanto, nada más se ejecuta. El replace sobre /name tiene éxito porque la ruta ya existe. El add con el token - añade "editor" al array de roles. El segundo replace entra en el objeto anidado settings. Por último, remove elimina la clave notifications por completo. Ejecuta esto en cualquiera de las tres librerías anteriores y obtendrás un resultado determinista, o un fallo limpio con el documento original preservado. Compáralo con la alternativa — cinco actualizaciones de campo separadas, cada una en su propia petición, cada una una oportunidad de escritura parcial — y el atractivo de un único patch atómico se vuelve evidente.

JSON Patch frente a enviar el objeto completo (PUT)

Una pregunta justa es por qué no hacer simplemente un PUT con el objeto entero actualizado y dejar que el servidor lo sobrescriba. Para recursos pequeños y planos eso suele ser más simple, y no deberías recurrir a JSON Patch por reflejo. Pero tres situaciones hacen que un patch sea claramente mejor. Primero, documentos grandes con cambios pequeños: editar un campo de un registro de 50 KB implica enviar 50 KB con un PUT frente a unos pocos bytes con un patch. Segundo, editores concurrentes: un PUT ciego sobrescribe en silencio cualquier cosa que haya cambiado desde la última lectura del cliente, mientras que un patch con una operación test se niega a pisar un campo desactualizado. Tercero, semántica de actualización parcial: PUT reemplaza el recurso completo, así que cualquier campo que el cliente omita se trata como eliminado — una fuente clásica de pérdida accidental de datos que el patch evita porque solo toca las rutas que nombras. Si tu actualización es "cambia estos tres campos y deja todo lo demás intacto", JSON Patch lo expresa con precisión; PUT no puede.

Resumen

Aplicar un JSON Patch es sencillo una vez que respetas las garantías del estándar: usa una librería real, trabaja sobre una copia, encabeza con operaciones test para la seguridad ante concurrencia, valida las rutas del cliente contra una lista blanca y mapea cada modo de fallo al código HTTP correcto. JavaScript, Python y Go tienen librerías maduras y conformes al RFC, así que nunca deberías programar el bucle de aplicación a mano. Cuando algo no funcione, compara los dos documentos y revisa primero tus rutas — nueve de cada diez veces el patch es correcto y la ruta tiene un carácter de más.

← Volver al Blog