DevKitLab Logo DevKitLab
JSON / depuración / formateo

Cómo arreglar un JSON que no parsea (y ordenar el desastre que queda)

Un error de parseo suele significar que la entrada nunca fue JSON estricto. Aprende a leer el error, detectar los cinco fallos habituales y dejarlo legible.

Una línea de log desordenada convertida en JSON limpio e indentado, con la posición del error de parseo marcada

Un compañero suelta una línea en Slack: «¿por qué revienta esto?». Es un fragmento que copió de los logs de un servicio en Python y lo quiere como JSON limpio para poder leerlo. Lo pegas en un formateador, pulsas formatear y se pone en rojo. JSON inválido.

Aquí está lo que hace tropezar a la gente: la mayoría de las veces la herramienta tiene razón y la entrada está mal. No mal como en «roto sin remedio», sino mal como en nunca fue JSON estricto desde el principio. Es un diccionario de Python impreso, o un objeto de JavaScript que alguien logueó, o un payload con otro payload metido dentro como cadena. Todos parecen JSON a primera vista y ninguno parsea. Este artículo recorre el desastre en el orden en que realmente te lo encuentras: leer el error, averiguar por qué no es JSON, lidiar con el caso anidado más traicionero y, por fin —cuando ya parsea—, dejarlo legible.

El bloque que vamos a arreglar

Esto es lo que pegó el compañero, bastante cerca de algo real:

{
  users: [
    {'id': 1, 'name': "Ada", 'active': True, 'meta': '{"role":"admin","seen":null}'},
    {'id': 2, 'name': "Grace", 'active': False, 'meta': None},
  ]
}

Si entornas los ojos, es obviamente datos. Pero falla en el primerísimo token y esconde, en apenas cinco líneas, al menos cuatro motivos distintos de fallo. Los iremos despejando uno a uno y lo usaremos como ejemplo a lo largo de todo el artículo.

Primero, lee el error: es más útil de lo que parece

Antes de arreglar nada, lee lo que dijo el parser en realidad. JSON.parse no falla de forma vaga; falla en un carácter concreto, y el mensaje te dice cuál. El inconveniente es que el mensaje se ve distinto según dónde lo ejecutaste, y eso despista.

Toma un ejemplo roto más pequeño —una coma final— para que la posición sea fácil de contar:

{"name": "Ada",}

Un motor V8 antiguo (Node anterior a 21, Chrome viejo) informa:

SyntaxError: Unexpected token } in JSON at position 15

La posición 15 es un desplazamiento de caracteres desde el inicio de la cadena, contando desde cero e incluyendo cada espacio y cada comilla. Cuenta hasta 15 y caes justo en el }: el parser esperaba otra clave después de la coma y se topó con la llave de cierre. Ese número es lo importante: es una ubicación, no una queja difusa, y suele ser todo lo que necesitas para dar con el carácter de más.

El V8 más nuevo (Node 21+, Chrome reciente) reescribió el mensaje y, para fastidio de todos, se dejó por el camino ese desplazamiento tan claro:

SyntaxError: Unexpected token '}', "{"name": "Ada",}" is not valid JSON

Ahora recibes un fragmento del texto de alrededor en vez de un número. Firefox, por su parte, siempre ha hecho lo más amable de los tres:

SyntaxError: JSON.parse: expected property name or '}' at line 1 column 16 of the JSON data

Línea y columna, y además lo que esperaba. Tres motores, tres dialectos, el mismo hecho de fondo: algo en un punto exacto no está permitido ahí. Cuando un formateador te marca la ubicación del error y te deja saltar directo a esa línea y esa columna, ese es el paso que te ahorra: dejas de contar caracteres a mano y vas a mirar el punto. Pega nuestro bloque grande en el Formateador JSON y se detiene en el primer infractor y lo señala; haz clic en el error y el cursor cae justo ahí.

Así que el primer infractor de nuestro bloque es la u de users, arriba del todo. Lo que nos lleva al porqué.

Por qué no es JSON: cinco cosas que parecen bien y no lo están

El JSON estricto es un formato estrecho y aburrido a propósito: eso es justo lo que lo hace portable. La mayoría del «JSON inválido» es en realidad otra cosa válida que tomó prestadas las llaves. Estas cinco aparecen sin parar, y nuestro bloque carga con cuatro de ellas.

