JSON Toolkit

Sequências de texto JSON (RFC 7464): streaming JSON delimitado por RS

Resumo: A RFC 7464 usa um separador 0x1E para transmitir textos JSON que você pode ler e gravar ao mesmo tempo. Cobre as regras, como difere do NDJSON e uma implementação de dividir e depois analisar.

Intermediário~9 min de leituraPublicado em 2026-10-31Atualizado em 2026-10-31
Capa de Sequências de texto JSON e o separador de registro da RFC 7464

Resumo da ópera: use JSON-seq quando você controla as duas pontas, precisa de um tipo de mídia registrado e quer que o leitor se ressincronize após truncamento; use NDJSON quando os dados alimentam sistemas de log, precisam ser legíveis por humanos ou têm de funcionar com ferramentas existentes — nunca force JSON-seq em um par que só entende entrada delimitada por linha.

O NDJSON divide registros por quebras de linha, mas uma quebra de linha é uma convenção humana. A RFC 7464 escreve a fronteira no próprio fluxo de bytes: um invisível 0x1E antes de cada registro.

1. O problema resolvido por um único byte

A RFC 7464 foi publicada em fevereiro de 2015 sob o título JSON Text Sequences, e registra o tipo de mídia application/json-seq. Ela não acrescenta nenhum tipo de dado novo. Apenas define como uma sequência de textos JSON é organizada dentro de um fluxo de bytes. A regra inteira cabe em uma frase: coloque um RS antes de cada texto JSON e um LF depois dele.

  • RS é o separador de registro ASCII, ponto de código 0x1E, conhecido no Unicode como INFORMATION SEPARATOR TWO (U+001E);
  • LF é a quebra de linha, 0x0A.

Dois registros ficam assim no nível de bytes:

1E 7B 22 69 64 22 3A 31 7D 0A      ← 0x1E {"id":1} + LF
1E 7B 22 69 64 22 3A 32 7D 0A      ← 0x1E {"id":2} + LF

O RS fica antes do registro, o que é o oposto do layout do NDJSON de "escreva o conteúdo e depois o delimitador". Essa direção muda o comportamento de recuperação: um leitor que vê 0x1E sabe que o registro anterior terminou, então mesmo um registro truncado no meio da gravação não consegue borrar a fronteira do próximo.

Este não é um design teórico. A RFC menciona explicitamente registros anexados a arquivos de log que são truncados pelo sistema de arquivos após uma queda ou um encerramento administrativo, e diz que um analisador não deve abortar a leitura inteira por causa disso.

2. Dois conjuntos de regras: codificadores e analisadores

A RFC 7464 fornece, de forma incomum, duas gramáticas ABNF, uma que restringe os escritores e outra que relaxa os leitores.

Papel Gramática Significado
Codificador *(RS JSON-text LF) Um RS antes de cada texto JSON, um LF depois dele
Analisador *(1*RS possible-JSON) Bytes RS consecutivos são permitidos, e o LF entre registros é opcional

Em termos de engenharia, há cinco regras rígidas:

  1. UTF-8 é obrigatório; UTF-16 e UTF-32 não devem ser usados;
  2. O analisador trata "qualquer byte diferente de RS" como um possível texto JSON e tenta analisá-lo; em caso de falha, pula para o próximo RS e continua;
  3. Bytes RS consecutivos não representam elementos vazios e são ignorados;
  4. Não há marcador de fim de sequência, então o fluxo simplesmente termina;
  5. Quando o valor de nível superior é um número, true, false ou null, o registro precisa terminar com um byte de espaço em branco, caso contrário é descartado como possivelmente truncado.

A quinta regra é a que as pessoas esquecem. Objetos, arrays e strings são auto-delimitados em JSON; números não são. <RS>123<RS> não consegue dizer se contém um 123 completo ou um 1234 truncado. A RFC portanto exige que os escritores acrescentem um LF, usando-o como byte canário para detecção de truncamento. Do mesmo modo, <RS>true<RS> pode originalmente ter sido trueish.

