JSONL en Rust : serde_json, BufReader et abstractions a cout zero

Un guide complet pour travailler avec les fichiers JSONL (JSON Lines) en Rust. Apprenez a lire, ecrire, analyser et streamer des donnees JSONL en utilisant serde_json, BufReader, rayon pour le parallelisme et tokio pour les E/S asynchrones.

Derniere mise a jour : fevrier 2026

Pourquoi Rust pour le JSONL ?

Rust est un excellent choix pour le traitement JSONL quand la performance, la securite memoire et la correction comptent. Son modele de propriete elimine les conditions de course a la compilation, ses abstractions a cout zero vous permettent d'ecrire des chaines d'iterateurs de haut niveau qui se compilent en boucles serrees, et serde_json est l'un des parseurs JSON les plus rapides disponibles dans n'importe quel langage. Si vous construisez un pipeline de donnees qui doit traiter des millions d'enregistrements JSONL par seconde, Rust vous donne le controle pour y parvenir sans sacrifier la securite.

JSONL (JSON Lines) stocke un objet JSON par ligne, ce qui le rend ideal pour le streaming, le logging en ajout seul et le traitement de grands jeux de donnees. Le BufReader de Rust lit les fichiers ligne par ligne sans charger le fichier entier en memoire, et son modele d'iterateur s'adapte parfaitement a la structure orientee lignes du JSONL. Dans ce guide, vous apprendrez a lire et ecrire du JSONL avec BufReader, analyser les enregistrements dans des structs fortement types avec serde, traiter les enregistrements en parallele avec rayon, gerer les E/S asynchrones avec tokio et construire une gestion d'erreurs robuste avec Result et l'operateur ?.

Lire des fichiers JSONL avec BufReader

La bibliotheque standard de Rust fournit BufReader pour une lecture bufferisee efficace des fichiers. Combine avec l'iterateur lines(), il lit les fichiers JSONL une ligne a la fois avec un encombrement memoire minimal. Chaque ligne est analysee independamment, ce qui rend cette approche adaptee aux fichiers de toute taille.

L'approche la plus simple encapsule un File dans BufReader, itere sur lines() et analyse chaque ligne avec serde_json. Cela ne garde qu'une seule ligne en memoire a la fois, quelle que soit la taille du fichier.

Lecture JSONL basique avec BufReader
use std::fs::File;
use std::io::{BufRead, BufReader};
use serde_json::Value;
fn read_jsonl(path: &str) -> std::io::Result<Vec<Value>> {
let file = File::open(path)?;
let reader = BufReader::new(file);
let mut records = Vec::new();
for (line_num, line) in reader.lines().enumerate() {
let line = line?;
let trimmed = line.trim();
if trimmed.is_empty() {
continue; // Ignorer les lignes vides
}
match serde_json::from_str::<Value>(trimmed) {
Ok(value) => records.push(value),
Err(e) => {
eprintln!("Skipping invalid JSON at line {}: {}", line_num + 1, e);
}
}
}
Ok(records)
}
fn main() -> std::io::Result<()> {
let records = read_jsonl("data.jsonl")?;
println!("Loaded {} records", records.len());
if let Some(first) = records.first() {
println!("First record: {}", first);
}
Ok(())
}

Ajoutez serde et serde_json a votre Cargo.toml. La fonctionnalite derive active les macros derive Serialize et Deserialize pour definir des structs types.

Dependances Cargo.toml
[dependencies]
serde = { version = "1", features = ["derive"] }
serde_json = "1"
# Optionnel : pour le traitement parallele
rayon = "1.10"
# Optionnel : pour les E/S asynchrones
tokio = { version = "1", features = ["full"] }

Analyse type-safe avec serde_json

L'un des plus grands atouts de Rust pour le traitement JSONL est les macros derive de serde, qui vous permettent de deserialiser chaque ligne JSON directement dans un struct fortement type. Le compilateur verifie que votre code gere les donnees correctement, detectant les incompatibilites de type, les champs manquants et les erreurs structurelles a la compilation plutot qu'a l'execution.

Definissez un struct avec Deserialize et utilisez serde_json::from_str pour analyser chaque ligne JSONL dedans. Les champs manquants ou malformes produisent des messages d'erreur clairs lors de l'analyse au lieu de provoquer des panics plus tard dans votre programme.

