How to read a CSV file into structs
Short answer: Open the file with csv::Reader::from_path, then call deserialize() into a struct that derives Deserialize. The first row is the header, and its column names are matched against the struct’s field names — #[serde(rename = "...")] bridges the gap when they don’t match.
This continues the json-demo project from the earlier pages in this chapter — cargo new json-demo, with one more crate:
[dependencies]csv = "1.4.0"serde = { version = "1.0.229", features = ["derive"] }Each block below writes the file it reads before reading it, so it runs on its own — in your own code the file is already there and you delete that line.
A flat CSV is a struct
Section titled “A flat CSV is a struct”name,age,email,signupuse serde::Deserialize;use std::fs;
#[derive(Debug, Deserialize)]struct Person { name: String, age: u8, email: String,}
fn main() { fs::write( "people.csv", "name,age,email,signup\nAda Lovelace,36,[email protected],2024-01-15\nGrace Hopper,45,[email protected],2024-02-20\n", ) .unwrap();
let mut reader = csv::Reader::from_path("people.csv").unwrap(); for person in reader.deserialize::<Person>() { println!("{:?}", person.unwrap()); }}Person { name: "Ada Lovelace", age: 36, email: "[email protected]" }Person { name: "Grace Hopper", age: 45, email: "[email protected]" }deserialize() hands back an iterator of Result<Person, csv::Error>, one item per row with the header excluded. The header row names the fields, so a column the struct doesn’t have is ignored — signup never reaches Person, the same way an unknown JSON key is dropped. The mapping goes through the names, not the positions, so column order doesn’t matter either.
Headers that do not match
Section titled “Headers that do not match”A CSV exported from a spreadsheet usually has capitals, spaces or punctuation in its header row, and the match is exact, case included. #[serde(rename = "...")] on the field is the fix — the same attribute the JSON page uses against an API you cannot change:
age,Full Name,E-mail Addressuse serde::Deserialize;use std::fs;
#[derive(Debug, Deserialize)]struct Person { #[serde(rename = "Full Name")] name: String, age: u8, #[serde(rename = "E-mail Address")] email: String,}
fn main() { fs::write( "users.csv", "age,Full Name,E-mail Address\n36,Ada Lovelace,[email protected]\n45,Grace Hopper,[email protected]\n", ) .unwrap();
let mut reader = csv::Reader::from_path("users.csv").unwrap(); for person in reader.deserialize::<Person>() { println!("{:?}", person.unwrap()); }}Person { name: "Ada Lovelace", age: 36, email: "[email protected]" }Person { name: "Grace Hopper", age: 45, email: "[email protected]" }age needs no rename: it already matches. Note that the column order here is not the struct’s field order, and the records still come out right, because the header decides the mapping.
What a mismatched header looks like
Section titled “What a mismatched header looks like”Capitalised headers with no rename do not fail silently. The error names the field it could not find, which is the hint that a rename is what’s missing:
use serde::Deserialize;use std::fs;
#[derive(Debug, Deserialize)]struct Person { name: String, age: u8, email: String,}
fn main() { fs::write( "people.csv", ) .unwrap();
let mut reader = csv::Reader::from_path("people.csv").unwrap(); for person in reader.deserialize::<Person>() { match person { Ok(person) => println!("{person:?}"), Err(e) => println!("error: {e}"), } }}error: CSV deserialize error: record 1 (line: 2, byte: 15): missing field `name`The position in the error — record 1 (line: 2, byte: 15) — points at the first data row, and nothing is printed before it, so a file whose header is wrong yields no records at all, not a file of default values. unwrap() on the item instead of the match above turns the same error into a panic, which is what a quick script usually does.
- How to rename JSON fields with serde —
renameandrename_allagainst an API you cannot change - How to parse a TOML config file — the same derive against a different format
- Back to Working with JSON, TOML and CSV
The next page in this chapter writes CSV from structs, where the quoting rules surprise people.