3. Escolhendo entre JSON-seq e NDJSON

Os dois formatos fazem streaming, mas miram problemas diferentes:

Dimensão JSON Text Sequences NDJSON / JSON Lines
Posição do delimitador RS (0x1E) antes do registro LF (0x0A) depois do registro
Tipo de mídia application/json-seq, registrado na IANA Sem registro oficial, convenção da comunidade
Legibilidade em bytes Caractere de controle invisível, cat não mostra nada Texto puro, diretamente legível
Anexações concorrentes O leitor consegue ressincronizar e descartar uma cauda truncada Anexações também funcionam, linhas ruins são ignoradas
Números de nível superior Precisa de um byte de espaço em branco final para ser confiável A quebra de linha é a fronteira
Delimitador dentro de strings RS só pode aparecer como \u001e LF precisa ser escrito como \n
Ferramentas jq --seq e algumas bibliotecas Quase toda ferramenta de log e big data
Extensão de arquivo Não definida pela RFC .jsonl / .ndjson

A decisão é curta: use JSON-seq quando você controla as duas pontas, quer um tipo de mídia formal e precisa que o leitor se ressincronize após truncamento; use JSON Lines e NDJSON quando os dados alimentam sistemas de log, precisam ser legíveis por humanos ou têm de funcionar com ferramentas existentes.

4. Um analisador: divida em 0x1E e depois use JSON.parse

O benefício mais tangível da RFC 7464 é que você não precisa de um analisador JSON de streaming. Divida por byte e depois chame JSON.parse em cada segmento:

/** Separador de registro ASCII, definido pela RFC 7464 como 0x1E */
const RECORD_SEPARATOR = 0x1e;

/**
 * Analisa um chunk de bytes de JSON Text Sequence.
 * @param buffer Bytes lidos até agora
 * @returns Registros analisados com sucesso e o número de registros com falha
 */
function parseJsonSequence(buffer: Buffer): { records: unknown[]; badCount: number } {
  const records: unknown[] = [];
  let badCount = 0;
  let recordStart = buffer.indexOf(RECORD_SEPARATOR);

  while (recordStart !== -1) {
    const nextRecordStart = buffer.indexOf(RECORD_SEPARATOR, recordStart + 1);
    const recordEnd = nextRecordStart === -1 ? buffer.length : nextRecordStart;
    const rawText = buffer.subarray(recordStart + 1, recordEnd).toString('utf8').trim();

    if (rawText !== '') {
      try {
        records.push(JSON.parse(rawText));
      } catch {
        badCount += 1;
      }
    }

    recordStart = nextRecordStart;
  }

  return { records, badCount };
}

Três notas de implementação:

  • A divisão no nível de bytes é segura. Bytes de continuação UTF-8 ficam na faixa 0x80–0xBF, então nunca podem ser iguais a 0x1E. Dividir em 0x1E não consegue cortar um caractere multibyte ao meio.
  • Armazene a cauda em buffer no streaming. Ao ler em chunks, o segmento final pode ainda não ter alcançado o próximo RS. Mantenha-o no buffer até o próximo chunk chegar; caso contrário você analisará o mesmo meio-registro duas vezes.
  • Conte falhas em vez de abortar. A RFC diz aos analisadores para pular para o próximo RS e continuar, então badCount é uma métrica de alerta, não uma exceção. Um registro final truncado também pertence a esse balde.

O uso de memória depende do maior registro individual mais um buffer, não do tamanho total do arquivo. É a mesma ideia discutida em Análise e streaming de JSON grande.

5. Caracteres de controle dentro de strings

O JSON exige que todo caractere de controle de U+0000 a U+001F seja escapado dentro de uma string, e RS cai nessa faixa. Uma string JSON válida pode portanto conter apenas os cinco caracteres \u001e, nunca um byte 0x1E cru. É por isso que dividir em 0x1E é sempre seguro para dados válidos.