Deserialisation dans un struct type
use serde::Deserialize;
use std::fs::File;
use std::io::{BufRead, BufReader};
#[derive(Debug, Deserialize)]
struct LogEntry {
timestamp: String,
level: String,
message: String,
#[serde(default)]
metadata: Option<serde_json::Value>,
}
fn read_typed_jsonl(path: &str) -> anyhow::Result<Vec<LogEntry>> {
let file = File::open(path)?;
let reader = BufReader::new(file);
let mut entries = Vec::new();
for line in reader.lines() {
let line = line?;
let trimmed = line.trim();
if trimmed.is_empty() {
continue;
}
let entry: LogEntry = serde_json::from_str(trimmed)?;
entries.push(entry);
}
Ok(entries)
}
fn main() -> anyhow::Result<()> {
let entries = read_typed_jsonl("logs.jsonl")?;
for entry in &entries {
println!("[{}] {}: {}", entry.level, entry.timestamp, entry.message);
}
Ok(())
}

Quand les enregistrements JSONL ont des schemas variables, utilisez serde_json::Value pour un acces flexible. Vous pouvez indexer les valeurs avec la notation entre crochets ou utiliser les methodes as_str(), as_i64() pour une conversion de type sure.

Analyse dynamique avec serde_json::Value
use serde_json::Value;
use std::fs::File;
use std::io::{BufRead, BufReader};
fn process_dynamic_jsonl(path: &str) -> anyhow::Result<()> {
let file = File::open(path)?;
let reader = BufReader::new(file);
for line in reader.lines() {
let line = line?;
let trimmed = line.trim();
if trimmed.is_empty() {
continue;
}
let value: Value = serde_json::from_str(trimmed)?;
// Acces dynamique aux champs
if let Some(name) = value.get("name").and_then(Value::as_str) {
println!("Name: {}", name);
}
if let Some(age) = value.get("age").and_then(Value::as_i64) {
println!("Age: {}", age);
}
// Verifier si un champ existe
if value.get("email").is_some() {
println!("Has email field");
}
}
Ok(())
}

Ecrire des fichiers JSONL en Rust

Ecrire du JSONL est l'inverse de la lecture : serialisez chaque enregistrement en chaine JSON, ajoutez un retour a la ligne et ecrivez-le dans un fichier. L'utilisation de BufWriter et serde_json::to_string assure a la fois la correction et la performance.

Serialisez chaque struct avec serde_json::to_string() et ecrivez-le suivi d'un retour a la ligne. La macro derive Serialize gere automatiquement la conversion de vos types Rust vers JSON.

Ecriture JSONL basique
use serde::Serialize;
use std::fs::File;
use std::io::Write;
#[derive(Serialize)]
struct Record {
id: u64,
name: String,
score: f64,
}
fn write_jsonl(path: &str, records: &[Record]) -> std::io::Result<()> {
let mut file = File::create(path)?;
for record in records {
let json = serde_json::to_string(record)
.map_err(|e| std::io::Error::new(std::io::ErrorKind::Other, e))?;
writeln!(file, "{}", json)?;
}
Ok(())
}
fn main() -> std::io::Result<()> {
let records = vec![
Record { id: 1, name: "Alice".into(), score: 95.5 },
Record { id: 2, name: "Bob".into(), score: 87.3 },
Record { id: 3, name: "Charlie".into(), score: 92.1 },
];
write_jsonl("output.jsonl", &records)?;
println!("Wrote {} records", records.len());
Ok(())
}

Pour ecrire de gros fichiers JSONL, encapsulez le File dans un BufWriter pour reduire le nombre d'appels systeme. Cela regroupe les petites ecritures en vidages de tampon plus grands, ameliorant significativement le debit.

Ecriture bufferisee pour de gros fichiers
use serde::Serialize;
use std::fs::File;
use std::io::{BufWriter, Write};
#[derive(Serialize)]
struct Event {
timestamp: u64,
event_type: String,
payload: serde_json::Value,
}
fn write_buffered_jsonl(path: &str, events: &[Event]) -> std::io::Result<()> {
let file = File::create(path)?;
let mut writer = BufWriter::new(file);
for event in events {
serde_json::to_writer(&mut writer, event)
.map_err(|e| std::io::Error::new(std::io::ErrorKind::Other, e))?;
writer.write_all(b"\n")?;
}
writer.flush()?;
Ok(())
}

Traitement parallele avec rayon

Le crate rayon de Rust fournit du parallelisme de donnees avec un minimum de modifications de code. En appelant par_iter() au lieu de iter(), rayon distribue automatiquement le travail sur tous les coeurs CPU en utilisant un pool de threads avec vol de travail. C'est particulierement efficace pour les transformations JSONL intensives en CPU ou l'analyse et le traitement dominent le temps d'E/S.

