JSONL w Rust: serde_json, BufReader i abstrakcje o zerowym koszcie

Kompletny przewodnik pracy z plikami JSONL (JSON Lines) w Rust. Naucz sie czytac, zapisywac, parsowac i strumieniowac dane JSONL przy uzyciu serde_json, BufReader, rayon do rownoleglosci i tokio do asynchronicznego I/O.

Ostatnia aktualizacja: luty 2026

Dlaczego Rust do JSONL?

Rust to doskonaly wybor do przetwarzania JSONL, gdy liczy sie wydajnosc, bezpieczenstwo pamieci i poprawnosc. Model wlasnosci eliminuje wyscigy danych na etapie kompilacji, abstrakcje o zerowym koszcie pozwalaja pisac lancuchy iteratorow wysokiego poziomu kompilowane do ciaslych petli, a serde_json jest jednym z najszybszych parserow JSON dostepnych w jakimkolwiek jezyku. Jesli budujesz potok danych, ktory musi przetwarzac miliony rekordow JSONL na sekunde, Rust daje Ci kontrole, aby to osiagnac bez poswiecania bezpieczenstwa.

JSONL (JSON Lines) przechowuje jeden obiekt JSON na linie, co czyni go idealnym do strumieniowania, logowania w trybie append-only i przetwarzania duzych zbiorow danych. BufReader w Rust czyta pliki linia po linii bez ladowania calego pliku do pamieci, a model iteratorow idealnie pasuje do liniowej struktury JSONL. W tym przewodniku nauczysz sie, jak czytac i zapisywac JSONL za pomoca BufReader, parsowac rekordy do silnie typowanych struktur za pomoca serde, przetwarzac rekordy rownolegle za pomoca rayon, obslugiwac asynchroniczne I/O za pomoca tokio i budowac solidna obsluge bledow za pomoca Result i operatora ?.

Czytanie plikow JSONL za pomoca BufReader

Biblioteka standardowa Rust udostepnia BufReader do wydajnego buforowanego czytania plikow. W polaczeniu z iteratorem lines() czyta pliki JSONL linia po linii z minimalnym narzutem pamieci. Kazda linia jest parsowana niezaleznie, co sprawia, ze podejscie to nadaje sie do plikow dowolnego rozmiaru.

Najprostsze podejscie opakowuje File w BufReader, iteruje po lines() i parsuje kazda linie za pomoca serde_json. Utrzymuje w pamieci tylko jedna linie naraz, niezaleznie od rozmiaru pliku.

Podstawowe czytanie JSONL za pomoca 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; // Pomin puste linie
}
match serde_json::from_str::<Value>(trimmed) {
Ok(value) => records.push(value),
Err(e) => {
eprintln!("Pomijanie nieprawidlowego JSON w linii {}: {}", line_num + 1, e);
}
}
}
Ok(records)
}
fn main() -> std::io::Result<()> {
let records = read_jsonl("data.jsonl")?;
println!("Zaladowano {} rekordow", records.len());
if let Some(first) = records.first() {
println!("Pierwszy rekord: {}", first);
}
Ok(())
}

Dodaj serde i serde_json do swojego Cargo.toml. Funkcja derive wlacza makra derive Serialize i Deserialize do definiowania typowanych struktur.

Zaleznosci Cargo.toml
[dependencies]
serde = { version = "1", features = ["derive"] }
serde_json = "1"
# Opcjonalnie: do rownoleglego przetwarzania
rayon = "1.10"
# Opcjonalnie: do asynchronicznego I/O
tokio = { version = "1", features = ["full"] }

Bezpieczne typowo parsowanie za pomoca serde_json

Jedna z najwiekszych zalet Rust do przetwarzania JSONL sa makra derive serde, ktore pozwalaja deserializowac kazda linie JSON bezposrednio do silnie typowanej struktury. Kompilator weryfikuje, ze Twoj kod poprawnie obsluguje dane, wylapujac niezgodnosci typow, brakujace pola i bledy strukturalne na etapie kompilacji, a nie w czasie dzialania.

Zdefiniuj strukture z Deserialize i uzyj serde_json::from_str do parsowania kazdej linii JSONL. Brakujace lub znieksztalcone pola generuja jasne komunikaty o bledach w momencie parsowania, zamiast powodowac paniki pozniej w programie.

Typowana deserializacja do struktur
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(())
}