O perigo vive do lado do escritor:

  1. Montar texto JSON à mão com '\x1e', inserindo um caractere de controle cru em uma string;
  2. Construir JSON com template literals em vez de JSON.stringify, deixando quebras de linha, aspas e caracteres de controle sem escape;
  3. Copiar texto que contém RS de um terminal ou editor, onde o caractere de controle é engolido ou substituído.

O padrão correto é sempre gerar o texto primeiro e depois anexar o delimitador:

const recordText = JSON.stringify(record);
const line = `${String.fromCharCode(RECORD_SEPARATOR)}${recordText}\n`;

JSON.stringify emite caracteres de controle como \u00XX, então um RS cru nunca pode aparecer dentro do registro. Uma vez que essa etapa está certa, a lógica de divisão só precisa lidar com bytes e nunca precisa entender a sintaxe JSON.

A maioria das falhas desta categoria aparece como JSON.parse lançando Unexpected token, especialmente o caso confuso em que "o primeiro caractere é um byte de controle invisível". A tabela de consulta de mensagens de erro JSON percorre essa classe de erro linha a linha.

6. Transporte HTTP e o tipo de mídia

application/json-seq é um tipo de mídia registrado sem parâmetros obrigatórios ou opcionais, e o conjunto de caracteres é fixo em UTF-8. No HTTP, três práticas importam:

  • Envie Accept: application/json-seq e responda com Content-Type: application/json-seq;
  • Para fazer streaming de verdade, o servidor precisa usar transferência chunked e desativar o buffering de resposta em intermediários como proxies reversos, CDNs e ferramentas de teste de carga;
  • Um cliente que lê chunk por chunk pode receber um chunk que termina no meio de um registro, então o analisador precisa armazenar em buffer até o próximo RS antes de emitir um registro.

Se a outra ponta só entende entrada delimitada por linha, não force JSON-seq nela. É um padrão, mas sua penetração no ecossistema é bem menor que a do NDJSON, então a compatibilidade deve ser decidida durante a negociação do protocolo e não depois de uma falha de análise.

7. Linha de comando, logs e suporte de ferramentas

O jq é uma das poucas ferramentas com suporte nativo; sua flag --seq existe exatamente para a RFC 7464:

printf '\x1e{"id":1}\n\x1e{"id":2}\n' | jq --seq -c '.id'

Isso imprime 1 e 2 em linhas separadas. Para um endpoint de streaming, lembre-se de desativar o buffering do curl:

curl -sN https://api.example.com/events | jq --seq -c '.type'

jq --seq lê entrada da RFC 7464, ignora registros ruins e continua analisando. Isso é especialmente útil ao acompanhar um arquivo de log que ainda está sendo escrito: o último registro pode estar pela metade, e a saída não mostrará um objeto final falso.

7.1 Suporte nativo por ambiente

Ambiente Suporte embutido Observações
jq --seq A opção de linha de comando mais fácil
Node.js Nenhum Buffer.indexOf mais JSON.parse
Python Nenhum Divida em b'\x1e'
Go Nenhum SplitFunc personalizado para bufio.Scanner
Navegador Nenhum fetch mais ReadableStream, divida os bytes manualmente

7.2 Armadilhas comuns

Os erros se concentram do lado da leitura:

  1. Ler com readline, o que deixa o prefixo RS invisível na primeira linha e faz JSON.parse falhar sempre;
  2. Abrir um arquivo .json-seq em um editor onde caracteres de controle são invisíveis, então ele parece um documento JSON bizarro;
  3. Dividir com split(/\n/), que confunde quebras de linha escapadas dentro de um registro com o LF entre registros;
  4. Tratar RS como um caractere comum na documentação, então ele desaparece após copiar e colar.

8. Quando vale a pena usar

