JSON Toolkit

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.

Intermediário~10 min de leituraPublicado em 2026-10-30Atualizado em 2026-10-30
Capa de JSON Lines e NDJSON para dados de streaming

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:

  1. A codificação é UTF-8, sem BOM;
  2. Cada linha contém um valor JSON completo e válido;
  3. As linhas são separadas por \n; a última linha pode ou não terminar com uma quebra de linha;
  4. 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;
  5. 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:

  1. A saída de JSON.stringify não contém quebras de linha (a menos que o \n de uma string seja escapado em dois caracteres), então uma linha é sempre exatamente um registro;
  2. Quando vários processos anexam concorrentemente, um único write menor 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 .jsonl com 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 (.jsonl ou .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.