Secuencias de texto JSON (RFC 7464): JSON en streaming delimitado por RS
En resumen: RFC 7464 usa un separador 0x1E para transmitir textos JSON que se pueden leer y escribir a la vez. Cubre las reglas, en qué se diferencia de NDJSON y una implementación que divide y luego parsea.

En resumen: usa JSON-seq cuando controles ambos extremos, necesites un media type registrado y quieras que el lector se resincronice tras un truncamiento; usa NDJSON cuando los datos alimenten sistemas de logs, deban ser legibles por humanos o tengan que funcionar con herramientas existentes — nunca impongas JSON-seq a un par que solo entiende entrada delimitada por líneas.
NDJSON divide los registros por saltos de línea, pero un salto de línea es una convención humana. RFC 7464 escribe la frontera en el propio flujo de bytes: un 0x1E invisible antes de cada registro.
1. El problema que resuelve un solo byte
RFC 7464 se publicó en febrero de 2015 con el título JSON Text Sequences, y registra el media type application/json-seq. No añade nuevos tipos de datos. Solo define cómo se organiza una secuencia de textos JSON dentro de un flujo de bytes. Toda la regla cabe en una frase: pon un RS antes de cada texto JSON y un LF después.
- RS es el separador de registro ASCII, punto de código
0x1E, conocido en Unicode como INFORMATION SEPARATOR TWO (U+001E); - LF es el salto de línea,
0x0A.
Dos registros se ven así a nivel de byte:
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
El RS se coloca antes del registro, que es lo contrario del diseño de NDJSON de "escribe el contenido y luego el delimitador". Esa dirección cambia el comportamiento de recuperación: un lector que ve 0x1E sabe que el registro anterior ha terminado, así que incluso un registro truncado a mitad de escritura no puede difuminar la frontera del siguiente.
No es un diseño teórico. El RFC menciona explícitamente los registros añadidos a archivos de log que el sistema de archivos trunca tras un fallo o una terminación administrativa, y dice que un parser no debería abortar toda la lectura por ello.
2. Dos conjuntos de reglas: codificadores y parsers
RFC 7464 proporciona, de forma poco habitual, dos gramáticas ABNF, una que restringe a los escritores y otra que relaja a los lectores.
| Rol | Gramática | Significado |
|---|---|---|
| Codificador | *(RS JSON-text LF) |
Un RS antes de cada texto JSON, un LF después |
| Parser | *(1*RS possible-JSON) |
Se permiten bytes RS consecutivos, y el LF entre registros es opcional |
En términos de ingeniería, hay cinco reglas duras:
- UTF-8 es obligatorio; UTF-16 y UTF-32 no deben usarse;
- El parser trata "cualquier byte distinto de RS" como un posible texto JSON e intenta parsearlo; si falla, salta al siguiente RS y continúa;
- Los bytes RS consecutivos no representan elementos vacíos y se ignoran;
- No hay marcador de fin de secuencia, así que el flujo simplemente termina;
- Cuando el valor de nivel superior es un número,
true,falseonull, el registro debe terminar con un byte de espacio en blanco; de lo contrario se descarta como posiblemente truncado.
La quinta regla es la que la gente olvida. Los objetos, arrays y cadenas se autodelimitan en JSON; los números no. <RS>123<RS> no puede decirte si contiene un 123 completo o un 1234 truncado. Por eso el RFC exige a los escritores añadir un LF, usándolo como byte canario para detectar truncamientos. Del mismo modo, <RS>true<RS> podría haber sido originalmente trueish.
3. Elegir entre JSON-seq y NDJSON
Ambos formatos hacen streaming, pero apuntan a problemas distintos:
| Dimensión | Secuencias de texto JSON | NDJSON / JSON Lines |
|---|---|---|
| Posición del delimitador | RS (0x1E) antes del registro |
LF (0x0A) después del registro |
| Media type | application/json-seq, registrado en IANA |
Sin registro oficial, convención de la comunidad |
| Legibilidad del byte | Carácter de control invisible, cat no muestra nada |
Texto plano, legible directamente |
| Añadidos concurrentes | El lector puede resincronizar y descartar una cola truncada | Los añadidos también funcionan, las líneas malas se saltan |
| Números en el nivel superior | Necesitan un byte de espacio en blanco final para ser fiables | El salto de línea es la frontera |
| Delimitador dentro de cadenas | RS solo puede aparecer como \u001e |
LF debe escribirse como \n |
| Herramientas | jq --seq y algunas bibliotecas |
Casi cualquier herramienta de logs y big data |
| Extensión de archivo | No definida por el RFC | .jsonl / .ndjson |
La decisión es breve: usa JSON-seq cuando controles ambos extremos, quieras un media type formal y necesites que el lector se resincronice tras un truncamiento; usa JSON Lines y NDJSON cuando los datos alimenten sistemas de logs, deban ser legibles por humanos o tengan que funcionar con herramientas existentes.
4. Un parser: dividir por 0x1E y luego JSON.parse
El beneficio más tangible del RFC 7464 es que no necesitas un parser JSON de streaming. Divide por byte y luego llama a JSON.parse en cada segmento:
/** Separador de registro ASCII, definido por RFC 7464 como 0x1E */
const RECORD_SEPARATOR = 0x1e;
/**
* Parsea un fragmento de bytes de Secuencias de texto JSON.
* @param buffer Bytes leídos hasta el momento
* @returns Los registros parseados con éxito y el número de registros fallidos
*/
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 };
}
Tres notas de implementación:
- Dividir a nivel de byte es seguro. Los bytes de continuación de UTF-8 viven en el rango
0x80–0xBF, así que nunca pueden ser iguales a0x1E. Dividir por 0x1E no puede cortar un carácter multibyte por la mitad. - Bufferea la cola cuando hagas streaming. Al leer por chunks, el segmento final puede que aún no haya alcanzado el siguiente RS. Mantenlo en el buffer hasta que llegue el siguiente chunk; de lo contrario parsearás el mismo medio registro dos veces.
- Cuenta los fallos en lugar de abortar. El RFC indica a los parsers saltar al siguiente RS y continuar, así que
badCountes una métrica de advertencia, no una excepción. Un registro final truncado también pertenece a ese grupo.
El uso de memoria depende del registro individual más grande más un buffer, no del tamaño total del archivo. Es la misma idea que se trata en Parseo y streaming de JSON grande.
5. Caracteres de control dentro de cadenas
JSON exige que todos los caracteres de control de U+0000 a U+001F se escapen dentro de una cadena, y RS cae en ese rango. Por lo tanto, una cadena JSON válida solo puede contener los cinco caracteres \u001e, nunca un byte 0x1E en crudo. Por eso dividir por 0x1E siempre es seguro para datos válidos.
El peligro vive en el lado del escritor:
- Construir a mano el texto JSON con
'\x1e', insertando un carácter de control en crudo dentro de una cadena; - Construir JSON con template literals en lugar de
JSON.stringify, dejando saltos de línea, comillas y caracteres de control sin escapar; - Copiar texto que contiene RS desde una terminal o un editor, donde el carácter de control se traga o se reemplaza.
El patrón correcto es siempre generar primero el texto y luego adjuntar el delimitador:
const recordText = JSON.stringify(record);
const line = `${String.fromCharCode(RECORD_SEPARATOR)}${recordText}\n`;
JSON.stringify emite los caracteres de control como \u00XX, así que un RS en crudo nunca puede aparecer dentro del registro. Una vez que este paso es correcto, la lógica de división solo tiene que lidiar con bytes y nunca necesita entender la sintaxis de JSON.
La mayoría de los fallos de esta categoría se manifiestan como JSON.parse lanzando Unexpected token, especialmente el caso confuso en el que "el primer carácter es un byte de control invisible". La tabla de búsqueda de mensajes de error de JSON recorre ese tipo de error línea a línea.
6. Transporte HTTP y el media type
application/json-seq es un media type registrado sin parámetros obligatorios ni opcionales, y el conjunto de caracteres está fijado a UTF-8. En HTTP, tres prácticas importan:
- Envía
Accept: application/json-seqy responde conContent-Type: application/json-seq; - Para hacer streaming de verdad, el servidor debe usar transferencia chunked y desactivar el buffering de respuestas en intermediarios como reverse proxies, CDNs y herramientas de pruebas de carga;
- Un cliente que lee chunk a chunk puede recibir un chunk que termina en mitad de un registro, así que el parser debe bufferear hasta el siguiente RS antes de emitir un registro.
Si la otra parte solo entiende entrada delimitada por líneas, no le impongas JSON-seq. Es un estándar, pero su penetración en el ecosistema está muy por debajo de NDJSON, así que la compatibilidad debería decidirse durante la negociación del protocolo y no después de un fallo de parseo.
7. Línea de comandos, logs y soporte de herramientas
jq es una de las pocas herramientas con soporte nativo; su flag --seq existe precisamente para RFC 7464:
printf '\x1e{"id":1}\n\x1e{"id":2}\n' | jq --seq -c '.id'
Eso imprime 1 y 2 en líneas separadas. Para un endpoint en streaming, recuerda desactivar el buffering de curl:
curl -sN https://api.example.com/events | jq --seq -c '.type'
jq --seq lee entrada RFC 7464, ignora los registros malos y sigue parseando. Eso es especialmente útil al seguir un archivo de log que aún se está escribiendo: el último registro puede estar a medio escribir, y la salida no mostrará un objeto final falso.
7.1 Soporte nativo por entorno
| Entorno | Soporte integrado | Notas |
|---|---|---|
| jq | --seq |
La opción de línea de comandos más sencilla |
| Node.js | Ninguno | Buffer.indexOf más JSON.parse |
| Python | Ninguno | Dividir por b'\x1e' |
| Go | Ninguno | SplitFunc personalizado para bufio.Scanner |
| Navegador | Ninguno | fetch más ReadableStream, dividir bytes manualmente |
7.2 Errores comunes
Los errores se concentran en el lado de la lectura:
- Leer con
readline, que deja el prefijo RS invisible en la primera línea, así queJSON.parsesiempre falla; - Abrir un archivo
.json-seqen un editor donde los caracteres de control son invisibles, así que parece un único documento JSON extraño; - Dividir con
split(/\n/), que confunde los saltos de línea escapados dentro de un registro con el LF entre registros; - Tratar RS como un carácter ordinario en la documentación, así que desaparece tras copiar y pegar.
8. Cuándo merece la pena usarlo
Encajes adecuados:
- Eres dueño tanto del productor como del consumidor, sin herramientas de terceros en medio;
- Los datos son un flujo de eventos que se añade de forma continua y el proceso puede caerse, así que necesitas recuperación tras truncamientos;
- El protocolo merece un media type formal en lugar de una convención como
.jsonl; - Los registros individuales pueden ser números o booleanos en el nivel superior, donde una regla explícita de truncamiento importa.
Encajes pobres:
- Los logs deben ir a ELK, Loki u otro sistema que solo entiende entrada delimitada por líneas;
- Las personas necesitan hacer
cat,grepyawkdirectamente sobre los datos; - La otra parte solo puede parsear
application/json.
9. Lista de comprobación
- [ ] El escritor genera los registros con
JSON.stringify, luego antepone0x1Ey añade\n; - [ ] Los registros con números, booleanos o
nullen el nivel superior terminan con LF; - [ ] El lector divide por bytes en lugar de leer línea a línea;
- [ ] Las lecturas en streaming mantienen una cola sin cerrar en el buffer hasta que llega el siguiente RS;
- [ ] Los fallos de parseo se cuentan y se saltan, nunca son fatales para la secuencia;
- [ ] Las interfaces públicas declaran
application/json-seqexplícitamente y mantienen un fallback NDJSON.
FAQ
¿Debería adoptar JSON-seq para un endpoint de API público?
Solo cuando ambos extremos puedan cambiarse o el formato se negocie de antemano. JSON-seq es un estándar registrado con el media type application/json-seq, pero su penetración en el ecosistema está muy por debajo de NDJSON, así que imponerlo a un cliente de terceros normalmente fracasa. Decide la compatibilidad durante la negociación del protocolo y mantén un fallback NDJSON.
¿Debería convertir un pipeline NDJSON existente a JSON-seq?
Normalmente no. NDJSON ya hace streaming, añadidos y salto de líneas malas, y casi cualquier herramienta de logs y big data lo acepta. Convierte solo cuando necesites el media type registrado en IANA o la resincronización que proporciona un delimitador a la cabeza, y solo cuando el productor y el consumidor puedan cambiarse juntos.
¿Qué es más común, las secuencias de texto JSON o NDJSON?
NDJSON. Sus reglas son más simples, su cobertura de herramientas es mucho mayor y los sistemas de logs y los pipelines de big data generalmente lo aceptan. JSON-seq gana en el media type registrado en IANA y en la resincronización que viene de poner el delimitador al principio, lo que encaja con protocolos de streaming controlados más que con el intercambio general de datos.
¿Por qué se coloca el delimitador antes del registro en lugar de después?
Porque los registros pueden truncarse. Con un delimitador a la cabeza, un lector que ve RS sabe que ha empezado un nuevo registro, así que el registro anterior no puede corromper el siguiente, esté completo o no. Con un delimitador al final, una escritura interrumpida deja un segmento final que no tiene ni marcador de fin ni marcador de inicio posterior, así que la frontera solo se puede adivinar.
¿Qué ocurre si 0x1E aparece dentro de un registro?
Una cadena JSON válida debe escapar RS como \u001e, así que un 0x1E en crudo nunca debería aparecer. Si lo hace, el texto ya no es JSON válido: dividir por 0x1E se romperá en el lugar equivocado y JSON.parse fallará. La corrección pertenece al lado del escritor: usa JSON.stringify en lugar de texto construido a mano.
¿Puede un navegador parsear application/json-seq directamente?
No. Los navegadores no incluyen un parser JSON-seq; fetch solo entrega los bytes. Necesitas usar response.body.getReader(), acumular chunks hasta el siguiente 0x1E y llamar a JSON.parse en cada segmento. Para payloads pequeños, leer todo el cuerpo como cadena primero y dividir después también funciona.
¿Por qué un número en el nivel superior necesita un LF final?
Los objetos, arrays y cadenas se autodelimitan en JSON, mientras que los números no: <RS>123<RS> podría ser un 123 completo o un 1234 truncado. Por eso RFC 7464 exige un byte de espacio en blanco después de cada registro, y el LF actúa como canario para detectar truncamientos. La misma regla se aplica a true, false y null en el nivel superior.