DevKitLab Logo DevKitLab
JSON / diferencias / flujo de trabajo

Cómo comparar dos archivos JSON y ver solo los cambios reales

Un diff de texto confunde formato y orden de claves con cambios de datos. Aprende a comparar JSON desde la terminal, el editor, el código y el navegador.

Comparación estructurada de dos documentos JSON con el campo de correo modificado resaltado

Cambiaste un endpoint y solo quieres demostrar que hizo exactamente lo previsto: aquí se añadió un campo, allí cambió un valor y nada más. Tomas la respuesta anterior y la nueva, las pones en un diff, y de pronto aparecen decenas de líneas rojas y verdes para una modificación de dos valores.

Con frecuencia el código no tiene ningún problema. El diff está respondiendo a la pregunta equivocada. La mayoría de los diffs comparan texto, y un JSON con el mismo significado puede escribirse de muchas formas: claves en otro orden, dos o cuatro espacios de sangría, un payload comprimido frente a otro formateado. Lo que necesitas comparar son los datos. Este artículo explica cómo hacerlo en la terminal, en el editor, con código y en el navegador, además de cuándo conviene cada enfoque.

El ejemplo que usaremos

Son dos versiones de una respuesta de /users. La versión nueva añade un usuario al principio del array y corrige el dominio del correo de otra usuaria. Ese es todo el cambio.

// before.json
{
  "users": [
    { "id": 1, "name": "Ada", "email": "ada@example.com", "role": "admin" },
    { "id": 2, "name": "Grace", "email": "grace@example.com", "role": "user" }
  ]
}
// after.json — se añade el usuario 3 y se corrige el dominio de Grace
{
  "users": [
    { "id": 3, "name": "Linus", "email": "linus@example.com", "role": "user" },
    { "id": 1, "name": "Ada", "email": "ada@example.com", "role": "admin" },
    { "id": 2, "name": "Grace", "email": "grace@example.org", "role": "user" }
  ]
}

Primero, un matiz importante

Decir que «un diff de texto no sirve para JSON» no es del todo correcto. Los algoritmos modernos de diff por líneas detectan bien inserciones y eliminaciones. Si pasas nuestros dos archivos a git diff, reconoce con claridad la línea añadida y la línea modificada:

@@ -1,6 +1,7 @@
 {
   "users": [
+    { "id": 3, "name": "Linus", "email": "linus@example.com", "role": "user" },
     { "id": 1, "name": "Ada", "email": "ada@example.com", "role": "admin" },
-    { "id": 2, "name": "Grace", "email": "grace@example.com", "role": "user" }
+    { "id": 2, "name": "Grace", "email": "grace@example.org", "role": "user" }
   ]
 }

La inserción queda limpia y el cambio es correcto. El problema aparece cuando el texto cambia aunque los datos no hayan cambiado.

Dónde un diff de texto genera ruido

Hay tres situaciones muy habituales en JSON real.

Formato. Un lado está minificado y el otro tiene sangría. Los datos son idénticos, pero el diff muestra una reescritura completa:

@@ -1 +1,6 @@
-{"users":[{"id":1,"name":"Ada"},{"id":2,"name":"Grace"}]}
+{
+  "users": [
+    { "id": 1, "name": "Ada" },
+    { "id": 2, "name": "Grace" }
+  ]
+}

Orden de las claves. Los objetos JSON no tienen un orden semántico de claves, pero un diff textual no lo sabe. Dos servicios, o bibliotecas de JSON de lenguajes distintos, pueden serializar el mismo registro en órdenes diferentes:

@@ -1,5 +1,5 @@
 {
+  "email": "ada@example.com",
   "name": "Ada",
-  "role": "admin",
-  "email": "ada@example.com"
+  "role": "admin"
 }

Ada no cambió. El orden del array provoca un efecto parecido: si los mismos elementos se reordenan, un diff por líneas puede presentar toda la cola como eliminada y añadida de nuevo. Un formateador al guardar, un serializador que ordena claves o dos entornos con distinto orden de salida bastan para ocultar el único cambio relevante.

Normalizar primero en la terminal

La solución más barata es normalizar ambos lados antes de compararlos. jq -S analiza el JSON y lo vuelve a emitir con las claves ordenadas y un formato uniforme; así desaparecen las diferencias de espacios y de orden de claves.

diff <(jq -S . before.json) <(jq -S . after.json)

En los dos casos anteriores donde los datos eran iguales, no habrá salida porque los documentos normalizados son idénticos. En nuestro cambio real solo quedarán el usuario añadido y el correo editado. Si prefieres color y una salida conocida, git diff --no-index before.json after.json ejecuta el motor de Git incluso para archivos que no están bajo seguimiento.

jq -S tiene un límite importante: ordena claves, no elementos de arrays. Si el cambio es un reordenamiento del array, necesitas una comparación estructural.

Comparar desde el editor

Si ambos archivos ya están abiertos, no siempre hace falta la terminal. Los editores modernos suelen permitir seleccionar dos archivos y compararlos con desplazamiento sincronizado y resaltado dentro de la línea. Sigue siendo un diff de texto, con las mismas limitaciones de formato y orden, pero para dos archivos de aspecto similar es el método más rápido que ya tienes a mano.

Comparar dentro de código

Cuando la comprobación debe ejecutarse en un test o script, compara estructuras ya analizadas. En Python, deepdiff puede ignorar el orden de arrays:

