JSONL en la práctica: abrir, convertir y depurar líneas defectuosas
En resumen: Entregar un archivo .jsonl completo a JSON.parse y recibir un error casi nunca significa que el archivo esté roto — significa que la estrategia de lectura está mal. Esta guía cubre las formas estándar de inspeccionar y leer JSONL línea a línea, los comandos para convertir de y hacia arrays JSON, una tabla de seis tipos de líneas defectuosas y la manera correcta de consumir NDJSON desde respuestas HTTP en streaming.

En esencia: cada línea de un archivo JSONL es un valor JSON válido e independiente, pero el archivo como conjunto no es un único documento JSON. Si entregar el archivo completo a JSON.parse o json.load falla, nueve de cada diez veces el archivo está bien — tu estrategia de lectura es la que está equivocada.
Un registro por línea, sin comas entre líneas, sin array externo — JSONL renuncia a la "estructura del documento completo" a cambio de escritura por adición, lectura en streaming y aislamiento de fallos línea a línea. Las tres ventajas solo se materializan cuando procesas el archivo línea a línea, y las tres se convierten en errores en el momento en que intentas analizarlo todo de una vez.
1. Tres nombres, un formato: JSONL, NDJSON, JSON Lines
Los principiantes suelen asumir que la diferencia de nombres implica una diferencia de sintaxis. No la implica:
| Nombre | Contexto típico | Extensión / media type |
|---|---|---|
| JSON Lines | Ingeniería de datos, datasets de ML (pandas, Spark, Hugging Face) | .jsonl |
| NDJSON | API HTTP en streaming, datos en tiempo real | .ndjson, application/x-ndjson |
| JSONL | Abreviatura informal de cualquiera de los dos | — |
Cualquier herramienta que lea uno lee el otro. El único criterio para elegir el nombre es la convención de tu ecosistema aguas abajo: escribe .jsonl al preparar datasets para Hugging Face, declara application/x-ndjson al exponer una API en streaming. Un archivo JSONL portable cumple tres reglas: codificación UTF-8 (sin BOM), un valor JSON completo por línea, líneas separadas por \n. Un salto de línea final al terminar el archivo es muy recomendable, y las líneas en blanco no son registros válidos. Los detalles del protocolo y la motivación del diseño quedan cubiertos en nuestra visión general de JSON Lines y NDJSON; este artículo se ocupa estrictamente de qué hacer cuando ya tienes el archivo.
2. Inspección: primero head, luego grep, el editor al final
Hacer doble clic en un .jsonl de 500 MB para abrirlo en un editor es el error más común: tres millones de líneas de texto más el resaltado de sintaxis más un búfer de deshacer congelarán prácticamente cualquier editor — y el JSON no tiene culpa. Analizar un documento de 45 MB lleva unos 208 ms en Node 22; lo que se cuelga es el renderizado. Los formatos orientados a línea están hechos para las herramientas de línea, porque los registros coinciden con las líneas físicas:
# Mide primero la escala: número de registros y tamaño del archivo, sin leer el contenido
wc -l events.jsonl && du -h events.jsonl
# Mira cómo es un registro (confirma campos y estructura)
head -n 1 events.jsonl | jq .
# Encuentra registros objetivo directamente: las herramientas de línea simplemente funcionan — un privilegio de JSONL
grep '"level":"error"' events.jsonl | head -n 5
tail -n 100 events.jsonl | jq 'select(.latency_ms > 500)'
Fíjate en el último comando: cada línea que sale de tail sigue siendo un valor JSON independiente, así que va directo a jq sin necesidad de reconstruir un array. Este enfoque funciona igual en archivos de varios gigabytes con una sobrecarga de memoria casi nula — es el mismo principio de por qué los archivos JSON grandes ralentizan las páginas.
3. Los tres patrones estándar de lectura línea a línea
La lectura línea a línea debe gestionar tres cosas: saltar líneas en blanco (especialmente el salto final), reportar errores con el número de línea y no dejar que una sola línea defectuosa mate todo el trabajo. Los patrones estándar de los tres lenguajes giran en torno a estos puntos.
Python, solo biblioteca estándar:
import json
with open("events.jsonl", encoding="utf-8") as f:
for line_number, line in enumerate(f, start=1):
if not line.strip(): # saltar líneas en blanco
continue
try:
record = json.loads(line) # analizar cada línea independientemente
except json.JSONDecodeError as e:
print(f"Línea {line_number} falló: {e}") # reportar, no abortar
continue # aislar la línea defectuosa
handle(record)
Node con el módulo readline integrado, memoria constante sin importar el tamaño del archivo:
import fs from "node:fs";
import readline from "node:readline";
const input = fs.createReadStream("events.jsonl", "utf8");
const lines = readline.createInterface({ input, crlfDelay: Infinity });
let lineNumber = 0;
for await (const line of lines) {
lineNumber += 1;
if (!line.trim()) continue;
try {
handle(JSON.parse(line));
} catch (e) {
console.error(`Línea ${lineNumber}: ${e.message}`);
}
}
En Go 1.27, encoding/json/v2 se convierte en el estándar por defecto, y su decodificador en streaming valor a valor encaja perfecto con JSONL; usa gojq si necesitas incrustar expresiones jq. En cuanto a bibliotecas, no corras a instalar paquetes: jsonlines en Python reporta el número y el texto crudo de toda línea que falle, y orjsonl lee y escribe directamente archivos comprimidos con gzip/bzip2/zstd; el paquete Node ndjson no publica versiones desde 2020 — el readline integrado basta.
4. Conversión de y hacia arrays JSON
La dirección importa; inviértela y todos los comandos saldrán mal. Recuerda una regla: array → JSONL es un map (cada registro se compacta en una línea), JSONL → array es un collect (las líneas se recogen en una lista).
# Array JSON → JSONL: -c compacta cada elemento en una línea
jq -c '.[]' records.json > records.jsonl
# JSONL → array JSON: -s recoge toda la entrada en un único array
jq -s '.' events.jsonl > records.json
# Filtrar JSONL línea a línea: no hace falta -s, jq procesa la entrada como flujo
jq -c 'select(.active == true)' events.jsonl > active.jsonl
Los one-liners de Python son equivalentes: array → JSONL escribe json.dumps(r, ensure_ascii=False) + "\n" por registro; el sentido inverso recoge los json.loads línea a línea en una lista. Cada dirección tiene su trampa. En la ida (array → JSONL), todo salto de línea real dentro de un registro debe estar escapado por json.dumps — si no, un registro se rasga en varias líneas. En la vuelta (JSONL → array), no concatenes las líneas en una cadena gigante para analizarla — con millones de líneas es una copia íntegra innecesaria en memoria. Las agregaciones (sumas, agrupaciones, medias) requieren -s, que carga todo; en archivos de varios GB pasa a awk o a un acumulador en streaming en Python.
5. Seis tipos de líneas defectuosas y cómo diagnosticarlas
Casi todo error de JSONL vive a nivel de línea. Validar línea a línea reportando números es el único diagnóstico fiable. Ordenadas por frecuencia:
| Síntoma | Causa raíz | Corrección |
|---|---|---|
Extra data al final de una línea |
Comas añadidas entre líneas | Elimínalas; cada línea debe valerse por sí sola |
| Un registro partido en varias líneas | Salto de línea real sin escapar dentro del registro (formateado a mano) | Re-compacta con json.dumps / jq -c |
La primera línea falla con Unexpected token ï |
El archivo tiene BOM | Guarda de nuevo en UTF-8 sin BOM |
| Solo la última línea da error aunque parece correcta | Falta el salto final o hay espacios sueltos | Añade \n; salta las líneas en blanco por convención |
| Error de salto de línea dentro de una cadena | Salto literal dentro de una cadena, no \n |
Arregla el generador; nunca concatenes cadenas JSON a mano |
| Enteros grandes cambian de valor | Análisis vía double (truncado en 2^53) | Usa un parser que preserve la precisión, ver precisión bigint |
La rutina de diagnóstico es siempre de tres pasos: head -n 1 para confirmar codificación y estructura de la primera línea; ejecutar un script línea a línea que reporte el número de cada línea defectuosa en vez de pararse en la primera; luego sacar las líneas defectuosas una a una — una línea es un registro completo, inspeccionarla aislada no cuesta nada. Para importaciones en lote, la política recomendada es "cuarentena, no aborto": escribe las líneas defectuosas con sus números en errors.jsonl, sigue cargando las buenas, y decide la reimportación según la proporción de líneas defectuosas.
6. JSONL sobre HTTP: consumo línea a línea y contrapresión
El patrón típico de NDJSON sobre HTTP es un servidor escribiendo continuamente y un cliente consumiendo continuamente. En el cliente, no llames a response.json() — almacena en búfer todo el cuerpo antes de analizarlo, lo que anula el sentido del streaming. Lee el flujo del cuerpo directamente, parte por \n y arrastra el fragmento final incompleto a la siguiente iteración:
const reader = response.body.pipeThrough(new TextDecoderStream()).getReader();
let buffer = "";
for (;;) {
const { value, done } = await reader.read();
if (done) break;
buffer += value;
const lines = buffer.split("\n");
buffer = lines.pop(); // el último trozo puede ser una línea incompleta
for (const line of lines) {
if (line.trim()) handle(JSON.parse(line));
}
}
En el servidor dos cosas son obligatorias: primero, cuando res.write() devuelve false, espera el evento drain (contrapresión), o la memoria crece sin límites cada vez que el consumidor es más lento que el productor; segundo, desactiva el middleware gzip en las rutas de streaming — gzip debe almacenar toda la respuesta antes de poder comprimir nada, lo que neutraliza por completo el streaming de NDJSON. Ambos comportamientos están confirmados por pruebas locales y varias fuentes independientes en inglés, no son conjeturas.
7. Cuándo JSONL es la elección equivocada
El espacio negativo de la elección de formato también importa. Tres escenarios deben quedarse con el array JSON:
| Requisito | Por qué JSONL encaja mal | Usa en su lugar |
|---|---|---|
| Validar la estructura del documento completo | No hay estructura global; la deriva de campos pasa desapercibida | Array JSON + Schema |
| Acceso aleatorio al registro N | Longitudes de línea variables; sin búsqueda binaria | SQLite / Parquet |
| Anidamiento profundo, referencias entre registros | Cada línea está aislada; las referencias no tienen dónde vivir | Documento JSON estándar |
Regla de bolsillo en una frase: si los registros llegan con el tiempo o se consumen línea a línea (logs, eventos, exportaciones, datasets de entrenamiento), usa JSONL; si un volumen pequeño se produce una vez, se consume una vez y necesita semántica de documento completo, usa un array JSON. Las variantes derivadas (JSON Text Sequences con delimitador RS) son otro tema — ver el artículo sobre RFC 7464.
FAQ
¿Existe una diferencia real entre JSONL y NDJSON?
En términos de ingeniería, no. Ambos significan "un valor JSON completo por línea, sin envoltorio externo, sin comas entre líneas", y cualquier herramienta que soporte uno soporta el otro. La diferencia es de nomenclatura y hábitos del ecosistema: .jsonl domina la ingeniería de datos y los datasets de ML; .ndjson y application/x-ndjson dominan las API HTTP en streaming. Al integrarte con un sistema externo, sigue la extensión y el media type que su documentación especifique.
¿Puedo leer un archivo .jsonl directamente con JSON.parse?
No el archivo entero. JSON.parse acepta exactamente un texto JSON, mientras que JSONL es una sucesión de valores independientes dispuestos línea a línea, así que analizarlo todo siempre falla con "Unexpected token" o "Extra data". Divide en líneas y analiza cada línea no vacía por separado, reportando el número de línea en caso de fallo.
Si una línea de un archivo JSONL está rota, ¿todo el archivo está arruinado?
No — el aislamiento de fallos línea a línea es el núcleo del diseño del formato. Una línea defectuosa solo perjudica a sí misma: sáltala, anota su número y sigue leyendo. Para importaciones en lote, pon las líneas defectuosas en cuarentena en un archivo aparte, carga los registros correctos con normalidad y decide la corrección y reimportación según la proporción de líneas defectuosas.
¿Cómo convierto JSONL a Excel o CSV?
Primero conviértelo a array o extrae los campos línea a línea, y luego ejecuta una conversión tabular: compacta con jq -c '.[]' data.jsonl y entrégalo a un conversor, o haz pasar cada línea por json.loads hacia pandas. Ojo: los campos anidados aplanados producen nombres de columna con puntos, los campos tipo array solo pueden serializarse como texto dentro de la celda, y las columnas de IDs largos conviene declararlas como cadenas desde el principio para evitar pérdida de precisión.