Working with CSV in Rust: libraries, code examples and best practices
CSV (Comma-Separated Values) is one of the most common formats for storing tabular data. In the Rust ecosystem, there is a powerful and flexible csv library which, combined with the serde crate, allows you to conveniently read, write, and transform CSV data. In this article, we will thoroughly examine how to work with CSV in Rust, starting from basic parsing to serialization of complex structures.
Installing and adding dependencies
To get started, add the following dependencies to your Cargo.toml file:
[dependencies]csv = "1.3"serde = { version = "1.0", features = ["derive"] }The csv library provides a low-level API for reading and writing, while serde provides automatic deserialization of CSV rows into Rust structures.
Reading a CSV file line by line
The simplest way to read a CSV is to use the Reader from the csv crate. Let's look at an example with the users.csv file:
use csv::Reader;use std::error::Error;
fn main() -> Result<(), Box<dyn Error>> { let mut rdr = Reader::from_path("users.csv")?;
for result in rdr.records() { let record = result?; println!("{:?}", record); } Ok(())}This code outputs each row as a vector of strings StringRecord. However, in practice, it is more common to work with typed data.
Deserialization into structures with serde
Let's declare a structure that corresponds to the CSV columns:
use serde::Deserialize;
#[derive(Debug, Deserialize)]struct User { name: String, age: u32, city: String,}Now you can read records directly into User instances:
use csv::Reader;use std::error::Error;
fn main() -> Result<(), Box<dyn Error>> { let mut rdr = Reader::from_path("users.csv")?;
for result in rdr.deserialize() { let user: User = result?; println!("{} ({} years old) from {}", user.name, user.age, user.city); } Ok(())}The library automatically maps CSV headers to structure fields. If the order or column names differ, use the #[serde(rename = "...")] attribute.
Writing a CSV file
Creating a CSV file is done using the Writer. Writing structured data via serde:
use csv::Writer;use serde::Serialize;use std::error::Error;
#[derive(Debug, Serialize)]struct Product { id: u32, name: String, price: f64,}
fn main() -> Result<(), Box<dyn Error>> { let mut wtr = Writer::from_path("products.csv")?;
let products = vec![ Product { id: 1, name: "Laptop".into(), price: 75000.0 }, Product { id: 2, name: "Mouse".into(), price: 1500.0 }, ];
for product in &products { wtr.serialize(product)?; }
wtr.flush()?; println!("File products.csv successfully written"); Ok(())}Note: the Writer automatically adds headers based on the structure field names.
Configuring delimiters and quotes
By default, a comma and double quotes are used. If your file uses a semicolon (for example, in European locales), configure the ReaderBuilder:
use csv::ReaderBuilder;
let mut rdr = ReaderBuilder::new() .delimiter(b';') .has_headers(true) .from_path("data.tsv")?;The WriterBuilder is configured similarly for writing.
Error handling and invalid data
Also in library
Ardalis.Specification for C# (.NET): Complete Guide with Examples