from deepdiff import DeepDiff
import json

before = json.load(open("before.json"))
after = json.load(open("after.json"))

print(DeepDiff(before, after, ignore_order=True))

Antes de confiar en el resultado hay que entender una salvedad. Con ignore_order=True, los elementos se emparejan inicialmente por su contenido. Linus se identifica sin problema como nuevo, pero el objeto de Grace sí cambió. Si solo se compara contenido, una herramienta puede tratarlo como un elemento eliminado y otro añadido, en vez de como «se modificó el correo».

La solución es ofrecer una identidad estable: primero decidir si dos objetos representan la misma entidad, y después comparar sus campos.

from deepdiff.helper import CannotCompare

def same_user(left, right, level=None):
    try:
        return left["id"] == right["id"]
    except (TypeError, KeyError):
        raise CannotCompare() from None

diff = DeepDiff(before, after, ignore_order=True, iterable_compare_func=same_user)

Ahora los objetos con el mismo id se emparejan antes de comparar sus campos: el correo de Grace es un cambio de valor y Linus es una adición. DeepDiff usa su propia notación root[...]; si necesitas una dirección como $.users[2].email, utiliza una biblioteca o herramienta que exponga JSONPath. En un test normalmente no imprimes la diferencia: verificas que no exista.

assert DeepDiff(expected, actual, ignore_order=True) == {}

El principio es el mismo en cualquier lenguaje: analizar ambos documentos, recorrer los árboles y devolver cambios tipados. Emparejar elementos por identidad y no por posición convierte «cambió todo el array» en «cambió este elemento».

Los cambios tienen dirección: JSONPath

La comparación estructural aporta algo que un diff por líneas no ofrece: una dirección dentro de los datos. Cada biblioteca puede tener una notación distinta; una herramienta interactiva puede mostrar el mismo lugar como JSONPath, por ejemplo $.users[2].email. Al revisar una configuración muy anidada o un payload grande, «cambió la línea 214» ayuda poco; «$.users[2].email pasó de X a Y» se puede citar en una revisión, ticket o prueba. Los números de línea describen el archivo; las rutas describen los datos.

Para una revisión visual rápida

La comparación estructural en código resuelve el problema del orden, pero para una tarea cotidiana —pegar dos payloads y revisar el cambio— implica demasiado montaje. Tendrías que definir una identidad cada vez, y una salida de consola no resalta qué parte de una cadena larga cambió.

Pega ambos lados en JSON Diff. La herramienta busca campos estables en objetos de arrays, como id, key, name, uuid o _id; al insertar Linus, los elementos posteriores no se desplazan. Ada y Grace permanecen alineadas, el usuario 3 aparece como añadido y la fila de Grace muestra el cambio, incluido el fragmento com → org. A su lado hay una lista de cambios por JSONPath:

La herramienta JSON Diff: el lado modificado muestra el usuario añadido y el fragmento cambiado del correo de Grace; un panel lateral enumera los dos cambios por ruta JSON
Arrays alineados por id, resaltado dentro de la cadena y lista de cambios por JSONPath.

Si el array contiene escalares o sus objetos no tienen una clave estable, la herramienta vuelve al emparejamiento posicional. También puedes ignorar el orden de claves en la comparación semántica y contraer zonas sin cambios para mantener una revisión de cinco campos en una configuración grande. Todo se procesa en el navegador: es útil con respuestas de producción o payloads que no quieres pegar en un servicio remoto. No sustituye a jq ni a una biblioteca estructural dentro de un pipeline; es la opción sin preparación para revisar con cuidado.

Casos de uso habituales

  • Revisar cambios de API. Pega la respuesta anterior y actual de un endpoint para confirmar que solo cambiaron los campos esperados, incluso dentro de objetos anidados y cadenas largas.
  • Editar configuraciones. tsconfig.json, package.json, reglas de lint o flags. El orden de claves suele ser ruido; la comparación semántica deja visible el cambio real.
  • Fixtures y datos semilla. Compara una fixture antes y después para probar que el cambio solo tocó los casos previstos.

Cuando el texto aún no es JSON válido

Todo lo anterior presupone que ambos lados se pueden analizar. Durante la edición puede haber una coma final, una llave sin cerrar o un payload pegado a medias; el parser se detiene en el primer error y no existe comparación estructural posible. Primero localiza el problema con jq . file.json o python -m json.tool file.json, que indican línea y columna. Si las dos versiones incompletas son parecidas y quieres ver dónde divergen, usa Text Compare para comparar texto bruto; corrige la sintaxis y vuelve a la comparación estructural.

Qué usar en cada situación

SituaciónHerramienta adecuada
Solo cambió el formato o el orden de clavesjq -S + diff en la terminal
Se reordenaron elementos de un arrayComparación estructural o una herramienta que entienda JSON
Los archivos ya están abiertosComparación integrada del editor
El diff vive en un test o scriptdeepdiff con identidad, u otra biblioteca estructural
Revisión visual y cambios en cadenas largasJSON Diff en el navegador
La entrada aún no se analizajq . / python -m json.tool, luego Text Compare

El error no es usar un diff. Es entregar datos a una comparación que solo entiende texto, ver todo el archivo en rojo y concluir que hubo una modificación enorme cuando las claves solo cambiaron de orden. Compara datos como datos, empareja arrays por identidad y describe cambios por ruta: así solo verás lo que realmente cambiaste.