JSON Toolkit

JSON Lines et NDJSON : les données en flux

L’essentiel : Une valeur JSON par ligne vous apporte l'ajout incrémental, le flux continu et l'isolation des dommages à une seule ligne ; le coût est l'absence de structure d'ensemble, la validation doit donc se faire ligne par ligne.

Intermédiaire~10 min de lecturePublié le 2026-10-30Mis à jour le 2026-10-30
Illustration de l'article JSON Lines et NDJSON pour les données en flux

L'essentiel : utilisez un tableau JSON lorsque vous traitez tout un lot d'un coup, et JSON Lines lorsque vous avez besoin d'ajouts en flux et d'isolation des lignes défectueuses ; si vous devez encore valider la structure d'ensemble, ne passez pas à un format délimité par lignes.

Dès que les données dépassent la mémoire, ou arrivent continuellement, la structure « un grand tableau » devient l'ennemi.

1. Règles du format

JSON Lines (.jsonl) et NDJSON (.ndjson) sont pratiquement synonymes, et les règles sont courtes dans les deux cas :

  1. L'encodage est UTF-8, sans BOM ;
  2. Chaque ligne contient une valeur JSON complète et valide ;
  3. Les lignes sont séparées par \n ; la dernière ligne peut ou non se terminer par un saut de ligne ;
  4. Aucun saut de ligne non échappé ne doit apparaître dans une ligne — un saut de ligne dans une chaîne doit s'écrire \n ;
  5. Les lignes vides sont généralement ignorées, mais une implémentation stricte devrait les considérer comme invalides.
{"time":"2026-10-30T10:41:45+08:00","level":"info","message":"boot"}
{"time":"2026-10-30T10:41:46+08:00","level":"warn","message":"slow query","costMs":812}
{"time":"2026-10-30T10:41:47+08:00","level":"error","message":"timeout","traceId":"a1b2"}

La différence subtile entre les deux : le site JSON Lines met l'accent sur « une valeur par ligne, et la valeur peut être n'importe quel JSON (y compris un scalaire) », tandis que la spécification NDJSON ajoute des règles d'implémentation plus strictes comme « pas de lignes vides ni de scalaires racine ». En ingénierie, suivre l'ensemble le plus strict vous évite des ennuis.

2. Comparaison avec un tableau JSON

Dimension Tableau JSON JSON Lines
Ajouter un enregistrement Réécrire tout le fichier (modifier le ] final) Ajouter simplement une ligne
Lecture en flux Nécessite un analyseur en flux suivant la profondeur des crochets Découper par ligne
Une entrée corrompue Tout le fichier devient inanalysable Seule cette ligne est affectée
Validation du fichier entier Tout valider en une passe Seulement ligne par ligne
Compression Taille similaire après gzip Idem, et les blocs peuvent être compressés en parallèle
Expression de l'imbrication Prise en charge native Chaque ligne peut s'imbriquer en interne ; les lignes sont indépendantes
Consommateurs typiques Tous les outils JSON Systèmes de journaux, pipelines big data, jeux d'entraînement de LLM

La conclusion est directe : c'est le fait que les données soient « un lot d'enregistrements » ou « un document unique » qui décide du format.

3. Lecture : flux ligne par ligne

Sous Node, l'approche la plus simple est readline :

import { createReadStream } from 'node:fs';
import { createInterface } from 'node:readline';

/**
 * Reads a JSON Lines file line by line and invokes a callback for each record.
 * @param filePath Path to the file
 * @param onRecord Handler for a single record
 * @returns Statistics containing the number of successful and failed lines
 */
async function readJsonLines(
  filePath: string,
  onRecord: (record: unknown, lineNumber: number) => void,
): Promise<{ okCount: number; badCount: number }> {
  const lineReader = createInterface({
    input: createReadStream(filePath, { encoding: 'utf8' }),
    crlfDelay: Infinity,
  });

  let okCount = 0;
  let badCount = 0;

  for await (const rawLine of lineReader) {
    const line = rawLine.trim();
    if (!line) {
      continue;
    }
    try {
      onRecord(JSON.parse(line), okCount + badCount + 1);
      okCount += 1;
    } catch {
      badCount += 1;
    }
  }

  return { okCount, badCount };
}

La consommation mémoire de ce code ne dépend que de la taille d'une seule ligne, pas de la taille totale du fichier — c'est la plus grande valeur d'un format délimité par lignes.

3.1 Quand seuls quelques champs sont nécessaires, ne faites pas d'abord JSON.parse

Si chaque enregistrement a 50 champs et que vous n'en avez besoin que de 3, JSON.parse construit des objets pour les 50. Un analyseur en flux (stream-json, clarinet) peut extraire uniquement les champs visés, au prix d'une implémentation plus complexe. La règle empirique : lorsqu'un enregistrement fait moins de quelques centaines de Ko, appeler directement JSON.parse reste généralement plus avantageux.

3.2 Texte chinois et BOM

Les fichiers .jsonl générés sous Windows portent souvent un BOM, et JSON.parse échouera à coup sûr sur la première ligne. Supprimez le BOM avant de lire, ou inspectez les trois premiers octets lors du flux du premier bloc.

4. Écriture : ajouts et atomicité

import { appendFile } from 'node:fs/promises';

