Comparar objetos JSON
Cómo comparar dos objetos JSON y encontrar las diferencias en Python, Java y JavaScript.
Descripción general
Comparar dos objetos JSON suena trivial hasta que lo intentas con datos anidados. {"a": {"b": 1}} y {"a": {"b": 2}} no son iguales, pero == solo te dice que difieren, no dónde ni cómo. Un diff de JSON de verdad recorre ambos árboles e informa de qué claves se añadieron, se eliminaron o cambiaron, a cualquier profundidad. Eso es lo que necesitas para tests de snapshot, detección de desviación de configuración, monitorización de contratos de API y auditorías de migración.
Esta receta muestra tres sabores en Python, JavaScript y Java: un informe categorizado (added / removed / updated), un parche reversible RFC 6902 y un diff recursivo sin dependencias.
Cuándo usarla
Usa esta receta cuando:
- Valides que una respuesta de API REST coincide con un snapshot esperado
- Detectes desviación de configuración entre archivos de entorno (staging frente a producción)
- Audites una migración comparando exportaciones de antes y después
- Construyas un flujo con
applyPatchque reproduzca cambios entre sistemas
Si solo necesitas combinar dos objetos, consulta Merge JSON Files. Comparar y combinar son operaciones emparentadas pero opuestas.
Solución
Python
deepdiff da un informe categorizado sin configuración:
# pip install deepdiff
from deepdiff import DeepDiff
old = {"user": {"name": "Alice", "age": 30}, "roles": ["admin"]}
new = {"user": {"name": "Alice", "age": 31}, "roles": ["admin", "editor"]}
diff = DeepDiff(old, new)
print(diff)
# {'values_changed': {"root['user']['age']": {'old_value': 30, 'new_value': 31}},
# 'iterable_item_added': {"root['roles'][1]": 'editor'}}
Sin dependencias de terceros, un pequeño recorrido recursivo hace el mismo trabajo y es más fácil de personalizar:
def json_diff(old, new, path=""):
"""Devuelve {path: cambio} para dos estructuras tipo JSON."""
changes = {}
if old == new:
return changes
if isinstance(old, dict) and isinstance(new, dict):
for key in old.keys() | new.keys():
if key not in old:
changes[f"{path}/{key}"] = {"added": new[key]}
elif key not in new:
changes[f"{path}/{key}"] = {"removed": old[key]}
else:
changes.update(json_diff(old[key], new[key], f"{path}/{key}"))
elif isinstance(old, list) and isinstance(new, list):
for i, (o, n) in enumerate(zip(old, new)):
changes.update(json_diff(o, n, f"{path}/{i}"))
for i in range(len(old), len(new)):
changes[f"{path}/{i}"] = {"added": new[i]}
for i in range(len(new), len(old)):
changes[f"{path}/{i}"] = {"removed": old[i]}
else:
changes[path or "/"] = {"old": old, "new": new}
return changes
JavaScript
fast-json-patch produce un parche RFC 6902 que puedes reproducir con applyPatch:
// npm install fast-json-patch
import * as jsonpatch from 'fast-json-patch';
const oldDoc = { user: { name: 'Alice', age: 30 }, roles: ['admin'] };
const newDoc = { user: { name: 'Alice', age: 31 }, roles: ['admin', 'editor'] };
const patch = jsonpatch.compare(oldDoc, newDoc);
console.log(patch);
// [
// { op: 'replace', path: '/user/age', value: 31 },
// { op: 'add', path: '/roles/1', value: 'editor' }
// ]
Para un informe sencillo de añadido/cambiado/eliminado, deep-object-diff es más ligero:
// npm install deep-object-diff
import { detailedDiff } from 'deep-object-diff';
console.log(detailedDiff(oldDoc, newDoc));
// {
// added: { roles: { '1': 'editor' } },
// deleted: {},
// updated: { user: { age: 31 } }
// }
Java
zjsonpatch emite el mismo formato RFC 6902 a través de Jackson:
// Maven: com.flipkart.zjsonpatch:zjsonpatch
import com.fasterxml.jackson.databind.JsonNode;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.flipkart.zjsonpatch.JsonDiff;
public class JsonDiffExample {
public static void main(String[] args) throws Exception {
ObjectMapper mapper = new ObjectMapper();
JsonNode oldNode = mapper.readTree("{\"user\":{\"name\":\"Alice\",\"age\":30}}");
JsonNode newNode = mapper.readTree("{\"user\":{\"name\":\"Alice\",\"age\":31}}");
JsonNode patch = JsonDiff.asJson(oldNode, newNode);
System.out.println(patch.toPrettyString());
// [{"op":"replace","path":"/user/age","value":31}]
}
}
Si quieres un informe sencillo sin la dependencia, recorre el árbol Jackson de forma recursiva. Un equals superficial sobre la raíz solo te dice que los árboles difieren:
public static Map<String, Object[]> diff(JsonNode a, JsonNode b,
String path, Map<String, Object[]> out) {
if (a.equals(b)) return out;
if (a.isObject() && b.isObject()) {
a.fields().forEachRemaining(e -> {
JsonNode av = e.getValue();
JsonNode bv = b.get(e.getKey());
if (bv == null) out.put(path + "/" + e.getKey(), new Object[]{av, null});
else diff(av, bv, path + "/" + e.getKey(), out);
});
b.fields().forEachRemaining(e -> {
if (a.get(e.getKey()) == null)
out.put(path + "/" + e.getKey(), new Object[]{null, e.getValue()});
});
} else if (a.isArray() && b.isArray()) {
int common = Math.min(a.size(), b.size());
for (int i = 0; i < common; i++) diff(a.get(i), b.get(i), path + "[" + i + "]", out);
for (int i = common; i < a.size(); i++) out.put(path + "[" + i + "]", new Object[]{a.get(i), null});
for (int i = common; i < b.size(); i++) out.put(path + "[" + i + "]", new Object[]{null, b.get(i)});
} else {
out.put(path.isEmpty() ? "/" : path, new Object[]{a, b});
}
return out;
}
Explicación
Comparar JSON es recorrer árboles. Ambos árboles se recorren nodo a nodo: las claves de objeto se comprueban en los dos lados, los elementos de array se comparan por índice y los escalares por igualdad. Lo que cambia entre librerías es la salida:
- Informes categorizados (
DeepDiff,deep-object-diff, los recorridos recursivos de arriba) responden a “¿qué cambió?”. Ideales para tests y auditorías. - Parches RFC 6902 (
fast-json-patch,zjsonpatch) responden a “¿cómo convierto A en B?”: una lista de operaciones compacta y reproducible. Úsalos para APIs de sincronización y funciones de deshacer.
Un detalle: los arrays se comparan por posición. [1, 2] y [2, 1] producen un diff de dos cambios aunque el orden no importe en tu dominio. Ordena o normaliza primero cuando sea el caso.
Ejemplo práctico
Comparando una configuración de staging contra producción:
// staging.json
{ "db": { "host": "staging.db.local", "pool": 5 }, "cache": true, "features": ["search"] }
// prod.json
{ "db": { "host": "prod.db.local", "pool": 5 }, "cache": true, "features": ["search", "beta-ui"], "logLevel": "warn" }
El diff recursivo informa de tres cambios, no de un genérico “los archivos difieren”:
/db/host → staging.db.local → prod.db.local
/features/1 → añadido: "beta-ui"
/logLevel → añadido: "warn"
Eso sí es accionable: una diferencia de entorno esperada, una funcionalidad que alguien olvidó desactivar y un nivel de log que nunca se documentó. Un simple == no te habría contado nada de eso.
Variantes
| Tecnología | Librería | Enfoque | Mejor para |
|---|---|---|---|
| Python | deepdiff | DeepDiff(old, new) | Informes categorizados, ignore_order, exclusiones |
| Python | recursiva stdlib | json_diff() de arriba | Cero dependencias, control total del formato de salida |
| JavaScript | fast-json-patch | compare(old, new) | Parches RFC 6902, reversibles, compactos |
| JavaScript | deep-object-diff | detailedDiff(old, new) | División sencilla añadido/actualizado/eliminado |
| Java | zjsonpatch | JsonDiff.asJson(a, b) | RFC 6902 vía Jackson, probado en producción |
| Java | recursión Jackson | diff() de arriba | Sin dependencia, informe personalizado |
Lo que funciona
- Normaliza antes de comparar: ordena claves y arrays cuando el orden no importe;
DeepDiffaceptaignore_order=True - Excluye campos volátiles: timestamps, IDs de petición y contadores siempre difieren. Usa
exclude_pathsendeepdiffo poda las claves antes de comparar - Elige la salida según el trabajo: informe categorizado para auditorías, RFC 6902 cuando necesites reproducir el cambio
- Compara subárboles en documentos enormes: un diff profundo de un payload de 10MB es lento; compara las secciones que te importan o hashea primero
- Commitea los snapshots golden: los tests de snapshot necesitan archivos base en el control de versiones, revisados como código
Errores comunes
- Comparar floats directamente:
1.0y1.00serializan igual, pero el ruido de punto flotante (0.1 + 0.2) crea diffs fantasma. Redondea o usa una tolerancia - Ignorar el orden de arrays a propósito:
[1, 2]y[2, 1]son JSONs distintos; decide de antemano si el orden importa y normaliza si no - Comparar JSON serializado:
JSON.stringifyconserva el orden de inserción, así que{"a":1,"b":2}y{"b":2,"a":1}comparan como cadenas distintas. Parsea primero y compara después - Tratar null y ausente como iguales:
{"a": null}y{}son semánticamente distintos; asegúrate de que tu diff informa la clave como eliminada, no como cambiada - Loguear diffs en crudo: un diff estructural completo de una configuración grande es ilegible y puede filtrar secretos. Resume rutas y valores, y enmascara los campos sensibles primero
Cuándo NO comparar así
- Solo necesitas igualdad:
==(oassertEqualssobre nodos parseados) responde “igual o no” sin el coste del informe - El payload es enorme: los diffs profundos sobre documentos de varios MB queman CPU; compara hashes o checksums y solo difiere cuando no coincidan
- Los cambios llegan en tiempo real: comparar cada payload en streaming es demasiado caro; valida incrementalmente o muestrea
- Necesitas el resultado combinado: comparar te dice qué cambió, no combina los objetos. Para eso usa una receta de merge
Véase también
- RFC 6902 — JSON Patch: el formato estándar de operaciones que usan
fast-json-patchyzjsonpatch - RFC 6901 — JSON Pointer: la sintaxis
/ruta/al/valorque los parches usan para direccionar - Documentación de deepdiff: exclusiones,
ignore_order, vista de árbol y operadores personalizados - Merge JSON Files: la operación inversa, combinar objetos en vez de compararlos
- Validate JSON Schema: cuando necesitas comprobar la forma, no las diferencias
- Repositorio de recursos complementarios: diffs ejecutables en Python, JavaScript y Java con payloads de ejemplo
Preguntas frecuentes
¿Cómo ignoro campos concretos al comparar JSON?
Usa reglas de exclusión. DeepDiff soporta exclude_paths y exclude_regex_paths. fast-json-patch no filtra de forma nativa: preprocesa ambos objetos borrando las claves ignoradas antes de comparar. En los recorridos recursivos, salta la clave dentro del bucle.
¿Puedo comparar JSON ignorando el orden de los arrays?
Sí. DeepDiff tiene ignore_order=True. En JavaScript, ordena los arrays o conviértelos en conjuntos antes de comparar. Para los recorridos propios, ordena una copia de cada array con una clave estable antes de la comparación por índice.
¿Qué diferencia hay entre un informe de diff y un JSON Patch?
Un informe (added/removed/updated) es legible por personas y está pensado para inspección. Un JSON Patch (RFC 6902) es una lista de operaciones legible por máquina: add, remove, replace, move, copy, test, diseñada para reproducirse con applyPatch. Elige el informe para tests y auditorías; elige el parche para sincronización y deshacer.
¿Cómo genero un informe de diff legible?
El método pretty() de DeepDiff produce una salida legible. Para parches RFC 6902, mapea los códigos de operación a verbos: replace → "cambiado", add → "añadido", remove → "eliminado", e imprime cada uno con su ruta. Los recorridos recursivos te dejan formatear cada entrada como quieras.
¿Funciona el diff con arrays anidados dentro de objetos?
Sí, siempre que el algoritmo recorra ambos contenedores de forma recursiva. Los recorridos de arriba manejan anidamiento objeto-en-array y array-en-objeto a cualquier profundidad; lo que no pueden hacer es emparejar elementos de array reordenados por identidad, un problema más difícil que la mayoría de librerías solo resuelve aproximadamente.
¿Cómo gestiono null frente a una clave ausente?
Significan cosas distintas: {"a": null} asigna a a null explícitamente, mientras que {} ni siquiera tiene la clave. DeepDiff y los recorridos recursivos informan la clave ausente como añadida/eliminada y el null como un valor, que suele ser lo que quieres. Si tu dominio las trata como equivalentes, normaliza los campos null fuera antes de comparar.
Recursos Relacionados
Fusionar Archivos JSON
Cómo fusionar múltiples archivos JSON en un solo objeto o array en Python, Java y JavaScript.
RecipeMerge de Archivos JSON en JavaScript
Cómo combinar múltiples archivos JSON con estrategias de resolución de conflictos usando Node.js.
RecipeValidar JSON Schema
Cómo validar datos JSON contra schemas en Python, Java y JavaScript.
RecipeSerializar y Deserializar Datos
Cómo serializar y deserializar datos en JSON, XML y YAML en Python, Java y JavaScript.
RecipeDeep Clone de Objetos en JavaScript: Más Allá de JSON.parse
Compara estrategias de deep clone en JavaScript: JSON.parse, structuredClone, recursión manual y librerías para objetos anidados con referencias circulares.
RecipeParsear JSON en Python, JavaScript y Java
Parsea cadenas JSON a objetos nativos en Python, JavaScript y Java — con manejo de errores, streaming para archivos grandes y validación de esquema.