JSON Toolkit

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 ».

Intermédiaire~9 min de lecturePublié le 2026-10-31Mis à jour le 2026-10-31
Illustration des séquences de textes JSON et du séparateur d'enregistrement RFC 7464

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 :

  1. UTF-8 est obligatoire ; UTF-16 et UTF-32 ne doivent pas être utilisés ;
  2. 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 ;
  3. Les octets RS consécutifs ne représentent pas des éléments vides et sont ignorés ;
  4. Il n'y a pas de marqueur de fin de séquence, le flux se termine simplement ;
  5. Lorsque la valeur racine est un nombre, true, false ou null, 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 égaler 0x1E. 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, badCount est 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 :

  1. Construire à la main du texte JSON avec '\x1e', en insérant un caractère de contrôle brut dans une chaîne ;
  2. 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 ;
  3. 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-seq et répondez avec Content-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 :

  1. Lire avec readline, ce qui laisse le préfixe RS invisible sur la première ligne et fait échouer JSON.parse à coup sûr ;
  2. Ouvrir un fichier .json-seq dans un éditeur où les caractères de contrôle sont invisibles, ce qui donne l'impression d'un seul document JSON bizarre ;
  3. Découper avec split(/\n/), ce qui confond les sauts de ligne échappés dans un enregistrement avec le LF entre enregistrements ;
  4. 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, grep et awk directement 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éfixe 0x1E et suffixe \n ;
  • [ ] Les enregistrements contenant des nombres, booléens ou null racine 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-seq et 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.