JSON Lines e NDJSON: dados de streaming
Resumo: Um valor JSON por linha compra a possibilidade de anexar, streaming e dano isolado a uma única linha; o custo é a ausência de estrutura geral, então a validação precisa acontecer linha a linha.

Resumo da ópera: use um array JSON quando você processa um lote inteiro de uma vez, e JSON Lines quando precisa de anexações em streaming e isolamento de linha ruim; se ainda precisa validar a estrutura geral, não mude para um formato delimitado por linha.
Uma vez que os dados excedem a memória ou continuam chegando continuamente, a estrutura de "um grande array" se torna o inimigo.
1. Regras do formato
JSON Lines (.jsonl) e NDJSON (.ndjson) são praticamente sinônimos, e as regras são curtas nos dois casos:
- A codificação é UTF-8, sem BOM;
- Cada linha contém um valor JSON completo e válido;
- As linhas são separadas por
\n; a última linha pode ou não terminar com uma quebra de linha; - Nenhuma quebra de linha não escapada pode aparecer dentro de uma linha — uma quebra de linha dentro de uma string precisa ser escrita como
\n; - Linhas vazias costumam ser ignoradas, mas uma implementação estrita deve tratá-las 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"}
A diferença sutil entre os dois: o site do JSON Lines enfatiza "um valor por linha, e o valor pode ser qualquer JSON (incluindo escalares)", enquanto a especificação NDJSON acrescenta regras de implementação mais estritas, como "sem linhas vazias e sem escalares no nível superior". Na engenharia, seguir o conjunto mais estrito mantém você fora de encrencas.
2. Comparação com um array JSON
| Dimensão | Array JSON | JSON Lines |
|---|---|---|
| Anexar um registro | Reescreve o arquivo inteiro (mudar o ] de fechamento) |
Basta anexar uma linha |
| Leitura em streaming | Precisa de um analisador que acompanhe a profundidade de colchetes | Dividir por linha |
| Uma entrada corrompida | O arquivo inteiro fica não analisável | Apenas aquela linha é afetada |
| Validação do arquivo inteiro | Valida tudo em uma passada | Apenas linha a linha |
| Compressão | Tamanho semelhante após gzip | Igual, e os blocos podem ser comprimidos em paralelo |
| Expressar aninhamento | Suportado nativamente | Cada linha pode aninhar internamente; as linhas não têm relação entre si |
| Consumidores típicos | Todas as ferramentas JSON | Sistemas de log, pipelines de big data, conjuntos de treinamento de LLM |
A conclusão é direta: se os dados são "um lote de registros" ou "um único documento" decide qual formato usar.
3. Leitura: streaming linha a linha
No Node, a abordagem mais simples é readline:
import { createReadStream } from 'node:fs';
import { createInterface } from 'node:readline';
/**
* Lê um arquivo JSON Lines linha a linha e invoca um callback para cada registro.
* @param filePath Caminho do arquivo
* @param onRecord Handler para um único registro
* @returns Estatísticas contendo o número de linhas com sucesso e com falha
*/
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 };
}
O uso de memória desse código depende apenas do tamanho de uma única linha, não do tamanho total do arquivo — esse é o maior valor de um formato delimitado por linha.
3.1 Quando você só precisa de alguns campos, não chame JSON.parse primeiro
Se cada registro tem 50 campos e você só precisa de 3, JSON.parse constrói objetos para todos os 50.
Um analisador de streaming (stream-json, clarinet) consegue extrair apenas os campos alvo, ao custo de maior complexidade de implementação.
A regra geral: quando um único registro tem menos de algumas centenas de KB, chamar JSON.parse diretamente normalmente ainda é o melhor negócio.
3.2 Texto em chinês e BOMs
Arquivos .jsonl gerados no Windows frequentemente carregam um BOM, e JSON.parse na primeira linha falha com certeza. Remova o BOM antes de ler, ou inspecione os três primeiros bytes ao transmitir o primeiro chunk.
4. Escrita: anexações e atomicidade
import { appendFile } from 'node:fs/promises';
/**
* Anexa um único registro JSON Lines.
* @param filePath Arquivo de destino
* @param record O objeto de registro a gravar
* @returns Uma promessa que resolve quando a gravação termina
*/
async function appendRecord(filePath: string, record: unknown): Promise<void> {
const line = `${JSON.stringify(record)}\n`;
await appendFile(filePath, line, { encoding: 'utf8' });
}
Duas coisas a observar:
- A saída de
JSON.stringifynão contém quebras de linha (a menos que o\nde uma string seja escapado em dois caracteres), então uma linha é sempre exatamente um registro; - Quando vários processos anexam concorrentemente, um único
writemenor que PIPE_BUF (normalmente 4096 bytes no Linux) é atômico; qualquer coisa maior não é garantida. Para cenários de alta concorrência, mude para "um arquivo por processo, mesclado após descarregar em disco", ou use armazenamento com transações.
5. Tratamento de linhas ruins
Esta é a decisão que um formato delimitado por linha força você a tomar de antemão:
| Estratégia | Adequada para | Custo |
|---|---|---|
| Ignorar e contar | Dados que toleram lacunas, como logs e eventos de análise | Os dados se perdem silenciosamente, então alertas são obrigatórios |
| Falhar imediatamente | Dados que não podem ser perdidos, como cobrança e faturas | Uma linha ruim bloqueia o lote inteiro |
| Encaminhar para um arquivo de letras mortas | Casos que precisam de recarga posterior | Armazenamento extra e um processo de replay |
Qualquer que seja a escolha, registre o número da linha e um fragmento do conteúdo original, caso contrário você não consegue localizar o problema depois. O número da linha não está presente na mensagem de erro do JSON.parse, então você precisa mantê-lo por conta própria.
6. Compressão e fragmentação
- O gzip comprime
.jsonlcom uma taxa consistentemente alta (nomes de campo se repetem), então.jsonl.gzé recomendado; - O gzip comprime em blocos e suporta nativamente descompressão em streaming, então não é preciso descomprimir o arquivo inteiro;
- Divida arquivos grandes em fatias de cerca de
1GB, com números de sequência nos nomes, para facilitar processamento paralelo e transferência retomável; - Nunca pré-processe
.jsonl"mesclando-o em um array" — isso joga fora todas as suas vantagens.
7. Trade-offs em relação a outros formatos de big data
| Formato | Estrutura | Schema incluído | Bom para |
|---|---|---|---|
| JSON Lines | Independente por linha | Não (precisa de um contrato externo) | Logs, importação e exportação, dados de treinamento |
| CSV | Tabular | Não | Relatórios, intercâmbio com Excel |
| Parquet | Colunar | Sim | Consultas analíticas, conjuntos offline muito grandes |
| Avro | Baseado em linha | Sim | Mensagens Kafka, evolução de schema |
A vantagem do JSON Lines é que qualquer linguagem e qualquer ferramenta conseguem lê-lo e gravá-lo imediatamente; a desvantagem é a falta de schema e de compressão colunar. Quando os dados já não cabem em uma única máquina e você precisa de consultas colunares frequentes, é hora de considerar Parquet.
8. Lista de verificação
- [ ] A extensão do arquivo é consistente (
.jsonlou.ndjson) e nunca misturada; - [ ] O escritor garante "um registro por linha", com quebras de linha dentro de strings já escapadas;
- [ ] O leitor remove o BOM, ignora linhas vazias e mantém números de linha;
- [ ] A estratégia para linhas ruins está explicitamente definida e monitorada (linhas ruins / total de linhas);
- [ ] Arquivos grandes são fatiados por tamanho e combinados com gzip e leitura paralela;
- [ ] Onde existe um contrato de campos, um schema adicional é fornecido e validado linha a linha.
Perguntas frequentes
Como escolho entre um array JSON e JSON Lines?
Veja se os dados são um lote de registros ou um único documento. Escolha JSON Lines quando os registros são anexados e lidos como fluxo: anexar significa escrever uma linha no fim, e ler só exige dividir por linha. Mantenha um array JSON quando toda a estrutura precisa ser validada em uma passada, porque um array JSON valida tudo de uma vez enquanto JSON Lines só pode ser validado linha a linha.
Por que uma linha corrompida não quebra o arquivo inteiro?
Cada linha em JSON Lines é um valor JSON independente e completo, e a análise é naturalmente linha a linha, então dados corrompidos afetam apenas aquela linha. Em um array JSON, um erro de sintaxe deixa o arquivo inteiro não analisável, que é exatamente por que dados delimitados por linha convêm a logs e eventos de análise.
Devo converter JSON Lines em um array JSON?
Você pode: leia linha a linha, chame JSON.parse em cada registro e colete-os em um array. Isso exige que todo o conteúdo caiba na memória e abre mão de leitura em streaming e anexações de uma linha. Pré-processar .jsonl em um array joga fora todas as vantagens do formato, então só é razoável para uma entrega pontual em que os dados cabem na memória.