Gdy rekordy JSONL maja zmienne schematy, uzyj serde_json::Value do elastycznego dostepu. Mozesz indeksowac wartosci za pomoca notacji nawiasowej lub uzywac metod as_str(), as_i64() do bezpiecznej konwersji typow.

Dynamiczne parsowanie za pomoca 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)?;
// Dostep do pol dynamicznie
if let Some(name) = value.get("name").and_then(Value::as_str) {
println!("Imie: {}", name);
}
if let Some(age) = value.get("age").and_then(Value::as_i64) {
println!("Wiek: {}", age);
}
// Sprawdz czy pole istnieje
if value.get("email").is_some() {
println!("Posiada pole email");
}
}
Ok(())
}

Zapisywanie plikow JSONL w Rust

Zapisywanie JSONL to odwrotnosc czytania: serializuj kazdy rekord do ciagu JSON, dodaj nowa linie i zapisz do pliku. Uzycie BufWriter i serde_json::to_string zapewnia zarowno poprawnosc, jak i wydajnosc.

Serializuj kazda strukture za pomoca serde_json::to_string() i zapisz ja z nowa linia. Makro derive Serialize automatycznie obsluguje konwersje z typow Rust do JSON.

Podstawowy zapis JSONL
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!("Zapisano {} rekordow", records.len());
Ok(())
}

Przy zapisywaniu duzych plikow JSONL opakuj File w BufWriter, aby zmniejszyc liczbe wywolan systemowych. Laczy to male zapisy w wieksze operacje oprozniana bufora, znacznie poprawiajac przepustowosc.

Buforowany zapis dla duzych plikow wyjsciowych
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(())
}

Rownolegle przetwarzanie za pomoca rayon

Crate rayon w Rust zapewnia rownolegle przetwarzanie danych z minimalnymi zmianami w kodzie. Wywolujac par_iter() zamiast iter(), rayon automatycznie rozdziela prace miedzy wszystkie rdzenie procesora za pomoca puli watkow z kradziezy zadan. Jest to szczegolnie skuteczne dla transformacji JSONL intensywnie obciazajacych procesor, gdzie parsowanie i przetwarzanie dominuja nad czasem I/O.

Rownolegle przetwarzanie za pomoca 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> {
// Krok 1: Wczytaj wszystkie linie do pamieci
let file = File::open(input_path)?;
let reader = BufReader::new(file);
let lines: Vec<String> = reader.lines().collect::<Result<_, _>>()?;
// Krok 2: Parsuj i transformuj rownolegle
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();
// Krok 3: Zapisz wyniki
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!("Przetworzono {} rekordow rownolegle", count);
Ok(())
}

Ten wzorzec czyta wszystkie linie, przetwarza je rownolegle za pomoca par_iter() rayon i zapisuje wyniki sekwencyjnie. Krok rownolegle obsluguje parsowanie i transformacje na wszystkich dostepnych rdzeniach procesora. Dla plikow mieszczacych sie w pamieci mozna osiagnac niemal liniowe przyspieszenie wraz z liczba rdzeni. Dla bardzo duzych plikow rozwaiz dzielenie danych wejsciowych, aby zrownowazyc zuzycie pamieci i rownolegle przetwarzanie.

Asynchroniczne I/O za pomoca tokio

Gdy przetwarzanie JSONL obejmuje sieciowe I/O, takie jak czytanie z endpointow HTTP lub zapis do zdalnego magazynu, srodowisko uruchomieniowe tokio pozwala nakladac oczekiwanie na I/O z obliczeniami. Trait tokio::io::AsyncBufReadExt zapewnia asynchroniczna metode lines(), ktora odzwierciedla synchroniczne API BufReader.

Asynchroniczne I/O za pomoca 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!("Odczytano {} rekordow asynchronicznie", records.len());
write_jsonl_async("output.jsonl", &records).await?;
Ok(())
}

Podejscie asynchroniczne jest najbardziej korzystne w polaczeniu z operacjami sieciowymi. Dla czystego plikowego I/O na lokalnych dyskach synchroniczny BufReader jest czesto szybszy, poniewaz async dodaje narzut na planowanie zadan. Uzyj tokio, gdy musisz czytac JSONL ze strumieni HTTP, koszy S3 lub innych zrodel sieciowych, gdzie opoznienie I/O dominuje.

Obsluga bledow: Result i operator ?