Las claves necesitan comillas dobles. {users: [...]} es un objeto literal de JavaScript perfectamente correcto, y no es JSON. JSON exige "users". Este es el motivo número uno por el que un objeto JS copiado, o su console.log, no parsea: el lenguaje te deja omitir las comillas, el formato de intercambio no.

Las cadenas van con comillas dobles, no simples. 'name' y 'admin' quedan fuera. JSON solo conoce ". El print() de Python, la salida inspect de Ruby y un montón de formateadores de logs emiten comillas simples, así que cualquier cosa copiada de ahí tropieza aquí de inmediato.

Los booleanos y el nulo de Python no son los de JSON. Nuestro bloque tiene True, False y None: son literales de Python. JSON los escribe en minúsculas, true, false y null. La misma trampa con NaN e Infinity, que algunas librerías emiten y el JSON estricto prohíbe de plano; no hay forma válida de escribir «no es un número» en JSON, algo que conviene recordar antes de confiar en que hará ida y vuelta.

Nada de comas finales. La coma tras el último usuario —None}, y luego ]— es ilegal en JSON aunque casi todos los lenguajes de programación ya la permitan. Esta es la que más duele, porque los editores y los linters nos han entrenado para dejar comas finales por todas partes; JSON es el único sitio que aún las rechaza.

Nada de comentarios. No están en nuestro bloque, pero son de la misma familia: // y /* */ no son JSON. Si estás editando un tsconfig.json o un archivo de ajustes de VS Code con comentarios, eso es JSONC, un superconjunto: un parser estricto se atraganta con los comentarios.

Puedes arreglar todo esto a mano, y para cinco líneas vale. Para algo más grande es tedioso y propenso a errores, y por eso un buen formateador tiene una pasada de reparación: prueba primero el JSON estricto y luego interpretaciones cada vez más laxas —comillas simples, literales de Python, comas finales— y te dice qué tuvo que suponer. La reparación es una comodidad, no una garantía; sirve para leer una línea de log rápido, no para dar el visto bueno a entrada no confiable. Pero con nuestro bloque hace lo obvio y devuelve un JSON parseable.

El más traicionero: JSON dentro de JSON

Arregla los cuatro problemas de arriba y el bloque parsea, pero fíjate en el campo meta de Ada:

"meta": "{\"role\":\"admin\",\"seen\":null}"

Ese valor no es un objeto. Es una cadena que resulta contener JSON, escapado y metido dentro del documento exterior. Está en todas partes en cuanto empiezas a notarlo: una columna de base de datos de tipo texto que guarda JSON serializado, un webhook que anida su payload real como cadena, un JWT cuyo cuerpo decodificado es JSON que luego tienes que decodificar otra vez, un campo de log que serializó un objeto antes de escribirlo. Un formateador normal te mostrará ese valor como una larga cadena escapada y te dejará a ti arreglártelas.

