JSON Toolkit

JSON Lines y NDJSON: datos en streaming

En resumen: Un valor JSON por línea te compra capacidad de añadido, streaming y daños aislados en una sola línea; el coste es la ausencia de estructura global, así que la validación tiene que hacerse línea a línea.

Intermedio~10 min de lecturaPublicado el 2026-10-30Actualizado el 2026-10-30
Portada de JSON Lines y NDJSON para datos en streaming

En resumen: usa un array JSON cuando proceses un lote completo de una vez, y JSON Lines cuando necesites añadidos en streaming y aislamiento de líneas malas; si aún necesitas validar la estructura global, no cambies a un formato delimitado por líneas.

Una vez que los datos superan la memoria, o siguen llegando de forma continua, la estructura de "un gran array" se convierte en el enemigo.

1. Reglas del formato

JSON Lines (.jsonl) y NDJSON (.ndjson) son en la práctica sinónimos, y en ambos casos las reglas son breves:

  1. La codificación es UTF-8, sin BOM;
  2. Cada línea contiene un valor JSON completo y válido;
  3. Las líneas se separan con \n; la línea final puede terminar o no con un salto de línea;
  4. No puede aparecer ningún salto de línea sin escapar dentro de una línea: un salto de línea dentro de una cadena debe escribirse como \n;
  5. Las líneas vacías normalmente se ignoran, pero una implementación estricta debería tratarlas como inválidas.
{"time":"2026-10-30T10:41:45+08:00","level":"info","message":"boot"}
{"time":"2026-10-30T10:41:46+08:00","level":"warn","message":"slow query","costMs":812}
{"time":"2026-10-30T10:41:47+08:00","level":"error","message":"timeout","traceId":"a1b2"}

La diferencia sutil entre ambos: el sitio web de JSON Lines insiste en "un valor por línea, y el valor puede ser cualquier JSON (incluidos escalares)", mientras que la especificación NDJSON añade reglas de implementación más estrictas como "sin líneas vacías y sin escalares en el nivel superior". En ingeniería, seguir el conjunto más estricto te mantiene fuera de problemas.

2. Comparación con un array JSON

Dimensión Array JSON JSON Lines
Añadir un registro Reescribir todo el archivo (cambiar el ] de cierre) Basta con añadir una línea
Lectura en streaming Necesita un parser de streaming que siga la profundidad de corchetes Dividir por líneas
Una entrada corrupta Todo el archivo se vuelve imposible de parsear Solo se ve afectada esa línea
Validación del archivo completo Validar todo en una sola pasada Solo línea a línea
Compresión Tamaño similar tras gzip Igual, y los bloques se pueden comprimir en paralelo
Expresar anidamiento Soportado de forma nativa Cada línea puede anidar internamente; las líneas no se relacionan entre sí
Consumidores típicos Todas las herramientas JSON Sistemas de logs, pipelines de big data, conjuntos de entrenamiento de LLM

La conclusión es directa: si los datos son "un lote de registros" o "un documento" decide qué formato usar.

3. Lectura: streaming línea a línea

En Node, el enfoque más sencillo es readline:

import { createReadStream } from 'node:fs';
import { createInterface } from 'node:readline';

/**
 * Lee un archivo JSON Lines línea a línea e invoca un callback por cada registro.
 * @param filePath Ruta del archivo
 * @param onRecord Manejador de un único registro
 * @returns Estadísticas con el número de líneas correctas y fallidas
 */
async function readJsonLines(
  filePath: string,
  onRecord: (record: unknown, lineNumber: number) => void,
): Promise<{ okCount: number; badCount: number }> {
  const lineReader = createInterface({
    input: createReadStream(filePath, { encoding: 'utf8' }),
    crlfDelay: Infinity,
  });

  let okCount = 0;
  let badCount = 0;

  for await (const rawLine of lineReader) {
    const line = rawLine.trim();
    if (!line) {
      continue;
    }
    try {
      onRecord(JSON.parse(line), okCount + badCount + 1);
      okCount += 1;
    } catch {
      badCount += 1;
    }
  }

  return { okCount, badCount };
}

El uso de memoria de este código depende solo del tamaño de una única línea, no del tamaño total del archivo: ese es el mayor valor de un formato delimitado por líneas.

3.1 Cuando solo necesitas unos pocos campos, no llames primero a JSON.parse

Si cada registro tiene 50 campos y solo necesitas 3, JSON.parse construye objetos para los 50. Un parser de streaming (stream-json, clarinet) puede extraer solo los campos objetivo, a costa de una mayor complejidad de implementación. La regla práctica: cuando un registro individual tiene menos de unos cientos de KB, llamar directamente a JSON.parse suele seguir siendo mejor opción.

3.2 Texto en chino y BOM

Los archivos .jsonl generados en Windows a menudo llevan BOM, y JSON.parse en la primera línea fallará con seguridad. Elimina el BOM antes de leer, o inspecciona los tres primeros bytes mientras transmites el primer chunk.

4. Escritura: añadidos y atomicidad