Typ Result w Rust i operator ? zapewniaja czysty, komponowalny wzorzec obslugi bledow dla przetwarzania JSONL. Zamiast blokow try-catch kazda operacja mogaca zakonczyc sie niepowodzeniem zwraca Result, ktory mozesz propagowac za pomoca ? lub obsluzyc lokalnie za pomoca match. Sprawia to, ze sciezki bledow sa jawne i niemozliwe do przypadkowego zignorowania.

Obsluga bledow: Result i operator ?
use serde::Deserialize;
use std::fs::File;
use std::io::{BufRead, BufReader};
use thiserror::Error;
#[derive(Error, Debug)]
enum JsonlError {
#[error("Blad I/O: {0}")]
Io(#[from] std::io::Error),
#[error("Blad parsowania JSON w linii {line}: {source}")]
Parse {
line: usize,
source: serde_json::Error,
},
#[error("Blad walidacji w linii {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: "nazwa nie moze byc pusta".into(),
});
}
if record.value < 0.0 {
return Err(JsonlError::Validation {
line,
message: format!("wartosc musi byc nieujemna, otrzymano {}", record.value),
});
}
Ok(())
}
fn process_jsonl_with_errors(
path: &str,
) -> Result<Vec<Record>, JsonlError> {
let file = File::open(path)?; // Blad Io konwertowany automatycznie przez #[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?; // Propagacja bledow I/O
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!("Pomyslnie przetworzono {} rekordow", records.len()),
Err(JsonlError::Io(e)) => eprintln!("Blad pliku: {}", e),
Err(JsonlError::Parse { line, source }) => {
eprintln!("Blad JSON w linii {}: {}", line, source);
}
Err(JsonlError::Validation { line, message }) => {
eprintln!("Blad walidacji w linii {}: {}", line, message);
}
}
}

Ten wzorzec definiuje niestandardowe wyliczenie bledow za pomoca thiserror, ktore obejmuje wszystkie tryby awarii: bledy I/O, bledy parsowania JSON z numerami linii i bledy walidacji specyficzne dla domeny. Atrybut #[from] umozliwia automatyczna konwersje z std::io::Error, a operator ? propaguje bledy w gore stosu wywolan. Kazdy wariant bledu niesie wystarczajacy kontekst, aby wygenerowac jasny komunikat diagnostyczny, ulatwiajac dokladne wskazanie, ktora linia spowodowala problem.

Crate'y Rust do przetwarzania JSON

Ekosystem Rust oferuje kilka crate'ow do parsowania JSON o roznych charakterystykach wydajnosci. serde_json to standardowy wybor, podczas gdy simd-json i sonic-rs przesuwa granice wydajnosci za pomoca instrukcji SIMD.

serde_json

Standard

De facto standard dla JSON w Rust. Integruje sie bezproblemowo z makrami derive serde do bezkosztowej serializacji. Doskonaly dla wiekszosci zadan JSONL z silnym bezpieczenstwem typow, kompleksowymi komunikatami o bledach i szerokim wsparciem ekosystemu.

simd-json

Najszybszy

Port simdjson do Rust wykorzystujacy instrukcje SIMD do parsowania JSON z predkoscia wielu gigabajtow na sekunde. Wymaga mutowalnych buforow wejsciowych i zapewnia inne API niz serde_json, ale moze byc 2-4x szybszy dla obciazen intensywnie parsujacych. Najlepiej nadaje sie do potokow o duzej przepustowosci.

sonic-rs

SIMD + serde

Biblioteka JSON z akceleracja SIMD z API kompatybilnym z serde. Laczy szybkosc simd-json z ergonomia serde_json. Obsluguje zarowno leniwe, jak i zachlanne tryby parsowania, co czyni ja dobrym wyborem, gdy potrzebujesz wydajnosci bez przepisywania kodu.

Wyprobuj nasze darmowe narzedzia JSONL

Nie chcesz pisac kodu? Uzyj naszych darmowych narzedzi online, aby przegladac, walidowac i konwertowac pliki JSONL bezposrednio w przegladarce.

Pracuj z plikami JSONL online

Przegladaj, waliduj i konwertuj pliki JSONL do 1GB bezposrednio w przegladarce. Bez przesylania, 100% prywatnosci.

Czesto zadawane pytania

JSONL w Rust — serde_json, BufReader i wzorce strumieniow...