El mismo JSON, cuatro herramientas, cuatro resultados: cómo elegir un formateador
En resumen: Formatear no es sin pérdidas. El mismo JSON con un cero inicial, BOM o un entero de 20 dígitos da resultados distintos en Python, Node, jq y json.tool: algunos lanzan un error y otros reescriben el valor en silencio. Diez muestras ejecutadas localmente en cuatro rutas producen una tabla de elección y un script de validación estricta.

Conclusión: la diferencia decisiva entre formateadores JSON no es cuántos espacios indentan, sino que la misma entrada aquí lanza un error y allí reescribe los datos en silencio. Una sola muestra con valores límite enseña más que cualquier lista de funciones.
Se suele creer que formatear es «reordenar el texto». En realidad cada ruta vuelve a interpretarlo con su propio analizador. Si el analizador es tolerante, el error que debía aparecer se convierte en un cambio silencioso de valor, mientras la página sigue mostrando un verde tranquilizador de «Formateado correctamente».
1. El hecho: una entrada, cuatro rutas, cuatro resultados
Prueba con Python 3.11.1, Node v22.13.1 y jq 1.7. Diez cadenas JSON cortas construidas a mano se enviaron por entrada estándar, sin escribir en disco ni cambiar la configuración. Rutas: json.loads, JSON.parse, jq -c . y python -m json.tool. ERR indica excepción y parada; SILENT indica sin error pero salida distinta de la entrada.
| Muestra | python json.loads | node JSON.parse | jq -c . | python -m json.tool |
|---|---|---|---|---|
S1 {"a":1,"b":[1,2]} |
OK | OK | OK | OK |
S2 {"a":1,} |
ERR | ERR | ERR | ERR |
S3 {'a':1} |
ERR | ERR | ERR | ERR |
S4 {"a":01} |
ERR char 6 | ERR position 6 | SILENT {"a":1} |
ERR |
S5 {"a":1 /*x*/} |
ERR char 7 | ERR | ERR | ERR |
S6 \ufeff{"a":1} |
ERR utf-8-sig | ERR position 0 | SILENT {"a":1} |
ERR |
S7 {"id":9007199254740993} |
OK fiel | SILENT 9007199254740992 | OK fiel | OK |
S8 {"a":1,"a":2} |
SILENT {"a":2} |
SILENT {"a":2} |
SILENT {"a":2} |
OK |
S9 {"a":1 |
ERR char 6 | ERR | ERR EOF | ERR |
S10 <html>502</html> |
ERR char 0 | ERR Unexpected token '<' | ERR | ERR |
Cuatro de diez muestras dieron «sin error pero con datos cambiados»: S4, S6, S7 y S8. Si solo se confía en la luz verde, hay un cuarenta por ciento de probabilidades de tener algo distinto del original. Lo peor: ninguno aparece en una lista de errores de sintaxis. El archivo se abre, se copia y se exporta; la falla está en el contenido, no en la sintaxis.
2. La causa: sintaxis y semántica son dos capas
La RFC 8259 lo dice claro: la sección 1.2 recomienda evitar, por interoperabilidad, prácticas que ECMA-404 permite; la sección 4 exige nombres únicos solo como SHOULD sin definir qué hacer con claves duplicadas; la sección 6 solo da recomendaciones sobre números. ECMA-404 va más allá y se declara «agnóstica ante la semántica de los números»: especifica la representación, no el almacenamiento. La norma deja por tanto una amplia frontera a quien implementa.
Así el «formateo» se parte en dos: la capa sintáctica decide si el texto es legal y la semántica decide a qué valor se convierte. La primera tiene respuesta estándar; la segunda no. Que jq lea 01 como 1 y salte un BOM inicial es una decisión semántica; que Node meta un entero de 20 dígitos en un doble IEEE 754 es una decisión del runtime; que las cuatro rutas se queden con el último valor de {"a":1,"a":2} es una convención compartida. Nada de eso rompe «se puede analizar», pero todo cambia los datos.
Cómo corregir errores de análisis JSON trabaja en la capa sintáctica y traduce el mensaje de error a coordenadas. Este artículo va un paso antes: si la herramienta no da error, ni siquiera obtienes coordenadas.
3. Cuatro reescrituras silenciosas, una por una
Aisladas, las cuatro muestras muestran cambios tan pequeños que pasan desapercibidos:
{"a": 01} // cero inicial: jq devuelve {"a":1}
\ufeff{"a": 1} // BOM: jq salta los tres primeros bytes
{"id": 9007199254740993} // entero de 20 dígitos: JSON.parse da ...992
{"a": 1, "a": 2} // clave duplicada: las cuatro guardan {"a":2}
Cero inicial y BOM son del tipo «la entrada ya era inválida, pero la herramienta fue tolerante». 01 es ilegal en JSON estándar (RFC 8259 §6) y un BOM no debe abrir un texto JSON; Python y Node rechazan, jq sigue. Cómodo para limpiar logs, desastroso en validación de contrato: arriba creen que enviaron datos inválidos a corregir y abajo ya tienen un resultado «aparentemente normal».
El entero grande es «entrada totalmente legal, pero el runtime no puede contenerla». 9007199254740993 es un número JSON legal, pero supera 2^53-1 y JSON.parse solo puede representar 9007199254740992. El int de Python y jq conservan el valor. Si es un número de pedido o un Snowflake ID, un último dígito cambiado descuadra la conciliación.
Las claves duplicadas son «la norma no dice, cada implementación elige». Las cuatro rutas toman en silencio el último valor, sin aviso. La RFC 8259 solo dice SHOULD, así que no es un bug, pero es la clase de problema más difícil de rastrear al concatenar configs o fusionar parches.
4. Informes de error: quién da posición y quién una sola línea
También en las seis muestras fallidas cambia mucho la información. La JSONDecodeError de Python expone msg, lineno, colno y pos; Node v22 informa position N (line 1 column M) con un fragmento del original; python -m json.tool reutiliza los datos de Python; jq solo da parse error: ... at line N, sin desplazamiento de carácter.
En JSON minificado de una línea, línea y columna casi no sirven: todo está en la línea 1. Lo que hace falta es un desplazamiento de carácter. Este script normaliza la validación en «error + offset + contexto»:
import json, sys
def strict_check(text: str) -> None:
try:
json.loads(text)
print("OK")
except json.JSONDecodeError as e:
print(f"ERR {e.msg} | char {e.pos} | line {e.lineno} col {e.colno}")
lo, hi = max(0, e.pos - 30), min(len(text), e.pos + 30)
print("...", text[lo:hi].replace("\n", "\\n"), "...")
sys.exit(1)
strict_check(open(sys.argv[1], encoding="utf-8").read())
Es más estricto que jq . (rechaza cero inicial y BOM) y entrega e.pos. Cómo convertir line 1 column 23611 a un offset de byte y por qué los offsets de carácter y de byte se separan por un factor tres en documentos CJK lo explica Depurar JSON minificado; no se repite aquí.
5. La corrección: separar «validación estricta» y «embellecido»
La conclusión es simple: un botón «Formatear» no debe asumir validación y embellecido a la vez. Primero un analizador estricto decide la legalidad; después se elige la presentación. Una línea aísla el primer paso:
python3 -m json.tool input.json > /dev/null && echo "sintaxis OK" || echo "sintaxis invalida"
python3 -c "import json,sys; d=json.load(open(sys.argv[1],encoding='utf-8')); print(type(d).__name__, len(d))" input.json
La primera responde solo «¿se puede analizar?» y la segunda comprueba «¿la estructura es la esperada?». Con enteros grandes, leer con Python o jq en vez de JSON.parse; con posible BOM, abrir explícitamente con encoding='utf-8-sig' en lugar de esperar un salto automático; con claves que puedan repetirse, recorrer antes de analizar, porque ninguna de las cuatro rutas avisa. Llevado a CI es exactamente el flujo de Editores, línea de comandos y cadenas de herramientas de automatización con una capa extra de aserciones de fidelidad.
6. Cómo elegir: tabla de las cuatro rutas
| Ruta | Fortaleza | Riesgo | Ideal para |
|---|---|---|---|
python json.loads / json.tool |
Estricto, da línea/columna y offset, enteros sin pérdida | Rechaza cero inicial y BOM, codificación explícita | Validación de contrato, CI, triage de logs |
node JSON.parse |
Nativo en el navegador, error con fragmento de origen | Enteros grandes truncados en silencio, error con BOM | Depuración front, herramientas en navegador |
jq -c . |
Amigable con pipes, resultado más rápido | Cero inicial y BOM pasan en silencio, sin offset | Pipelines de operaciones, extracción rápida |
Herramientas web /parse, /editor |
Vista de árbol, orden de claves, copiar ruta, sin instalación | Depende del analizador de la página, verificar una vez | Inspección manual, diagnóstico puntual |
La pregunta no es «quién tiene más funciones» sino «¿mentirá sobre mis datos?». Con IDs de pedido, importes o IDs de dispositivo, mejor Python o una herramienta que declare su fidelidad; para depurar estructura sin persistir, el navegador basta. El /parse de este sitio valida y formatea dentro del navegador, ideal para «ver la forma», pero ante enteros grandes conviene reverificar con el script anterior.
7. La privacidad es solo una de las fronteras
«¿Se subirán mis datos?» es la pregunta más frecuente sobre herramientas online, pero es independiente de «¿se modificarán mis datos?». Una herramienta local sin red puede truncar igual un entero de 20 dígitos; a la inversa, una herramienta online que errorea con rigor puede hacer POST del texto original. Se verifican por separado y no se sustituyen. La FAQ de privacidad da tres comprobaciones de dos minutos; este artículo aporta la otra mitad: con la lista de muestras de abajo, confirmar que la herramienta no reescribe valores en silencio.
8. Lista de autocomprobación copiable
Guardar S1–S10 en cases.txt, una línea por muestra, pegar en cada candidato y comprobar tres cosas: si las muestras inválidas fallan de verdad, si el fallo da una posición concreta y si la salida de una muestra válida coincide carácter a carácter con la entrada. Vigilar sobre todo S4, S6, S7 y S8: una sola fila SILENT significa que esa ruta no sirve por sí sola para validar datos.
Añadir tres restricciones de ingeniería: poner la validación estricta en un hook pre-commit en vez de descubrir el desajuste en producción; escribir archivos como UTF-8 sin BOM en todos los idiomas; ante JSON de terceros, conservar el cuerpo de respuesta crudo y usar el resultado formateado solo para leer, nunca para reescribir.
Preguntas frecuentes
¿Un formateo correcto significa que el JSON está bien?
No. S4, S6, S7 y S8 «pasaron» el formateo, pero cero inicial, BOM, entero de 20 dígitos y clave duplicada se reescribieron o se tragaron. Formatear solo prueba que algún analizador aceptó el texto, no que los valores igualen a los enviados. Comprueba los campos críticos valor por valor.
¿Por qué jq acepta ceros iniciales y BOM que Python rechaza?
jq tiene su propio analizador léxico: es tolerante con números como 01 y salta un BOM inicial, mientras Python y Node siguen estrictamente la RFC 8259. No es un bug de versión, sino el compromiso entre analizador tolerante y estricto. El primero ahorra trabajo al limpiar; el segundo es obligatorio al validar contratos.
¿Cómo evito que un entero grande se convierta en 9007199254740992?
El Number de JavaScript es un doble IEEE 754 con límite seguro 2^53-1. Leer un ID de 20 dígitos con JSON.parse lo trunca inevitablemente. Guarda esos IDs como cadenas, o usa el int de Python, jq o un analizador con BigInt. Si el front necesita un número, sustituye ese campo por una cadena antes de analizar.
¿Herramienta online o línea de comandos local?
Por sensibilidad y propósito: para ver estructura o encontrar un campo, el navegador es lo más rápido; para pipelines y aserciones de fidelidad, python -m json.tool más un verificador estricto. En ambos casos ejecuta antes S1–S10, confirma errores y salidas y decide después qué clase de datos introduce.