import { appendFile } from 'node:fs/promises';

/**
 * Añade un único registro JSON Lines.
 * @param filePath Archivo de destino
 * @param record El objeto de registro que se va a escribir
 * @returns Una promesa que se resuelve cuando termina la escritura
 */
async function appendRecord(filePath: string, record: unknown): Promise<void> {
  const line = `${JSON.stringify(record)}\n`;
  await appendFile(filePath, line, { encoding: 'utf8' });
}

Dos cosas a tener en cuenta:

  1. La salida de JSON.stringify no contiene saltos de línea (salvo que el \n de una cadena se escape como dos caracteres), así que una línea es siempre exactamente un registro;
  2. Cuando varios procesos añaden de forma concurrente, una única write menor que PIPE_BUF (normalmente 4096 bytes en Linux) es atómica; cualquier cosa mayor no está garantizada. Para escenarios de alta concurrencia, cambia a "un archivo por proceso, fusionados después de volcarlos a disco", o usa almacenamiento con transacciones.

5. Gestión de líneas malas

Esta es la decisión que un formato delimitado por líneas te obliga a tomar por adelantado:

Estrategia Adecuada para Coste
Saltar y contar Datos que toleran huecos, como logs y eventos analíticos Los datos se pierden en silencio, así que la alerta es obligatoria
Fallar de inmediato Datos que no deben perderse, como facturación y facturas Una línea mala bloquea todo el lote
Derivar a un archivo de dead-letter Casos que necesitan rellenarse más tarde Almacenamiento extra y un proceso de reproducción

Elija lo que elijas, registra el número de línea y un fragmento del contenido original; de lo contrario no podrás localizar el problema después. El número de línea no aparece en el mensaje de error de JSON.parse, así que tienes que mantenerlo tú mismo.

6. Compresión y particionado

  • gzip comprime .jsonl con una ratio consistentemente alta (los nombres de campo se repiten), así que se recomienda .jsonl.gz;
  • gzip comprime por bloques y soporta de forma nativa la descompresión en streaming, así que no hace falta descomprimir todo el archivo;
  • Divide los archivos grandes en porciones de aproximadamente 1GB, con números de secuencia en los nombres, para facilitar el procesamiento en paralelo y la transferencia reanudable;
  • Nunca preproceses .jsonl "fusionándolo en un array": eso tira a la basura todas sus ventajas.

7. Compromisos frente a otros formatos de big data

Formato Estructura Incluye schema Bueno para
JSON Lines Independiente por línea No (necesita un contrato externo) Logs, importaciones y exportaciones, datos de entrenamiento
CSV Tabular No Informes, intercambio con Excel
Parquet Columnar Sí Consultas analíticas, conjuntos de datos offline muy grandes
Avro Basado en filas Sí Mensajes de Kafka, evolución de schema

La ventaja de JSON Lines es que cualquier lenguaje y cualquier herramienta puede leerlo y escribirlo de inmediato; la desventaja es la falta de schema y de compresión columnar. Una vez que los datos ya no caben en una sola máquina y necesitas consultas orientadas a columnas con frecuencia, es momento de considerar Parquet.

8. Lista de comprobación

  • [ ] La extensión del archivo es consistente (.jsonl o .ndjson) y nunca se mezcla;
  • [ ] El escritor garantiza "un registro por línea", con los saltos de línea dentro de cadenas ya escapados;
  • [ ] El lector elimina el BOM, salta las líneas vacías y mantiene los números de línea;
  • [ ] La estrategia de líneas malas está definida y monitorizada explícitamente (líneas malas / líneas totales);
  • [ ] Los archivos grandes se cortan por tamaño y se combinan con gzip y lecturas en paralelo;
  • [ ] Donde existe un contrato de campos, se proporciona un schema adicional y se valida línea a línea.

FAQ

¿Cómo elijo entre un array JSON y JSON Lines?

Mira si los datos son un lote de registros o un único documento. Elige JSON Lines cuando los registros se añaden y se leen como un stream: añadir significa escribir una línea al final, y leer solo requiere dividir por líneas. Mantén un array JSON cuando haya que validar toda la estructura en una sola pasada, porque un array JSON valida todo a la vez mientras que JSON Lines solo se puede validar línea a línea.

¿Por qué una línea corrupta no rompe todo el archivo?

Cada línea de JSON Lines es un valor JSON completo e independiente, y el parseo es naturalmente línea a línea, así que los datos corruptos afectan solo a esa línea. En un array JSON, un error de sintaxis vuelve todo el archivo imposible de parsear, que es exactamente por lo que los datos delimitados por líneas encajan con logs y eventos analíticos.

¿Debería convertir JSON Lines en un array JSON?

Puedes: leer línea a línea, llamar a JSON.parse en cada registro y recogerlos en un array. Hacerlo requiere que todo el contenido quepa en memoria y renuncia a las lecturas en streaming y a los añadidos de una línea. Preprocesar .jsonl en un array tira a la basura todas las ventajas del formato, así que solo es razonable para una entrega puntual en la que los datos caben en memoria.