Traitement parallele avec rayon
use rayon::prelude::*;
use serde::{Deserialize, Serialize};
use std::fs::File;
use std::io::{BufRead, BufReader, BufWriter, Write};
#[derive(Debug, Deserialize, Serialize)]
struct Record {
id: u64,
text: String,
#[serde(default)]
word_count: Option<usize>,
}
fn parallel_process_jsonl(
input_path: &str,
output_path: &str,
) -> anyhow::Result<usize> {
// Etape 1 : Lire toutes les lignes en memoire
let file = File::open(input_path)?;
let reader = BufReader::new(file);
let lines: Vec<String> = reader.lines().collect::<Result<_, _>>()?;
// Etape 2 : Analyser et transformer en parallele
let processed: Vec<Record> = lines
.par_iter()
.filter(|line| !line.trim().is_empty())
.filter_map(|line| {
serde_json::from_str::<Record>(line.trim()).ok()
})
.map(|mut record| {
record.word_count = Some(record.text.split_whitespace().count());
record
})
.collect();
// Etape 3 : Ecrire les resultats
let file = File::create(output_path)?;
let mut writer = BufWriter::new(file);
let count = processed.len();
for record in &processed {
serde_json::to_writer(&mut writer, record)?;
writer.write_all(b"\n")?;
}
writer.flush()?;
Ok(count)
}
fn main() -> anyhow::Result<()> {
let count = parallel_process_jsonl("input.jsonl", "output.jsonl")?;
println!("Processed {} records in parallel", count);
Ok(())
}

Ce modele lit toutes les lignes, les traite en parallele avec par_iter() de rayon et ecrit les resultats sequentiellement. L'etape parallele gere l'analyse et la transformation sur tous les coeurs CPU disponibles. Pour les fichiers qui tiennent en memoire, cela peut atteindre une acceleration quasi lineaire avec le nombre de coeurs. Pour les tres gros fichiers, envisagez de decouper l'entree pour equilibrer l'utilisation memoire et le parallelisme.

E/S asynchrones avec tokio

Quand votre traitement JSONL implique des E/S reseau, comme la lecture depuis des endpoints HTTP ou l'ecriture vers du stockage distant, le runtime asynchrone de tokio vous permet de chevaucher les attentes d'E/S avec le calcul. Le trait tokio::io::AsyncBufReadExt fournit une methode lines() asynchrone qui reflete l'API BufReader synchrone.

E/S asynchrones avec tokio
use tokio::fs::File;
use tokio::io::{AsyncBufReadExt, BufReader};
use serde::Deserialize;
#[derive(Debug, Deserialize)]
struct ApiRecord {
id: u64,
url: String,
status: String,
}
async fn read_jsonl_async(path: &str) -> anyhow::Result<Vec<ApiRecord>> {
let file = File::open(path).await?;
let reader = BufReader::new(file);
let mut lines = reader.lines();
let mut records = Vec::new();
while let Some(line) = lines.next_line().await? {
let trimmed = line.trim().to_string();
if trimmed.is_empty() {
continue;
}
let record: ApiRecord = serde_json::from_str(&trimmed)?;
records.push(record);
}
Ok(records)
}
async fn write_jsonl_async(
path: &str,
records: &[ApiRecord],
) -> anyhow::Result<()> {
use tokio::io::AsyncWriteExt;
let mut file = File::create(path).await?;
for record in records {
let json = serde_json::to_string(record)?;
file.write_all(json.as_bytes()).await?;
file.write_all(b"\n").await?;
}
file.flush().await?;
Ok(())
}
#[tokio::main]
async fn main() -> anyhow::Result<()> {
let records = read_jsonl_async("api_logs.jsonl").await?;
println!("Read {} async records", records.len());
write_jsonl_async("output.jsonl", &records).await?;
Ok(())
}

L'approche asynchrone est plus benefique quand elle est combinee avec des operations reseau. Pour les E/S sur fichiers locaux purs, le BufReader synchrone est souvent plus rapide car l'async ajoute de la surcharge pour l'ordonnancement des taches. Utilisez tokio quand vous devez lire du JSONL depuis des flux HTTP, des buckets S3 ou d'autres sources reseau ou la latence d'E/S domine.

Gestion des erreurs : Result et l'operateur ?

Le type Result de Rust et l'operateur ? fournissent un modele de gestion d'erreurs propre et composable pour le traitement JSONL. Au lieu de blocs try-catch, chaque operation faillible retourne un Result que vous pouvez propager avec ? ou gerer localement avec match. Cela rend les chemins d'erreur explicites et impossibles a ignorer accidentellement.

