Séquences de textes JSON (RFC 7464) : du JSON en flux délimité par RS
L’essentiel : La RFC 7464 utilise un séparateur 0x1E pour diffuser des textes JSON que l'on peut lire et écrire au fil de l'eau. Règles, différences avec NDJSON et implémentation « découper puis analyser ».

L'essentiel : utilisez JSON-seq lorsque vous maîtrisez les deux extrémités, avez besoin d'un type de média enregistré et voulez que le lecteur se resynchronise après une troncature ; utilisez NDJSON quand les données alimentent des systèmes de journaux, doivent être lisibles par un humain ou doivent fonctionner avec les outils existants — n'imposez jamais JSON-seq à un pair qui ne comprend que l'entrée délimitée par lignes.
NDJSON découpe les enregistrements sur les sauts de ligne, mais un saut de ligne est une convention humaine. La RFC 7464 inscrit la frontière dans le flux d'octets lui-même : un 0x1E invisible avant chaque enregistrement.
1. Le problème résolu par un seul octet
La RFC 7464 a été publiée en février 2015 sous le titre JSON Text Sequences, et elle enregistre le type de média application/json-seq. Elle n'ajoute aucun nouveau type de données. Elle définit seulement la manière dont une séquence de textes JSON est disposée dans un flux d'octets. Toute la règle tient en une phrase : placer un RS avant chaque texte JSON et un LF après.
- RS est le séparateur d'enregistrement ASCII, point de code
0x1E, connu en Unicode sous le nom INFORMATION SEPARATOR TWO (U+001E) ; - LF est le saut de ligne,
0x0A.
Deux enregistrements se présentent ainsi au niveau des octets :
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
Le RS se trouve avant l'enregistrement, soit l'inverse de la disposition de NDJSON qui « écrit le contenu, puis écrit le délimiteur ». Cette direction change le comportement de récupération : un lecteur qui voit 0x1E sait que l'enregistrement précédent est terminé, si bien même un enregistrement tronqué en cours d'écriture ne peut pas brouiller la frontière du suivant.
Ce n'est pas une conception théorique. La RFC mentionne explicitement les enregistrements ajoutés à des fichiers de journaux qui sont tronqués par le système de fichiers après un plantage ou un arrêt administratif, et elle indique qu'un analyseur ne doit pas interrompre toute la lecture pour autant.
2. Deux jeux de règles : encodeurs et analyseurs
La RFC 7464 fournit exceptionnellement deux grammaires ABNF, l'une contraignant les écrivains et l'autre assouplissant les lecteurs.
| Rôle | Grammaire | Signification |
|---|---|---|
| Encodeur | *(RS JSON-text LF) |
Un RS avant chaque texte JSON, un LF après |
| Analyseur | *(1*RS possible-JSON) |
Les octets RS consécutifs sont autorisés, et le LF entre enregistrements est facultatif |
En termes d'ingénierie, il y a cinq règles strictes :
- UTF-8 est obligatoire ; UTF-16 et UTF-32 ne doivent pas être utilisés ;
- L'analyseur considère « tout octet autre que RS » comme un texte JSON possible et tente de l'analyser ; en cas d'échec, il saute au RS suivant et continue ;
- Les octets RS consécutifs ne représentent pas des éléments vides et sont ignorés ;
- Il n'y a pas de marqueur de fin de séquence, le flux se termine simplement ;
- Lorsque la valeur racine est un nombre,
true,falseounull, l'enregistrement doit se terminer par un octet d'espacement, sinon il est écarté comme potentiellement tronqué.
La cinquième règle est celle qu'on oublie. Les objets, tableaux et chaînes sont auto-délimités en JSON ; les nombres ne le sont pas. <RS>123<RS> ne permet pas de savoir s'il contient un 123 complet ou un 1234 tronqué. La RFC exige donc des écrivains qu'ils ajoutent un LF, utilisé comme octet témoin pour détecter la troncature. De même, <RS>true<RS> pourrait à l'origine avoir été trueish.
3. Choisir entre JSON-seq et NDJSON
Les deux formats diffusent en flux, mais visent des problèmes différents :
| Dimension | Séquences de textes JSON | NDJSON / JSON Lines |
|---|---|---|
| Position du délimiteur | RS (0x1E) avant l'enregistrement |
LF (0x0A) après l'enregistrement |
| Type de média | application/json-seq, enregistré auprès de l'IANA |
Aucun enregistrement officiel, convention communautaire |
| Lisibilité en octets | Caractère de contrôle invisible, cat n'affiche rien |
Texte brut, directement lisible |
| Ajouts concurrents | Le lecteur peut se resynchroniser et écarter une fin tronquée | Les ajouts fonctionnent aussi, les lignes défectueuses sont ignorées |
| Nombres racine | Nécessite un octet d'espacement final pour être fiable | Le saut de ligne fait office de frontière |
| Délimiteur dans les chaînes | RS ne peut apparaître que sous la forme \u001e |
LF doit s'écrire \n |
| Outillage | jq --seq et quelques bibliothèques |
Presque tous les outils de journaux et big data |
| Extension de fichier | Non définie par la RFC | .jsonl / .ndjson |
La décision est courte : utilisez JSON-seq lorsque vous maîtrisez les deux extrémités, voulez un type de média formel et avez besoin que le lecteur se resynchronise après une troncature ; utilisez JSON Lines et NDJSON quand les données alimentent des systèmes de journaux, doivent être lisibles par des humains ou doivent fonctionner avec les outils existants.
4. Un analyseur : découper sur 0x1E, puis JSON.parse
Le bénéfice le plus tangible de la RFC 7464 est de ne pas avoir besoin d'un analyseur JSON en flux. Découpez par octet, puis appelez JSON.parse sur chaque segment :
/** ASCII record separator, defined by RFC 7464 as 0x1E */
const RECORD_SEPARATOR = 0x1e;
/**
* Parses a chunk of JSON Text Sequence bytes.
* @param buffer Bytes that have been read so far
* @returns Successfully parsed records and the number of failed records
*/
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 };
}
Trois notes d'implémentation :
- Le découpage au niveau des octets est sûr. Les octets de continuation UTF-8 vivent dans la plage
0x80–0xBF, ils ne peuvent donc jamais égaler0x1E. Découper sur 0x1E ne peut pas couper un caractère multioctet en deux. - Tamponnez la fin en flux. Lors d'une lecture par blocs, le segment final peut ne pas encore avoir atteint le RS suivant. Gardez-le dans le tampon jusqu'à l'arrivée du bloc suivant ; sinon vous analyserez deux fois le même demi-enregistrement.
- Comptez les échecs au lieu d'abandonner. La RFC demande aux analyseurs de sauter au RS suivant et de continuer,
badCountest donc une mesure d'alerte, pas une exception. Un enregistrement final tronqué appartient aussi à ce seau.
L'usage mémoire dépend du plus grand enregistrement individuel plus un tampon, pas de la taille totale du fichier. C'est la même idée abordée dans analyser et diffuser de gros JSON.
5. Caractères de contrôle dans les chaînes
JSON exige que tout caractère de contrôle de U+0000 à U+001F soit échappé dans une chaîne, et RS tombe dans cette plage. Une chaîne JSON valide ne peut donc contenir que les cinq caractères \u001e, jamais un octet 0x1E brut. C'est pourquoi le découpage sur 0x1E est toujours sûr pour des données valides.
Le danger se situe côté écrivain :
- Construire à la main du texte JSON avec
'\x1e', en insérant un caractère de contrôle brut dans une chaîne ; - Construire du JSON avec des littéraux de gabarit au lieu de
JSON.stringify, en laissant sauts de ligne, guillemets et caractères de contrôle non échappés ; - Copier depuis un terminal ou un éditeur du texte contenant RS, où le caractère de contrôle est avalé ou remplacé.
Le bon schéma consiste toujours à générer d'abord le texte, puis à y attacher le délimiteur :
const recordText = JSON.stringify(record);
const line = `${String.fromCharCode(RECORD_SEPARATOR)}${recordText}\n`;
JSON.stringify émet les caractères de contrôle sous la forme \u00XX, un RS brut ne peut donc jamais apparaître dans l'enregistrement. Une fois cette étape correcte, la logique de découpage n'a plus qu'à traiter des octets et n'a jamais besoin de comprendre la syntaxe JSON.
La plupart des échecs de cette catégorie se manifestent par un JSON.parse levant Unexpected token, notamment le cas déroutant où « le premier caractère est un octet de contrôle invisible ». La table de recherche des messages d'erreur JSON détaille cette classe d'erreur ligne par ligne.
6. Transport HTTP et type de média
application/json-seq est un type de média enregistré sans paramètre obligatoire ni facultatif, et le jeu de caractères est fixé à UTF-8. Sur HTTP, trois pratiques comptent :
- Envoyez
Accept: application/json-seqet répondez avecContent-Type: application/json-seq; - Pour diffuser réellement, le serveur doit utiliser le transfert par blocs et désactiver la mise en tampon des réponses dans les intermédiaires tels que proxys inverses, CDN et outils de test de charge ;
- Un client lisant bloc par bloc peut recevoir un bloc qui se termine au milieu d'un enregistrement, l'analyseur doit donc tamponner jusqu'au RS suivant avant d'émettre un enregistrement.
Si l'autre partie ne comprend que l'entrée délimitée par lignes, ne lui imposez pas JSON-seq. C'est une norme, mais sa pénétration écosystémique est bien inférieure à NDJSON, la compatibilité doit donc se décider lors de la négociation de protocole plutôt qu'après un échec d'analyse.
7. Ligne de commande, journaux et prise en charge par les outils
jq est l'un des rares outils dotés d'une prise en charge native ; son option --seq existe précisément pour la RFC 7464 :
printf '\x1e{"id":1}\n\x1e{"id":2}\n' | jq --seq -c '.id'
Cela affiche 1 et 2 sur des lignes séparées. Pour un point de terminaison en flux, pensez à désactiver la mise en tampon de curl :
curl -sN https://api.example.com/events | jq --seq -c '.type'
jq --seq lit l'entrée RFC 7464, ignore les enregistrements défectueux et continue l'analyse. C'est particulièrement utile lors du suivi d'un fichier journal encore en cours d'écriture : le dernier enregistrement peut être à moitié écrit, et la sortie n'affichera pas de faux objet final.
7.1 Prise en charge native par environnement
| Environnement | Prise en charge intégrée | Remarques |
|---|---|---|
| jq | --seq |
L'option en ligne de commande la plus simple |
| Node.js | Aucune | Buffer.indexOf plus JSON.parse |
| Python | Aucune | Découper sur b'\x1e' |
| Go | Aucune | SplitFunc personnalisée pour bufio.Scanner |
| Navigateur | Aucune | fetch plus ReadableStream, découper les octets manuellement |
7.2 Pièges courants
Les erreurs se concentrent côté lecture :
- Lire avec
readline, ce qui laisse le préfixe RS invisible sur la première ligne et fait échouerJSON.parseà coup sûr ; - Ouvrir un fichier
.json-seqdans un éditeur où les caractères de contrôle sont invisibles, ce qui donne l'impression d'un seul document JSON bizarre ; - Découper avec
split(/\n/), ce qui confond les sauts de ligne échappés dans un enregistrement avec le LF entre enregistrements ; - Traiter RS comme un caractère ordinaire dans la documentation, si bien qu'il disparaît après copier-coller.
8. Quand cela vaut la peine
Cas adaptés :
- Vous possédez à la fois le producteur et le consommateur, sans outil tiers entre les deux ;
- Les données sont un flux d'événements ajoutés en continu et le processus peut planter, vous avez donc besoin d'une reprise après troncature ;
- Le protocole mérite un type de média formel plutôt qu'une convention comme
.jsonl; - Les enregistrements individuels peuvent être des nombres ou des booléens racine, où une règle explicite de troncature compte.
Cas inadaptés :
- Les journaux doivent aller dans ELK, Loki ou un autre système qui ne comprend que l'entrée délimitée par lignes ;
- Des personnes doivent
cat,grepetawkdirectement les données ; - L'autre partie ne sait analyser que
application/json.
9. Liste de vérification
- [ ] L'écrivain génère les enregistrements avec
JSON.stringify, puis préfixe0x1Eet suffixe\n; - [ ] Les enregistrements contenant des nombres, booléens ou
nullracine se terminent par LF ; - [ ] Le lecteur découpe sur les octets plutôt que de lire ligne par ligne ;
- [ ] Les lectures en flux gardent une fin non fermée dans le tampon jusqu'à l'arrivée du RS suivant ;
- [ ] Les échecs d'analyse sont comptés et ignorés, jamais fatals pour la séquence ;
- [ ] Les interfaces publiques déclarent explicitement
application/json-seqet conservent un repli NDJSON.
FAQ
Faut-il adopter JSON-seq pour un point de terminaison d'API public ?
Seulement lorsque les deux extrémités peuvent être modifiées ou que le format est négocié en amont. JSON-seq est une norme enregistrée avec le type de média application/json-seq, mais sa pénétration écosystémique est bien inférieure à NDJSON, l'imposer à un client tiers échoue donc généralement. Décidez la compatibilité lors de la négociation de protocole et conservez un repli NDJSON.
Faut-il convertir un pipeline NDJSON existant en JSON-seq ?
Généralement non. NDJSON diffuse déjà en flux, accepte les ajouts et ignore les lignes défectueuses, et presque tous les outils de journaux et big data l'acceptent. Ne convertissez que si vous avez besoin du type de média enregistré auprès de l'IANA ou de la resynchronisation qu'apporte un délimiteur initial, et uniquement si le producteur et le consommateur peuvent être modifiés ensemble.
Qu'est-ce qui est le plus courant, les séquences de textes JSON ou NDJSON ?
NDJSON. Ses règles sont plus simples, sa couverture outillée bien plus large, et les systèmes de journaux comme les pipelines big data l'acceptent généralement. JSON-seq l'emporte sur le type de média enregistré auprès de l'IANA et sur la resynchronisation issue du délimiteur placé en tête, ce qui convient aux protocoles de flux maîtrisés plutôt qu'à l'échange de données général.
Pourquoi le délimiteur est-il placé avant l'enregistrement plutôt qu'après ?
Parce que les enregistrements peuvent être tronqués. Avec un délimiteur initial, un lecteur qui voit RS sait qu'un nouvel enregistrement a commencé, l'enregistrement précédent ne peut donc pas corrompre le suivant, qu'il ait été complet ou non. Avec un délimiteur final, une écriture interrompue laisse un segment final qui n'a ni marqueur de fin ni marqueur de début suivant, la frontière ne peut donc qu'être devinée.
Que se passe-t-il si 0x1E apparaît dans un enregistrement ?
Une chaîne JSON valide doit échapper RS en \u001e, un 0x1E brut ne devrait donc jamais apparaître. S'il apparaît, le texte n'est plus du JSON valide : le découpage sur 0x1E cassera au mauvais endroit et JSON.parse échouera. La correction se situe côté écrivain : utilisez JSON.stringify au lieu de texte construit à la main.
Un navigateur peut-il analyser directement application/json-seq ?
Non. Les navigateurs n'embarquent aucun analyseur JSON-seq ; fetch ne livre que les octets. Il faut utiliser response.body.getReader(), accumuler les blocs jusqu'au prochain 0x1E et appeler JSON.parse sur chaque segment. Pour de petites charges, lire tout le corps en chaîne puis découper ensuite fonctionne aussi.
Pourquoi un nombre racine a-t-il besoin d'un LF final ?
Les objets, tableaux et chaînes sont auto-délimités en JSON, tandis que les nombres ne le sont pas : <RS>123<RS> pourrait être un 123 complet ou un 1234 tronqué. La RFC 7464 exige donc un octet d'espacement après chaque enregistrement, et le LF sert de témoin pour détecter la troncature. La même règle s'applique à true, false et null racine.