Rust CSV Data
CSV is rows of values split by commas. The csv crate reads rows as plain text records or fills your structs directly, and it writes rows back out.
Read rows as plain text records
records() skips the header row and hands you each row as text.
let text = "name,age\nAda,36\nGrace,45\n";
let mut reader = csv::Reader::from_reader(text.as_bytes());
for result in reader.records() {
let record = result.unwrap();
println!("{} -> {}", record.get(0).unwrap(), record.get(1).unwrap());
}Read rows into structs
Add Deserialize to the struct and the reader fills its fields by column name.
#[derive(Deserialize)]
struct Person { name: String, age: u32 }
let mut reader = csv::Reader::from_reader(text.as_bytes());
for result in reader.deserialize() {
let person: Person = result.unwrap();
println!("{} is {}", person.name, person.age);
}A bad row arrives as an Err
deserialize gives one Result per row: bad rows surface without killing the loop.
let bad = "name,age\nAda,thirty-six\n";
let mut reader = csv::Reader::from_reader(bad.as_bytes());
for result in reader.deserialize::<Person>() {
match result {
Ok(p) => println!("ok: {}", p.name),
Err(_) => println!("bad row skipped"),
}
}Write rows with a Writer
write_record takes plain values. The bytes come back from into_inner.
let mut writer = csv::Writer::from_writer(Vec::new());
writer.write_record(["name", "age"]).unwrap();
writer.write_record(["Ada", "36"]).unwrap();
let bytes = writer.into_inner().unwrap();
print!("{}", String::from_utf8(bytes).unwrap());Example
use serde::{Deserialize, Serialize};
#[derive(Deserialize, Serialize)]
struct Product {
name: String,
price: f64,
}
fn main() {
let text = "name,price\napple,0.5\nlaptop,999.0\nbanana,0.25\n";
let mut reader = csv::Reader::from_reader(text.as_bytes());
let mut kept = Vec::new();
for result in reader.deserialize() {
let product: Product = result.unwrap();
if product.price < 1.0 {
kept.push(product);
}
}
let mut writer = csv::Writer::from_writer(Vec::new());
for product in &kept {
writer.serialize(product).unwrap();
}
let bytes = writer.into_inner().unwrap();
print!("{}", String::from_utf8(bytes).unwrap());
println!("kept = {}", kept.len());
}
Key points
- Add csv = "1" to your Cargo.toml.
- The reader treats the first row as the headers row by default, so records() starts at the real data.
- Use csv::Reader::from_reader to read from anything that implements Read: a file, a string's bytes, or a network stream — from_reader(text.as_bytes()) in the snippets stands in for a real file.
- Use records() to read rows as plain text: each row is a StringRecord, and record.get(0) fetches a column by position.
- Use reader.deserialize() to read rows straight into a struct.
- The struct has Deserialize derived.
- Each column fills the matching field by name.
- Each row arrives as a Result.
- Use csv::Writer::from_writer(Vec::new()) plus write_record or serialize.
- Together they build CSV text in memory.
- into_inner() hands back the bytes you built.
- A missing column or a wrong number can cause it.
- Your loop can then skip or report it instead of crashing.
- Spreadsheets export CSV, and datasets ship in it.
- CSV stores data as comma-split rows of values.
The closing program reads a small products list. It keeps only the items under 1.0. It writes the survivors back out as CSV. And it reports the count.
Note
A row that does not fit your struct arrives as an Err.
Try it Yourself
Edit the code, then press Run.
Running is simulated · Rust Playground
Exercise
By default, how does csv::Reader treat the first row of your input?