Bons encaixes:

  • Você controla tanto o produtor quanto o consumidor, sem ferramentas de terceiros no meio;
  • Os dados são um fluxo de eventos anexado continuamente e o processo pode cair, então você precisa de recuperação de truncamento;
  • O protocolo merece um tipo de mídia formal em vez de uma convenção como .jsonl;
  • Registros individuais podem ser números ou booleanos de nível superior, onde uma regra explícita de truncamento importa.

Maus encaixes:

  • Os logs precisam ir para ELK, Loki ou outro sistema que só entende entrada delimitada por linha;
  • As pessoas precisam de cat, grep e awk diretamente nos dados;
  • A outra ponta só consegue analisar application/json.

9. Lista de verificação

  • [ ] O escritor gera registros com JSON.stringify e depois acrescenta 0x1E no início e \n no fim;
  • [ ] Registros contendo números, booleanos ou null de nível superior terminam com LF;
  • [ ] O leitor divide por bytes em vez de ler linha a linha;
  • [ ] Leituras em streaming mantêm uma cauda não fechada no buffer até o próximo RS chegar;
  • [ ] Falhas de análise são contadas e ignoradas, nunca fatais para a sequência;
  • [ ] Interfaces públicas declaram application/json-seq explicitamente e mantêm um fallback NDJSON.

Perguntas frequentes

Devo adotar JSON-seq para um endpoint de API público?

Só quando as duas pontas podem ser alteradas ou o formato é negociado de antemão. JSON-seq é um padrão registrado com o tipo de mídia application/json-seq, mas sua penetração no ecossistema é bem menor que a do NDJSON, então forçá-lo em um cliente de terceiros normalmente falha. Decida a compatibilidade durante a negociação do protocolo e mantenha um fallback NDJSON.

Devo converter um pipeline NDJSON existente para JSON-seq?

Normalmente não. O NDJSON já faz streaming, anexação e ignora linhas ruins, e quase toda ferramenta de log e big data o aceita. Converta apenas quando precisar do tipo de mídia registrado na IANA ou da ressincronização que um delimitador inicial proporciona, e apenas quando produtor e consumidor puderem ser alterados juntos.

Qual é mais comum, JSON Text Sequences ou NDJSON?

NDJSON. Suas regras são mais simples, sua cobertura de ferramentas é bem maior, e sistemas de log e pipelines de big data geralmente o aceitam. JSON-seq ganha no tipo de mídia registrado na IANA e na ressincronização que vem de colocar o delimitador primeiro, o que convém a protocolos de streaming controlados em vez de troca geral de dados.

Por que o delimitador fica antes do registro e não depois?

Porque registros podem ser truncados. Com um delimitador inicial, um leitor que vê RS sabe que um novo registro começou, então o registro anterior não consegue corromper o próximo, seja completo ou não. Com um delimitador final, uma gravação interrompida deixa um segmento final que não tem marcador de fim nem marcador de início seguinte, então a fronteira só pode ser adivinhada.

O que acontece se 0x1E aparecer dentro de um registro?

Uma string JSON válida precisa escapar RS como \u001e, então um 0x1E cru nunca deveria aparecer. Se aparecer, o texto não é mais JSON válido: dividir em 0x1E quebrará no lugar errado e JSON.parse falhará. A correção pertence ao lado do escritor: use JSON.stringify em vez de texto montado à mão.

Um navegador consegue analisar application/json-seq diretamente?

Não. Navegadores não trazem analisador de JSON-seq; fetch apenas entrega os bytes. Você precisa usar response.body.getReader(), acumular chunks até o próximo 0x1E e chamar JSON.parse em cada segmento. Para payloads pequenos, ler o corpo inteiro como string primeiro e dividir depois também funciona.

Por que um número de nível superior precisa de um LF final?

Objetos, arrays e strings são auto-delimitados em JSON, enquanto números não são: <RS>123<RS> pode ser um 123 completo ou um 1234 truncado. A RFC 7464 portanto exige um byte de espaço em branco após cada registro, e o LF age como canário para detecção de truncamento. A mesma regra se aplica a true, false e null de nível superior.