Gestion des erreurs : Result et l'operateur ?
use serde::Deserialize;
use std::fs::File;
use std::io::{BufRead, BufReader};
use thiserror::Error;
#[derive(Error, Debug)]
enum JsonlError {
#[error("I/O error: {0}")]
Io(#[from] std::io::Error),
#[error("JSON parse error at line {line}: {source}")]
Parse {
line: usize,
source: serde_json::Error,
},
#[error("Validation error at line {line}: {message}")]
Validation {
line: usize,
message: String,
},
}
#[derive(Debug, Deserialize)]
struct Record {
id: u64,
name: String,
value: f64,
}
fn validate_record(record: &Record, line: usize) -> Result<(), JsonlError> {
if record.name.is_empty() {
return Err(JsonlError::Validation {
line,
message: "name cannot be empty".into(),
});
}
if record.value < 0.0 {
return Err(JsonlError::Validation {
line,
message: format!("value must be non-negative, got {}", record.value),
});
}
Ok(())
}
fn process_jsonl_with_errors(
path: &str,
) -> Result<Vec<Record>, JsonlError> {
let file = File::open(path)?; // Erreur E/S auto-convertie via #[from]
let reader = BufReader::new(file);
let mut records = Vec::new();
for (idx, line) in reader.lines().enumerate() {
let line_num = idx + 1;
let line = line?; // Propager les erreurs d'E/S
let trimmed = line.trim();
if trimmed.is_empty() {
continue;
}
let record: Record = serde_json::from_str(trimmed)
.map_err(|e| JsonlError::Parse { line: line_num, source: e })?;
validate_record(&record, line_num)?;
records.push(record);
}
Ok(records)
}
fn main() {
match process_jsonl_with_errors("data.jsonl") {
Ok(records) => println!("Successfully processed {} records", records.len()),
Err(JsonlError::Io(e)) => eprintln!("File error: {}", e),
Err(JsonlError::Parse { line, source }) => {
eprintln!("JSON error at line {}: {}", line, source);
}
Err(JsonlError::Validation { line, message }) => {
eprintln!("Validation error at line {}: {}", line, message);
}
}
}

Ce modele definit un enum d'erreur personnalise avec thiserror qui couvre tous les modes de defaillance : erreurs d'E/S, erreurs d'analyse JSON avec numeros de ligne et erreurs de validation specifiques au domaine. L'attribut #[from] permet la conversion automatique depuis std::io::Error, et l'operateur ? propage les erreurs en remontant la pile d'appels. Chaque variante d'erreur porte suffisamment de contexte pour produire un message de diagnostic clair, facilitant l'identification precise de la ligne ayant cause le probleme.

Crates Rust pour le traitement JSON

L'ecosysteme Rust offre plusieurs crates d'analyse JSON avec differentes caracteristiques de performance. serde_json est le choix standard, tandis que simd-json et sonic-rs repoussent les limites de performance en utilisant les instructions SIMD.

serde_json

Standard

Le standard de facto pour le JSON en Rust. Il s'integre parfaitement avec les macros derive de serde pour une serialisation sans code repetitif. Excellent pour la plupart des charges de travail JSONL avec une forte securite de type, des messages d'erreur complets et un large support de l'ecosysteme.

simd-json

Le plus rapide

Un portage Rust de simdjson qui utilise les instructions SIMD pour analyser du JSON a plusieurs gigaoctets par seconde. Il necessite des tampons d'entree mutables et fournit une API differente de serde_json, mais il peut etre 2 a 4 fois plus rapide pour les charges de travail intensives en analyse. Ideal pour les pipelines a haut debit.

sonic-rs

SIMD + serde

Une bibliotheque JSON acceleree par SIMD avec une API compatible serde. Elle vise a combiner la vitesse de simd-json avec l'ergonomie de serde_json. Supporte les modes d'analyse paresseux et impatient, ce qui en fait un bon choix quand vous avez besoin de performance sans reecrire votre code.

Essayez nos outils JSONL gratuits

Vous ne voulez pas ecrire de code ? Utilisez nos outils en ligne gratuits pour visualiser, valider et convertir des fichiers JSONL directement dans votre navigateur.

Travaillez avec des fichiers JSONL en ligne

Visualisez, validez et convertissez des fichiers JSONL jusqu'a 1 Go directement dans votre navigateur. Sans telechargement, 100% prive.

Questions frequemment posees

JSONL en Rust — serde_json, BufReader et patterns de stre...