JSONL en pratique : ouvrir, convertir et déboguer les lignes invalides
L’essentiel : Passer un fichier .jsonl entier à JSON.parse et obtenir une erreur signifie presque toujours que la stratégie de lecture est fausse, pas que le fichier est cassé. Ce guide couvre les façons standard d'inspecter et de lire un JSONL ligne par ligne, les commandes de conversion vers et depuis un tableau JSON, un tableau de six types de lignes invalides, et la bonne façon de consommer du NDJSON depuis une réponse HTTP en flux.

L'essentiel : chaque ligne d'un fichier JSONL est une valeur JSON valide et indépendante, mais le fichier entier n'est pas un unique document JSON. Si le passage du fichier complet à JSON.parse ou json.load échoue, neuf fois sur dix le fichier va bien — c'est votre stratégie de lecture qui est fausse.
Un enregistrement par ligne, pas de virgules entre les lignes, pas de tableau englobant — le JSONL abandonne la « structure globale du document » pour gagner l'écriture par ajout, la lecture en flux et l'isolation des pannes ligne par ligne. Ces trois avantages ne se matérialisent qu'en traitant le fichier ligne par ligne, et tous se transforment en erreurs dès qu'on tente de tout analyser d'un bloc.
1. Trois noms, un seul format : JSONL, NDJSON, JSON Lines
Les débutants voient souvent une différence de syntaxe là où il n'y a qu'une différence de nom :
| Nom | Contexte typique | Extension / type média |
|---|---|---|
| JSON Lines | Ingénierie des données, jeux de données ML (pandas, Spark, Hugging Face) | .jsonl |
| NDJSON | API HTTP en flux, données temps réel | .ndjson, application/x-ndjson |
| JSONL | Abréviation courante pour l'un ou l'autre | — |
Tout outil qui lit l'un lit l'autre. Le seul critère de choix, ce sont les conventions de votre écosystème aval : écrivez .jsonl pour préparer des jeux de données Hugging Face, déclarez application/x-ndjson pour exposer une API en flux. Un fichier JSONL portable respecte trois règles : encodage UTF-8 (sans BOM), une valeur JSON complète par ligne, lignes séparées par \n. Un retour à la ligne final en fin de fichier est fortement recommandé, et les lignes vides ne sont pas des enregistrements valides. Les détails protocolaires et la logique du design sont traités dans notre aperçu de JSON Lines et NDJSON ; cet article porte strictement sur ce qu'il faut faire une fois le fichier en main.
2. Inspecter : d'abord head, puis grep, l'éditeur en dernier
Double-cliquer sur un .jsonl de 500 Mo pour l'ouvrir dans un éditeur est l'erreur la plus fréquente : trois millions de lignes, la coloration syntaxique et un tampon d'annulation feront planter quasiment n'importe quel éditeur — et ce n'est pas la faute du JSON. L'analyse d'un document de 45 Mo prend environ 208 ms dans Node 22 ; ce qui bloque, c'est le rendu. Les formats orientés ligne existent pour les outils en ligne de commande, car les enregistrements coïncident avec les lignes physiques :
# Évaluez d'abord l'ampleur : nombre d'enregistrements et taille, sans lire le contenu
wc -l events.jsonl && du -h events.jsonl
# Voyez à quoi ressemble un enregistrement (champs et structure)
head -n 1 events.jsonl | jq .
# Trouvez les enregistrements cibles directement : les outils ligne à ligne marchent — un privilège du JSONL
grep '"level":"error"' events.jsonl | head -n 5
tail -n 100 events.jsonl | jq 'select(.latency_ms > 500)'
Notez la dernière commande : chaque ligne sortant de tail reste une valeur JSON indépendante, qu'on peut donc passer telle quelle à jq, sans reconstruire un tableau. Cette approche fonctionne à l'identique sur des fichiers de plusieurs gigaoctets avec une mémoire quasi nulle — c'est le même principe que pourquoi les gros JSON ralentissent les pages.
3. Les trois modèles standards de lecture ligne à ligne
La lecture ligne à ligne doit gérer trois choses : ignorer les lignes vides (notamment le retour à la ligne final), signaler les erreurs avec le numéro de ligne, et empêcher qu'une seule ligne cassée ne tue tout le travail. Les modèles standards des trois langues tournent autour de ces points.
Python, bibliothèque standard uniquement :
import json
with open("events.jsonl", encoding="utf-8") as f:
for line_number, line in enumerate(f, start=1):
if not line.strip(): # ignorer les lignes vides
continue
try:
record = json.loads(line) # analyser chaque ligne indépendamment
except json.JSONDecodeError as e:
print(f"Ligne {line_number} : {e}") # signaler sans interrompre
continue # isoler la ligne invalide
handle(record)
Node avec le module readline intégré, mémoire constante quelle que soit la taille du fichier :
import fs from "node:fs";
import readline from "node:readline";
const input = fs.createReadStream("events.jsonl", "utf8");
const lines = readline.createInterface({ input, crlfDelay: Infinity });
let lineNumber = 0;
for await (const line of lines) {
lineNumber += 1;
if (!line.trim()) continue;
try {
handle(JSON.parse(line));
} catch (e) {
console.error(`Ligne ${lineNumber} : ${e.message}`);
}
}
Dans Go 1.27, encoding/json/v2 devient le défaut stable, et son décodage en flux valeur par valeur colle parfaitement au JSONL ; utilisez gojq si vous voulez embarquer des expressions jq. Côté bibliothèques, ne vous précipitez pas pour installer des paquets : jsonlines en Python signale le numéro et le texte brut de toute ligne fautive, et orjsonl lit et écrit directement les fichiers compressés gzip/bzip2/zstd ; le paquet Node ndjson n'a publié aucune version depuis 2020 — le readline intégré suffit.
4. Convertir vers et depuis un tableau JSON
Le sens compte ; prenez-le à l'envers et chaque commande sera fausse. Retenez une règle : tableau → JSONL est un map (chaque enregistrement compacté sur une ligne), JSONL → tableau est un collect (les lignes rassemblées en liste).
# Tableau JSON → JSONL : -c compacte chaque élément sur une ligne
jq -c '.[]' records.json > records.jsonl
# JSONL → tableau JSON : -s rassemble toute l'entrée dans un seul tableau
jq -s '.' events.jsonl > records.json
# Filtrer le JSONL ligne par ligne : pas besoin de -s, jq traite l'entrée comme un flux
jq -c 'select(.active == true)' events.jsonl > active.jsonl
Les one-liners Python sont équivalents : tableau → JSONL écrit json.dumps(r, ensure_ascii=False) + "\n" par enregistrement ; en sens inverse on collecte les json.loads ligne par ligne dans une liste. Chaque sens a son piège. À l'aller (tableau → JSONL), tout vrai saut de ligne à l'intérieur d'un enregistrement doit être échappé par json.dumps — sinon un enregistrement se déchire en plusieurs lignes. Au retour (JSONL → tableau), ne concaténez pas les lignes en une immense chaîne pour l'analyser — sur des millions de lignes, c'est une copie intégrale en mémoire de trop. Les agrégations (sommes, groupements, moyennes) exigent -s, qui charge tout ; sur des fichiers de plusieurs Go, passez à awk ou à un accumulateur Python en flux.
5. Six types de lignes invalides et comment les diagnostiquer
Presque toutes les erreurs JSONL se vivront au niveau de la ligne. Valider ligne par ligne en signalant les numéros est le seul diagnostic fiable. Par fréquence :
| Symptôme | Cause racine | Correction |
|---|---|---|
Extra data en fin de ligne |
Des virgules ajoutées entre les lignes | Supprimez-les ; chaque ligne doit tenir seule |
| Un enregistrement coupé sur plusieurs lignes | Vrai saut de ligne non échappé dans l'enregistrement (formatage à la main) | Re-compactez avec json.dumps / jq -c |
Première ligne en erreur avec Unexpected token ï |
Fichier avec BOM | Réenregistrez en UTF-8 sans BOM |
| Dernière ligne en erreur alors qu'elle semble correcte | Retour à la ligne final manquant ou espaces parasites | Ajoutez \n ; ignorez les lignes vides par convention |
| Erreur de saut de ligne dans une chaîne | Retour à la ligne littéral dans une chaîne, pas \n |
Corrigez le générateur ; ne concaténez jamais des chaînes JSON à la main |
| Grands entiers modifiés | Analyse via un double (troncature à 2^53) | Utilisez un analyseur préservant la précision, voir précision bigint |
La routine de diagnostic est toujours la même en trois étapes : head -n 1 pour vérifier l'encodage et la structure de la première ligne ; un script ligne à ligne qui signale le numéro de chaque ligne fautive au lieu de s'arrêter à la première ; puis sortez les lignes fautives une à une — une ligne, c'est un enregistrement complet, l'inspecter isolément ne coûte rien. Pour les imports en lot, la politique recommandée est « quarantaine, pas abandon » : écrivez les lignes fautives et leurs numéros dans errors.jsonl, continuez à charger les bonnes, puis décidez d'un réimport selon le taux de lignes fautives.
6. JSONL sur HTTP : consommation ligne à ligne et contre-pression
Le schéma typique du NDJSON sur HTTP : un serveur qui écrit en continu, un client qui lit en continu. Côté client, n'appelez pas response.json() — il met tout le corps en mémoire tampon avant l'analyse, ce qui annule l'intérêt du flux. Lisez directement le flux du corps, découpez sur \n et reportez le fragment terminal incomplet à l'itération suivante :
const reader = response.body.pipeThrough(new TextDecoderStream()).getReader();
let buffer = "";
for (;;) {
const { value, done } = await reader.read();
if (done) break;
buffer += value;
const lines = buffer.split("\n");
buffer = lines.pop(); // le dernier morceau peut être une ligne incomplète
for (const line of lines) {
if (line.trim()) handle(JSON.parse(line));
}
}
Côté serveur, deux choses sont obligatoires : d'abord, quand res.write() renvoie false, attendez l'événement drain (contre-pression), sinon la mémoire croît sans limite dès que le consommateur est plus lent que le producteur ; ensuite, désactivez le middleware gzip sur les routes en flux — gzip doit tamponner toute la réponse avant de compresser quoi que ce soit, ce qui neutralise entièrement le streaming NDJSON. Ces deux comportements sont recoupés par des tests locaux et plusieurs sources anglophones indépendantes, pas des suppositions.
7. Quand le JSONL est le mauvais choix
L'espace négatif du choix de format compte aussi. Trois scénarios doivent rester en tableau JSON :
| Besoin | Pourquoi le JSONL convient mal | Utilisez plutôt |
|---|---|---|
| Valider la structure du document entier | Pas de structure globale ; la dérive des champs passe inaperçue | Tableau JSON + Schema |
| Accès aléatoire à l'enregistrement N | Longueurs de lignes variables ; pas de recherche binaire | SQLite / Parquet |
| Imbrication profonde, références entre enregistrements | Chaque ligne est isolée ; les références n'ont nulle part où vivre | Document JSON standard |
Règle empirique en une phrase : si les enregistrements arrivent au fil du temps ou sont consommés ligne par ligne (logs, événements, exports, jeux d'entraînement), prenez le JSONL ; si un petit volume est produit une fois, consommé une fois et exige une sémantique de document entier, prenez un tableau JSON. Les variantes dérivées (JSON Text Sequences délimitées par RS) sont un autre sujet — voir l'article sur RFC 7464.
FAQ
Existe-t-il une vraie différence entre JSONL et NDJSON ?
Pas sur le plan de l'ingénierie. Les deux signifient « une valeur JSON complète par ligne, pas d'enveloppe externe, pas de virgules entre les lignes », et tout outil qui en supporte un supporte l'autre. La différence tient à l'appellation et aux habitudes de l'écosystème : .jsonl domine l'ingénierie des données et les jeux de données ML ; .ndjson et application/x-ndjson dominent les API HTTP en flux. Lors d'une intégration avec un système externe, suivez l'extension et le type média que sa documentation spécifie.
Puis-je lire un .jsonl directement avec JSON.parse ?
Pas le fichier entier. JSON.parse accepte exactement un texte JSON, alors que le JSONL est une suite de valeurs indépendantes disposées ligne par ligne : l'analyse du tout échoue toujours avec « Unexpected token » ou « Extra data ». Découpez en lignes et analysez chaque ligne non vide séparément, en signalant le numéro en cas d'échec.
Si une ligne d'un fichier JSONL est cassée, tout le fichier est-il fichu ?
Non — l'isolation des pannes ligne par ligne est au cœur du design du format. Une ligne fautive ne concerne qu'elle : ignorez-la, journalisez son numéro et continuez la lecture. Pour les imports en lot, mettez les lignes fautives en quarantaine dans un fichier séparé, chargez les enregistrements corrects normalement, puis décidez d'une correction et d'un réimport selon le taux de lignes fautives.
Comment convertir du JSONL vers Excel ou CSV ?
Convertissez-le d'abord en tableau ou extrayez les champs ligne par ligne, puis passez à une conversion tabulaire : compactez avec jq -c '.[]' data.jsonl et donnez le résultat à un convertisseur, ou faites transiter chaque ligne par json.loads vers pandas. Attention : les champs imbriqués aplaties produisent des noms de colonnes à points, les champs tableaux ne peuvent être sérialisés que comme texte dans une cellule, et les colonnes de longs identifiants devraient être déclarées en chaînes dès le départ pour éviter toute perte de précision.