Aquí hay dos jugadas. Si todo lo que pegaste es una cadena JSON —el bloque entero envuelto en comillas con \" por todos lados, que es lo que obtienes al copiar un valor serializado—, lo desescapas una vez para devolverlo a un documento estructurado. Si es un documento que tiene JSON serializado en algunos de sus campos, como nuestro meta, lo que quieres es expandir las cadenas anidadas ahí mismo, de modo que "{\"role\":\"admin\"}" pase a ser un objeto anidado de verdad que puedas leer y plegar. El Formateador JSON tiene ambas, y expandir cadenas anidadas es la que más alivio da encontrar: te ahorra el bailecito de «copiar este valor a otra pestaña, pegar, desescapar, copiar de vuelta» que suelen exigir los datos con doble codificación.

Tras expandir, el registro de Ada por fin se lee como datos:

{
  "id": 1,
  "name": "Ada",
  "active": true,
  "meta": { "role": "admin", "seen": null }
}
El Formateador JSON tras reparar y expandir las cadenas anidadas: el campo meta, que antes era una cadena escapada, ahora es un objeto anidado real, con la ruta JSON del cursor mostrada en la barra de estado
La reparación corrige la sintaxis, la expansión anidada devuelve el campo meta serializado a un objeto y la barra de estado muestra la ruta JSON.

Ya parsea: minificar, formatear u ordenar claves

Un JSON válido es la línea de salida, no la meta. Lo que hagas a continuación depende de por qué necesitabas leerlo.

Formatear (pretty-print) es lo predeterminado: indentar, poner cada cosa en su línea, hacerlo escaneable. La indentación de dos espacios es el estilo habitual y lo que produce la mayoría de los formateadores. Es lo que quieres cuando estás leyendo una respuesta o depurando un payload.

Minificar es lo contrario: quitar cada espacio y salto de línea hasta dejarlo en una sola línea. Esa es la forma que quieres cuando el JSON va dentro de algo: el cuerpo de una petición, una variable de entorno, una única línea de log, un campo de otro documento JSON. Nadie lee JSON minificado; es para transmitirlo, no para ti.

Ordenar claves es la más desaprovechada. Los objetos JSON no tienen un orden de claves definido, así que dos servicios pueden emitir el mismo registro con los campos en distinta secuencia, y en cuanto intentas comparar dos payloads, ese orden inestable entierra el cambio real bajo el ruido. Ordenar las claves de forma recursiva antes de guardar un fixture, o antes de un diff, hace estable la salida, de modo que la siguiente comparación muestra solo lo que de verdad cambió. (Un aviso: ordenar reescribe el orden, así que si el orden original importaba —una configuración afinada a mano donde los campos están agrupados a propósito—, copia antes el original.) Esto enlaza directo con el problema del ruido en comparar dos archivos JSON: normaliza primero ambos lados y el diff dirá la verdad.

Dónde formatearlo: terminal, editor, código o navegador

No hay un único sitio correcto, y fingir que lo hay sería deshonesto. Echa mano de lo que ya tengas delante.

Terminal. Si el archivo está en disco, jq es difícil de superar: jq . file.json lo formatea, jq -S . file.json lo formatea y ordena las claves, y jq -c . file.json lo minifica. ¿No tienes jq? python -m json.tool file.json está en casi cualquier máquina y también informa de la línea y la columna del primer error de sintaxis, lo que lo convierte en un validador decente para salir del paso. El inconveniente: tanto jq como python rechazan cualquier cosa que no sea ya JSON estricto, así que no ayudan con el lío de comillas simples y True del principio; se limitan a informar del error, no a repararlo.

Editor. VS Code y sus parientes formatean un archivo JSON con el formateador integrado (Shift+Alt+F, o formatear al guardar) y subrayan los errores de sintaxis mientras escribes. Estupendo cuando el archivo ya está abierto y ya es casi válido; menos práctico para un bloque suelto que copiaste de algún sitio y no quieres guardar como archivo primero.

Código. Dentro de un programa, no es más que JSON.stringify(value, null, 2) en JavaScript o json.dumps(obj, indent=2, sort_keys=True) en Python. Esta es la herramienta correcta cuando el formateo tiene que ocurrir como parte de un script o un paso de build, no como un vistazo puntual.

Navegador. Para la tarea concreta de pegar algo, verlo limpio y sacarlo de vuelta —que es la mayor parte de lo que «formatear JSON» significa en el día a día—, una herramienta de navegador gana por comodidad. Nada que instalar, formatea al pegar, repara los casos «casi JSON» que las herramientas de terminal rechazan y maneja el lío de las cadenas anidadas ahí mismo. No reemplaza a jq en un pipeline. Reemplaza los doce segundos de ceremonia de cuando solo quieres leer algo.

Por qué un JSON grande congela pestañas, y por qué no tiene por qué

Todo el que trabaja con JSON ha matado una pestaña del navegador así: pegas unos cuantos megas de un log exportado en algún formateador online, la página se cuelga, el ventilador se dispara y la cierras y vuelves a jq. Pasa lo bastante a menudo como para que mucha gente haya descartado en silencio los formateadores de navegador para cualquier cosa grande.

La causa habitual es el renderizado ingenuo. Muchos formateadores web meten todo el documento formateado en el DOM de la página de una vez —cada línea, cada corchete, cada span con resaltado de sintaxis— y luego rehacen buena parte de ese trabajo con cada tecla. Con unos cientos de líneas no lo notas. Con unos cientos de miles, el navegador intenta maquetar y pintar más nodos DOM de los que ninguna página debería contener, y se atasca.

El arreglo es dejar de renderizar lo que nadie está mirando. El Formateador JSON está construido sobre un editor (CodeMirror) que solo renderiza las líneas que hay ahora en el viewport: al hacer scroll, construye las filas que acabas de revelar y descarta las que dejaste atrás. El documento puede ser enorme mientras el DOM se mantiene pequeño, porque el DOM solo contiene la pantalla que de verdad puedes ver. El resaltado de sintaxis también es incremental: una edición vuelve a analizar solo la parte que cambió en lugar de reparsear el archivo entero, y el trabajo estructural más pesado (como plegar cada rama de golpe) se ejecuta con un presupuesto de tiempo limitado y cede con elegancia en vez de congelar la pestaña. Añade los resúmenes de plegado —un array plegado muestra 5 elements, un objeto plegado muestra 10 keys— y podrás recorrer un payload enorme plegándolo todo y abriendo solo la rama que te interesa, sin llegar nunca a maquetar el resto.

Nada de esto hace gratis a JSON.parse; parsear un archivo verdaderamente enorme sigue costando algo por adelantado. Pero la diferencia entre «fluido» y «congelado» casi nunca está en el parseo. Está en lo que le pasa al DOM después, y renderizar solo el viewport es lo que lo mantiene fluido.

¿Es seguro pegarlo en un formateador online?

Merece una pausa, porque la respuesta honesta es «depende de qué estés pegando». Buena parte del JSON que querrías formatear es justo del tipo sensible: una respuesta de API de producción, un token decodificado, una configuración interna, el cuerpo de un webhook con datos de clientes dentro. Si un formateador envía ese texto a un servidor para hacer el trabajo, acabas de entregarle una copia a un tercero, y «fue solo un segundo» no la des-envía.

La salida no es fiarte de una política de privacidad; es usar una herramienta que nunca necesita la red. Cuando el parseo, la reparación, el formateo, el plegado y la lectura de archivos corren todos en el navegador con JavaScript, el texto que pegas se queda en tu máquina: nada se sube, así que no hay nada que filtrar. Esa es la propiedad que hay que buscar, y es la razón por la que un formateador local-first es la opción segura por defecto para cualquier cosa que no publicarías abiertamente.

Dos trampas que sobreviven incluso a un JSON válido

Dos cosas que vale la pena saber, porque pasan todos los validadores y aun así muerden.

Los enteros grandes pierden precisión. Los números de JavaScript son de coma flotante de 64 bits, y JSON.parse lee los números en ellos. Un ID de base de datos de 64 bits o un snowflake al estilo de Twitter pueden superar lo que un flotante representa con exactitud, y se redondea en silencio:

JSON.parse('{"id": 9007199254740993}').id
// → 9007199254740992   ← ya no es el número que tenías

El JSON era válido. El ID ahora se ha desviado en uno, en silencio, y no saltó ningún error. Si mueves IDs enteros grandes por cualquier cosa basada en JavaScript, guárdalos como cadenas.

Claves duplicadas. {"a": 1, "a": 2} es JSON técnicamente válido, y la especificación no dice qué debería hacer un parser con ello. En la práctica, la mayoría se queda con el último, así que esto se lee de vuelta como {"a": 2} y el primer valor se esfuma sin quejarse. Es raro, pero cuando un documento generado tiene un campo duplicado, esta es la razón por la que uno de ellos parece desaparecer.

Volver a montarlo

Nuestro bloque de Slack empezó como la impresión de un diccionario de Python con comillas simples, True/None, una coma final y un payload escondido dentro de una cadena. La salida fue el mismo orden cada vez: leer el error para dar con dónde se rompe, reconocer por qué no es JSON estricto, expandir la cadena anidada que ocultaba datos reales y solo entonces formatear, ordenar o minificar según lo que vayas a hacer con él después.

Formatear suele ser el primer paso, no el trabajo entero. Una vez limpio, comparar dos versiones es tarea de un JSON Diff estructural; convertir una respuesta de ejemplo en interfaces tipadas es lo que hace JSON a TypeScript; y sacar un solo campo de un payload profundo sin hacer scroll es lo que hace un evaluador JSONPath. Trabajos distintos, pero todos empiezan igual: con un JSON que de verdad parsea y que por fin puedes leer.