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.
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.
[dependencies]serde = { version = "1", features = ["derive"] }serde_json = "1"# Optionnel : pour le traitement parallelerayon = "1.10"# Optionnel : pour les E/S asynchronestokio = { 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.
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.
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 champsif 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 existeif 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.
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.
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.
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 memoirelet file = File::open(input_path)?;let reader = BufReader::new(file);let lines: Vec<String> = reader.lines().collect::<Result<_, _>>()?;// Etape 2 : Analyser et transformer en parallelelet 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 resultatslet 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.
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.
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/Slet 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
StandardLe 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 rapideUn 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 + serdeUne 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.