Una breve nota sobre por qué una empresa de proxies escribe esto: somos Geonode, y el error JSON más habitual que nos comunican nuestros clientes es «Unexpected token '<'» en los datos que han obtenido. Esto significa que la respuesta era HTML —una página de error, una redirección de inicio de sesión o una página de bloqueo— y que el analizador indica correctamente que no se le ha proporcionado JSON. Antes de modificar ningún código, registra los primeros 200 caracteres de lo que has recibido. Casi todo lo demás que se explica en este artículo da por hecho que el archivo es realmente JSON, y esa suposición es la que falla con mayor frecuencia.
En Node: Lectura desde el disco
Tres métodos, y uno de ellos es la solución más moderna.
**fs/promises
, la vía estándar:**
import { readFile } from "node:fs/promises";
const raw = await readFile("./data.json", "utf8");
const data = JSON.parse(raw);
La documentación de Node es muy clara respecto al argumento de codificación, y es importante: sin una codificación, readFile
«devuelve una promesa que se cumple con un objeto <Buffer>
que contiene el contenido del archivo»; con una, «se cumple con un <string>
». JSON.parse
acepta un Buffer convirtiéndolo a una cadena, por lo que omitir la codificación suele funcionar y supone realizar una conversión adicional sin motivo. Pasa "utf8"
.
También admite un AbortSignal
a través de la opción signal
, «lo que te permite abortar una operación readFile
en curso», algo útil cuando una lectura forma parte de una solicitud que puede cancelarse.
Sincrónico, para el código de inicio:
import { readFileSync } from "node:fs";
const config = JSON.parse(readFileSync("./config.json", "utf8"));
El bloqueo está bien antes de que el servidor empiece a servir. No está bien dentro de un gestor de solicitudes, donde paraliza el bucle de eventos para todas las demás conexiones. Esa distinción es la regla fundamental.
**require
, solo en CommonJS:**
const data = require("./data.json");
Es conciso y tiene dos propiedades que la gente suele olvidar. Almacena en caché, por lo que un segundo require
de la misma ruta devuelve el mismo objeto sin volver a leer el archivo, lo que significa que editar el archivo en tiempo de ejecución no tiene ningún efecto. Además, no está disponible en los módulos ES.
Importación de atributos: el método estándar
La sintaxis a la que la mayoría de la gente aún no se ha adaptado, y la que se debe utilizar en el código nuevo.
import data from "./data.json" with { type: "json" };
O de forma dinámica:
const data = await import("./data.json", { with: { type: "json" } });
MDN lo registra como Baseline 2025, disponible desde abril de 2025 en los navegadores más recientes, y los entornos de ejecución que no son navegadores, como Node y Deno, se alinean con la semántica de los navegadores para los módulos JSON.
El atributo ``type: "json"`
no es meramente decorativo. MDN explica que «valida que un módulo se sirva con el tipo MIME ``application/json
», y que si el archivo «se sirve con cualquier tipo de medio distinto de ``application/json
`, la importación fallará».
Merece la pena citar el razonamiento de seguridad, ya que explica por qué el atributo es obligatorio y no opcional:
Si, por alguna razón (por ejemplo, si el servidor ha sido secuestrado o es falso), el tipo de medio en la respuesta del servidor se establece en
text/javascript
(para código fuente de JavaScript), entonces el archivo se analizaría y se ejecutaría como código. Si el archivo «JSON» contuviera realmente código malicioso, la declaración import
ejecutaría involuntariamente código externo, lo que supondría una grave amenaza.
Una nota sobre la migración: una propuesta anterior utilizaba la palabra clave «assert
» en lugar de «with
». MDN señala esto como un cambio que rompe la compatibilidad: las implementaciones que utilizan «assert
» «ya no son compatibles». Si encuentras «assert { type: "json" }
» en código antiguo o en un tutorial, es necesario actualizarlo.
Gestión de errores que te aporta información
El hábito que marca la diferencia entre un problema de cinco minutos y uno de una hora.
function parseJson(text, source) {
try {
return JSON.parse(text);
} catch (err) {
throw new Error(
`Failed to parse JSON from ${source}: ${err.message}. ` +
`First 200 chars: ${text.slice(0, 200)}`
);
}
}
Lo importante es el fragmento. JSON.parse Los errores indican una posición y un carácter; la entrada real te explica el motivo. Tres firmas cubren la mayoría de los casos:
Unexpected token '<' — el contenido es HTML. Una página de error, una redirección de inicio de sesión o un listado de directorios.
Unexpected end of JSON input — la entrada está vacía o truncada. Una respuesta 204, un archivo que no se ha podido escribir por completo o una solicitud de obtención que te has olvidado de esperar.
Unexpected token '}' en una posición plausible — JSON con un formato realmente incorrecto, a menudo una coma al final. JSON las prohíbe, aunque JavaScript las permita.
En la lectura de archivos, distingue entre el error de lectura y el error de análisis. ENOENT significa que el archivo no existe, lo cual es un problema distinto al de un contenido no válido y merece un mensaje diferente.
Validación de lo que lees
El análisis sintáctico se ha realizado con éxito. Esto solo indica que la sintaxis era válida, pero no dice nada en absoluto sobre si los datos tienen la estructura que espera tu código; y la diferencia entre ambos aspectos es precisamente donde se esconden una proporción sorprendente de fallos en producción.
El análisis sintáctico y la validación son pasos independientes. JSON.parse
devolverá sin problemas {"user": {"nmae": "Ada"}}
, aunque haya un error ortográfico en la clave, o un campo price
que contenga la cadena "n/a"
cuando se esperaba un número. Tu código fallará entonces en algún punto posterior, a varias funciones de distancia del problema real, con un error que indica un síntoma en lugar de la causa.
Para cualquier cosa que escape a tu control, valida el dato según un esquema. Hay varias bibliotecas que gestionan esto bien, y el patrón es el mismo independientemente de cuál elijas:
const Config = z.object({
port: z.number().int().min(1).max(65535),
host: z.string(),
retries: z.number().int().default(3),
features: z.array(z.string()).optional(),
});
const config = Config.parse(JSON.parse(await readFile("./config.json", "utf8")));
Ahora el error indica el campo, el tipo esperado y lo que se ha encontrado —lo cual marca la diferencia entre una corrección de cinco minutos y una tarde entera.
En casos sencillos, unas cuantas aserciones no cuestan nada:
const data = JSON.parse(raw);
if (!Array.isArray(data.items)) throw new Error("items must be an array");
if (data.items.length === 0) throw new Error("items is empty — check the source");
Esa segunda comprobación vale más de lo que parece. Un array vacío es JSON válido, se analiza correctamente y, muy a menudo, es un síntoma más que un resultado legítimo: una API que no ha devuelto nada porque un filtro era incorrecto, o un rastreo que ha tenido éxito en una página que había cambiado.
Desconfía de los números que no hayas generado tú mismo. Los números JSON se convierten en valores de tipo «double» de JavaScript, por lo que los enteros superiores a Number.MAX_SAFE_INTEGER
pierden precisión de forma silenciosa, sin que se produzca ningún error en ningún momento. Los identificadores suelen ser las víctimas habituales: dos registros distintos de una base de datos pueden analizarse y dar el mismo valor. Si un campo es un identificador en lugar de una cantidad, debería ser una cadena en el JSON; y si no controlas al generador, el argumento ``context.source`
` del recuperador te proporciona los dígitos originales.
Y valida en el punto de entrada, una sola vez. Comprobar la estructura de los datos en el momento en que entran en tu programa significa que todo lo que viene después puede dar por hecho que son correctos. Realizar comprobaciones preventivas en veinte lugares supone tener que actualizar veinte lugares y no haber un único punto donde quede registrado el contrato.
Archivos grandes: «
JSON.parse
» es un proceso síncrono y necesita tener todo el documento en memoria. Ambas cosas se convierten en un problema a medida que los archivos crecen.
Orientación aproximada. Si es inferior a un megabyte, ni te lo plantees. Entre uno y diez, evalúa la situación —especialmente en el hilo principal de un navegador, donde el análisis bloquea la visualización y provoca tirones visibles—. Por encima de diez megabytes, o de aproximadamente una décima parte de tu memoria disponible, busca otra solución.
Sácalo del hilo principal. En un navegador, un Web Worker analiza el documento sin que se cuelgue la interfaz. En Node, un hilo de trabajo hace lo mismo con el bucle de eventos.
Utiliza JSON delimitado por saltos de línea. Se trata de una solución estructural, más que de un truco. Un documento JSON por línea significa que procesas un archivo de cualquier tamaño con un consumo de memoria constante, cada línea se analiza de forma independiente y un archivo truncado sigue proporcionando todos los registros completos:
import { createReadStream } from "node:fs";
import { createInterface } from "node:readline";
const rl = createInterface({ input: createReadStream("./data.jsonl") });
for await (const line of rl) {
if (line.trim()) handle(JSON.parse(line));
}
Si controlas el formato, este es el mejor diseño para cualquier cosa que se vaya añadiendo con el tiempo —y la razón por la que un trabajo de recopilación que se cuelga deja un archivo utilizable en lugar de uno imposible de analizar.
Utiliza un analizador de flujo continuo cuando el formato sea una única matriz grande que no puedas modificar. Varias bibliotecas emiten los valores a medida que llegan, en lugar de construir el árbol completo.
O pide menos. Paginación, selección de campos, un punto final más específico. Casi siempre es la respuesta correcta y casi siempre se pasa por alto porque requiere hablar con quien sea el responsable de la API.
Escribir JSON de nuevo
La otra cara de la moneda, en pocas palabras, ya que suele ser la siguiente pregunta.
import { writeFile } from "node:fs/promises";
await writeFile("./out.json", JSON.stringify(data, null, 2), "utf8");
Los argumentos «null, 2
» generan una salida con sangría, lo cual es más importante de lo que parece: un archivo que vaya a ser leído por una persona o comparado en un sistema de control de versiones debe estar formateado, mientras que uno que se vaya a transmitir no debe estarlo.
Hay tres cosas que no sobreviven a ``JSON.stringify`
y provocan una pérdida silenciosa de datos en lugar de errores. Los valores ``undefined
y las funciones se eliminan por completo de los objetos y se convierten en ``null
dentro de los arrays. Los objetos ``Date
se convierten en cadenas ISO, por lo que no vuelven a ser fechas sin un «reviver». Y ``BigInt
` lanza un error directamente; el enfoque estándar es serializar los enteros grandes como cadenas.
Para añadir elementos, escribe JSON delimitado por saltos de línea en lugar de reescribir un array:
import { appendFile } from "node:fs/promises";
await appendFile("./log.jsonl", JSON.stringify(record) + "\n", "utf8");
Añadir elementos a un array JSON requiere leerlo, analizarlo, añadir elementos y reescribir todo el archivo, lo cual es costoso y corrompe el archivo si el proceso se interrumpe a mitad de la escritura.
Preguntas relacionadas
¿Cómo se lee un archivo JSON en Node.js?
const data = JSON.parse(await readFile("./data.json", "utf8")) utilizando node:fs/promises. O bien, utiliza la sintaxis de importación estándar: import data from "./data.json" with { type: "json" }, que es la norma de referencia a partir de 2025 y funciona tanto en Node como en los navegadores.
¿Puede JavaScript leer un archivo local en el navegador?
No por ruta. Los navegadores no pueden abrir archivos locales arbitrarios, lo cual es una restricción de seguridad deliberada. El usuario debe seleccionar un archivo mediante un <input type="file"> o arrastrar y soltar; a continuación, File.text() te muestra el contenido.
¿Qué hace import ... with { type: "json" }?
Importa un archivo JSON como módulo al tiempo que comprueba que el servidor lo haya enviado con el tipo MIME application/json. Sin esa comprobación, un archivo enviado como text/javascript se analizaría y se ejecutaría como código, lo cual es el problema de seguridad que este atributo pretende evitar.
¿Por qué aparece el mensaje «Unexpected token '<'» al leer JSON?
Porque el contenido comienza por <, lo que significa que has recibido HTML en lugar de JSON —normalmente una página de error o una redirección de inicio de sesión—. Con fetch, la causa es casi siempre que falta una comprobación de res.ok, ya que fetch no rechaza los estados de error HTTP.
¿Debería usar «require» o «import» para JSON?
Utiliza «import ... with { type: "json" }» en el código nuevo, ya que es el estándar y funciona en módulos ES. «require» es exclusivo de CommonJS y almacena el resultado en caché, por lo que un archivo editado en tiempo de ejecución no se volverá a leer. Ninguna de las dos opciones es adecuada para archivos que cambian mientras se ejecuta el programa; para esos casos, utiliza «readFile».
¿Cómo puedo leer un archivo JSON muy grande?
Traslada el análisis fuera del hilo principal utilizando un «worker», o reestructura los datos como JSON delimitado por saltos de línea para que cada línea se analice de forma independiente en memoria constante. Para un array grande que no se modifique, utiliza un analizador de flujo continuo. Y plantéate si puedes solicitar menos datos desde el principio.
¿Cuál es la diferencia entre JSON.parse y response.json()?
Response.json() lee el cuerpo de una solicitud y lo analiza en un solo paso, y no admite una función de reactivación. JSON.parse trabaja con una cadena que ya tienes y sí la admite. Si necesitas una función de reactivación —para fechas o números enteros grandes—, utiliza res.text() seguido de JSON.parse.
¿Cómo gestiono un archivo JSON que podría no existir?
Detecta el error de lectura por separado del error de análisis. En Node, un código de error ENOENT significa que el archivo no existe, lo que normalmente requiere un valor por defecto en lugar de un error, mientras que un SyntaxError significa que el archivo existe pero su contenido es incorrecto.
Conclusión
El método depende del entorno, y la solución actual es más uniforme de lo que solía ser. «import data from "./data.json" with { type: "json" }» funciona tanto en Node como en los navegadores, es «Baseline» a partir de 2025 e incluye una comprobación del tipo MIME que existe por motivos de seguridad reales y no solo por formalidad.
Para los archivos que cambian mientras se ejecuta el programa, léelos explícitamente: readFile con una codificación "utf8" en Node, fetch con una comprobación res.ok en un navegador, y File.text() para algo que haya elegido el usuario. Esa comprobación res.ok es la línea de mayor valor de este artículo, ya que omitirla es la causa directa del error JSON más común que existe.
Cuando algo falle, registra los primeros 200 caracteres de la entrada antes de tocar ningún código. «Unexpected token '<'» significa HTML, «Unexpected end of JSON input» significa que está vacío o truncado, y ambos se resuelven analizando lo que realmente ha llegado, en lugar de basarse en conjeturas sobre el analizador sintáctico.
Y si los archivos se están volviendo demasiado grandes, la solución estructural es utilizar JSON delimitado por saltos de línea en lugar de recurrir a un equipo más potente. Un documento por línea se procesa en memoria constante, se añade de forma segura y sobrevive a una escritura interrumpida con todos los registros completos intactos.
