rust 41 lines · 7 steps

Custom CSV deserialization with serde in Rust

Parsing a CSV of transactions into typed structs, with field renaming, defaults, and a custom money parser.

Explained by highlit
1use std::error::Error;
2use std::path::Path;
3 
4use serde::Deserialize;
5 
6#[derive(Debug, Deserialize)]
7struct Transaction {
8 id: u64,
9 #[serde(rename = "date")]
10 booked_at: String,
11 description: String,
12 #[serde(deserialize_with = "parse_cents")]
13 amount: i64,
14 #[serde(default)]
15 reconciled: bool,
16}
17 
18fn parse_cents<'de, D>(deserializer: D) -> Result<i64, D::Error>
19where
20 D: serde::Deserializer<'de>,
21{
22 let raw = String::deserialize(deserializer)?;
23 let cleaned = raw.trim().replace(['$', ','], "");
24 let dollars: f64 = cleaned.parse().map_err(serde::de::Error::custom)?;
25 Ok((dollars * 100.0).round() as i64)
26}
27 
28fn load_transactions(path: impl AsRef<Path>) -> Result<Vec<Transaction>, Box<dyn Error>> {
29 let mut reader = csv::ReaderBuilder::new()
30 .trim(csv::Trim::All)
31 .flexible(false)
32 .from_path(path)?;
33 
34 let mut transactions = Vec::new();
35 for result in reader.deserialize() {
36 let record: Transaction = result?;
37 transactions.push(record);
38 }
39 
40 Ok(transactions)
41}
01 / 01
STEP 01

Walkthrough

Space play step click any line
Three takeaways
  1. 1serde attributes let you map messy external data onto clean, typed fields without hand-written parsing loops.
  2. 2A deserialize_with function turns any string into a domain value while still routing failures through serde's error channel.
  3. 3Storing money as integer cents sidesteps the rounding pitfalls of holding currency in floating point.

Related explainers

Share this explainer

Here's the card — post it anywhere.

Custom CSV deserialization with serde in Rust — share card
Made with highlit — turn any snippet into a walkthrough like this in about a minute.
Explain your code