/**
 * Appends a single JSON Lines record.
 * @param filePath Target file
 * @param record The record object to write
 * @returns A promise that resolves when the write completes
 */
async function appendRecord(filePath: string, record: unknown): Promise<void> {
  const line = `${JSON.stringify(record)}\n`;
  await appendFile(filePath, line, { encoding: 'utf8' });
}

Deux points à noter :

  1. La sortie de JSON.stringify ne contient pas de saut de ligne (sauf si le \n d'une chaîne est échappé en deux caractères), une ligne correspond donc toujours exactement à un enregistrement ;
  2. Lorsque plusieurs processus ajoutent en parallèle, une écriture write unique inférieure à PIPE_BUF (généralement 4096 octets sous Linux) est atomique ; au-delà, ce n'est pas garanti. Pour les scénarios à forte concurrence, passez à « un fichier par processus, fusionné après écriture sur disque », ou utilisez un stockage transactionnel.

5. Traiter les lignes défectueuses

C'est la décision qu'un format délimité par lignes vous force à prendre à l'avance :

Stratégie Adaptée à Coût
Ignorer et compter Données tolérant des trous, comme les journaux et événements analytiques Perte silencieuse de données, l'alerte est donc indispensable
Échouer immédiatement Données qui ne doivent pas être perdues, comme la facturation Une ligne défectueuse bloque tout le lot
Rediriger vers un fichier de rebut Cas nécessitant un rattrapage ultérieur Stockage supplémentaire et processus de rejeu

Quel que soit votre choix, consignez le numéro de ligne et un fragment du contenu original, sinon vous ne pourrez pas localiser le problème ensuite. Le numéro de ligne n'est pas présent dans le message d'erreur de JSON.parse, il faut donc le maintenir vous-même.

6. Compression et découpage

  • gzip compresse les .jsonl avec un ratio régulièrement élevé (les noms de champs se répètent), .jsonl.gz est donc recommandé ;
  • gzip compresse par blocs et prend naturellement en charge la décompression en flux, inutile donc de décompresser tout le fichier ;
  • Découpez les gros fichiers en tranches d'environ 1GB, avec des numéros de séquence dans les noms, pour faciliter le traitement parallèle et le transfert reprenable ;
  • Ne prétraitez jamais un .jsonl en le « fusionnant en tableau » — cela jette tous ses avantages.

7. Arbitrages face aux autres formats big data

Format Structure Schéma inclus Adapté à
JSON Lines Indépendant par ligne Non (nécessite un contrat externe) Journaux, imports/exports, données d'entraînement
CSV Tabulaire Non Rapports, échange avec Excel
Parquet Colonne Oui Requêtes analytiques, très grands jeux hors ligne
Avro Ligne Oui Messages Kafka, évolution de schéma

L'avantage de JSON Lines est que tout langage et tout outil peuvent le lire et l'écrire immédiatement ; l'inconvénient est l'absence de schéma et de compression colonnaire. Dès que les données ne tiennent plus sur une seule machine et que vous avez besoin de requêtes colonnaires fréquentes, il est temps d'envisager Parquet.

8. Liste de vérification

  • [ ] L'extension du fichier est cohérente (.jsonl ou .ndjson) et jamais mélangée ;
  • [ ] L'écrivain garantit « un enregistrement par ligne », les sauts de ligne internes aux chaînes étant déjà échappés ;
  • [ ] Le lecteur supprime le BOM, ignore les lignes vides et tient à jour les numéros de ligne ;
  • [ ] La stratégie des lignes défectueuses est explicitement définie et surveillée (lignes défectueuses / lignes totales) ;
  • [ ] Les gros fichiers sont découpés par taille et associés à gzip et à des lectures parallèles ;
  • [ ] Là où un contrat de champs existe, un schéma supplémentaire est fourni et validé ligne par ligne.

FAQ

Comment choisir entre un tableau JSON et JSON Lines ?

Regardez si les données sont un lot d'enregistrements ou un document unique. Choisissez JSON Lines lorsque les enregistrements sont ajoutés et lus en flux : ajouter revient à écrire une ligne à la fin, et lire ne demande qu'un découpage par ligne. Gardez un tableau JSON lorsque toute la structure doit être validée en une passe, car un tableau JSON valide tout d'un coup tandis que JSON Lines ne peut être validé que ligne par ligne.

Pourquoi une ligne corrompue ne casse-t-elle pas tout le fichier ?

Chaque ligne de JSON Lines est une valeur JSON indépendante et complète, et l'analyse se fait naturellement ligne par ligne, une donnée corrompue n'affecte donc que cette ligne. Dans un tableau JSON, une seule erreur de syntaxe rend tout le fichier inanalysable, ce qui explique précisément pourquoi les données délimitées par lignes conviennent aux journaux et aux événements analytiques.

Faut-il convertir JSON Lines en tableau JSON ?

C'est possible : lisez ligne par ligne, appelez JSON.parse sur chaque enregistrement et collectez-les dans un tableau. Cela exige que tout le contenu tienne en mémoire et abandonne la lecture en flux et l'ajout d'une seule ligne. Prétraiter un .jsonl en tableau jette tous les avantages du format, ce n'est donc raisonnable que pour une livraison ponctuelle où les données tiennent en mémoire.