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.

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:
- UTF-8 é obrigatório; UTF-16 e UTF-32 não devem ser usados;
- 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;
- Bytes RS consecutivos não representam elementos vazios e são ignorados;
- Não há marcador de fim de sequência, então o fluxo simplesmente termina;
- Quando o valor de nível superior é um número,
true,falseounull, 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 a0x1E. 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:
- Montar texto JSON à mão com
'\x1e', inserindo um caractere de controle cru em uma string; - Construir JSON com template literals em vez de
JSON.stringify, deixando quebras de linha, aspas e caracteres de controle sem escape; - 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-seqe responda comContent-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:
- Ler com
readline, o que deixa o prefixo RS invisível na primeira linha e fazJSON.parsefalhar sempre; - Abrir um arquivo
.json-seqem um editor onde caracteres de controle são invisíveis, então ele parece um documento JSON bizarro; - Dividir com
split(/\n/), que confunde quebras de linha escapadas dentro de um registro com o LF entre registros; - 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,grepeawkdiretamente nos dados; - A outra ponta só consegue analisar
application/json.
9. Lista de verificação
- [ ] O escritor gera registros com
JSON.stringifye depois acrescenta0x1Eno início e\nno fim; - [ ] Registros contendo números, booleanos ou
nullde 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-seqexplicitamente 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.