---
title: CSV Examples
description: Complete examples for CSV file processing with Spring Batch RS
sidebar:
  order: 1
---

import { Tabs, TabItem, Card, CardGrid, Aside } from '@astrojs/starlight/components';

<Aside type="tip">
  View the complete source: [examples/csv_processing.rs](https://github.com/sboussekeyt/spring-batch-rs/blob/main/examples/csv_processing.rs)
</Aside>

This page provides comprehensive examples for working with CSV files using Spring Batch RS.

## Basic CSV Reading

### Reading from a File

```rust
use spring_batch_rs::{
    core::{job::JobBuilder, step::StepBuilder, item::PassThroughProcessor},
    item::csv::CsvItemReaderBuilder,
    BatchError,
};
use serde::Deserialize;

#[derive(Debug, Deserialize, Clone)]
struct Product {
    id: u32,
    name: String,
    price: f64,
    category: String,
}

fn main() -> Result<(), BatchError> {
    // Create CSV reader
    let reader = CsvItemReaderBuilder::<Product>::new()
        .has_headers(true)          // First row contains headers
        .delimiter(b',')            // Comma delimiter (default)
        .from_path("products.csv")?; // Read from file

    // Use logger writer to see the output
    let writer = spring_batch_rs::item::logger::LoggerItemWriterBuilder::new()
        .log_level(log::Level::Info)
        .build();

    let processor = PassThroughProcessor::<Product>::new();

    let step = StepBuilder::new("read-csv")
        .chunk(10)
        .reader(&reader)
        .processor(&processor)
        .writer(&writer)
        .build();

    let job = JobBuilder::new().start(&step).build();
    job.run().map(|_| ())
}
```

### Reading from a String

```rust
use spring_batch_rs::item::csv::CsvItemReaderBuilder;

let csv_data = r#"id,name,price,category
1,Laptop,999.99,Electronics
2,Coffee Mug,12.99,Kitchen
3,Notebook,5.99,Office"#;

let reader = CsvItemReaderBuilder::<Product>::new()
    .has_headers(true)
    .from_reader(csv_data.as_bytes());
```

## Basic CSV Writing

### Writing to a File

```rust
use spring_batch_rs::item::csv::CsvItemWriterBuilder;
use serde::Serialize;

#[derive(Serialize)]
struct Product {
    id: u32,
    name: String,
    price: f64,
}

let writer = CsvItemWriterBuilder::new()
    .has_headers(true)           // Write header row
    .delimiter(b',')             // Comma delimiter
    .from_path("output.csv")?;   // Write to file
```

## CSV to JSON Transformation

```rust
use spring_batch_rs::{
    core::{job::JobBuilder, step::StepBuilder, item::PassThroughProcessor},
    item::{
        csv::CsvItemReaderBuilder,
        json::JsonItemWriterBuilder,
    },
    BatchError,
};
use serde::{Deserialize, Serialize};

#[derive(Debug, Clone, Deserialize, Serialize)]
struct Product {
    id: u32,
    name: String,
    price: f64,
    category: String,
}

fn main() -> Result<(), BatchError> {
    let csv_data = r#"id,name,price,category
1,Laptop,999.99,Electronics
2,Coffee Mug,12.99,Kitchen
3,Notebook,5.99,Office
4,Wireless Mouse,29.99,Electronics"#;

    let reader = CsvItemReaderBuilder::<Product>::new()
        .has_headers(true)
        .from_reader(csv_data.as_bytes());

    let writer = JsonItemWriterBuilder::<Product>::new()
        .pretty_formatter(true)
        .from_path("products.json")?;

    let processor = PassThroughProcessor::<Product>::new();

    let step = StepBuilder::new("csv-to-json")
        .chunk(100)
        .reader(&reader)
        .processor(&processor)
        .writer(&writer)
        .build();

    let job = JobBuilder::new().start(&step).build();
    job.run().map(|_| ())
}
```

## CSV Processing with Transformation

### Applying Discounts

```rust
use spring_batch_rs::core::item::ItemProcessor;

#[derive(Deserialize, Clone)]
struct RawProduct {
    id: u32,
    name: String,
    price: f64,
    category: String,
}

#[derive(Serialize)]
struct DiscountedProduct {
    id: u32,
    name: String,
    original_price: f64,
    discounted_price: f64,
    discount_percent: u32,
    category: String,
}

struct DiscountProcessor;

impl ItemProcessor<RawProduct, DiscountedProduct> for DiscountProcessor {
    fn process(&self, item: RawProduct) -> ItemProcessorResult<DiscountedProduct> {
        // Category-based discounts
        let discount_percent = match item.category.as_str() {
            "Electronics" => 15,
            "Kitchen" => 10,
            "Office" => 5,
            _ => 0,
        };

        let discount_multiplier = 1.0 - (discount_percent as f64 / 100.0);
        let discounted_price = item.price * discount_multiplier;

        Ok(Some(DiscountedProduct {
            id: item.id,
            name: item.name,
            original_price: item.price,
            discounted_price,
            discount_percent,
            category: item.category,
        }))
    }
}
```

## Advanced CSV Examples

### Custom Delimiters and Quoting

```rust
let reader = CsvItemReaderBuilder::<Product>::new()
    .has_headers(true)
    .delimiter(b';')              // Semicolon delimiter
    .quote(b'"')                  // Custom quote character
    .flexible(true)               // Allow variable number of fields
    .from_path("products.csv")?;
```

### CSV Without Headers

```rust
#[derive(Deserialize)]
struct Product {
    #[serde(rename = "0")]  // Map to first column
    id: u32,
    #[serde(rename = "1")]  // Map to second column
    name: String,
    #[serde(rename = "2")]  // Map to third column
    price: f64,
}

let reader = CsvItemReaderBuilder::<Product>::new()
    .has_headers(false)  // No header row
    .from_path("products.csv")?;
```

### Handling CSV Errors with Fault Tolerance

```rust
use spring_batch_rs::{
    core::{job::JobBuilder, step::StepBuilder, item::ItemProcessor},
    item::{csv::CsvItemReaderBuilder, json::JsonItemWriterBuilder},
    BatchError,
};

#[derive(Deserialize, Clone)]
struct RawData {
    id: String,
    value: String,
    amount: String,
}

#[derive(Serialize)]
struct ValidatedData {
    id: u32,
    value: f64,
    amount: f64,
}

struct ValidationProcessor;

impl ItemProcessor<RawData, ValidatedData> for ValidationProcessor {
    fn process(&self, item: RawData) -> ItemProcessorResult<ValidatedData> {
        // Parse and validate
        let id = item.id.parse::<u32>()
            .map_err(|e| BatchError::ItemProcessor(
                format!("Invalid ID '{}': {}", item.id, e)
            ))?;

        let value = item.value.parse::<f64>()
            .map_err(|e| BatchError::ItemProcessor(
                format!("Invalid value '{}': {}", item.value, e)
            ))?;

        let amount = item.amount.parse::<f64>()
            .map_err(|e| BatchError::ItemProcessor(
                format!("Invalid amount '{}': {}", item.amount, e)
            ))?;

        // Business validation
        if value < 0.0 || amount < 0.0 {
            return Err(BatchError::ItemProcessor(
                "Negative values not allowed".to_string()
            ));
        }

        Ok(Some(ValidatedData { id, value, amount }))
    }
}

fn main() -> Result<(), BatchError> {
    let reader = CsvItemReaderBuilder::<RawData>::new()
        .has_headers(true)
        .from_path("raw_data.csv")?;

    let writer = JsonItemWriterBuilder::<Product>::new()
        .pretty_formatter(true)
        .from_path("validated_data.json")?;

    let processor = ValidationProcessor;

    let step = StepBuilder::new("validate-csv")
        .chunk(50)
        .reader(&reader)
        .processor(&processor)
        .writer(&writer)
        .skip_limit(10)  // Skip up to 10 invalid records
        .build();

    let job = JobBuilder::new().start(&step).build();
    job.run().map(|_| ())
}
```

## Real-World Use Cases

### Data Enrichment

Read CSV, enrich with external data, write back to CSV:

```rust
use std::collections::HashMap;

#[derive(Deserialize, Clone)]
struct CustomerOrder {
    order_id: u32,
    customer_id: u32,
    product_id: u32,
    quantity: u32,
}

#[derive(Serialize)]
struct EnrichedOrder {
    order_id: u32,
    customer_name: String,
    product_name: String,
    quantity: u32,
    total_price: f64,
}

struct EnrichmentProcessor {
    customers: HashMap<u32, String>,
    products: HashMap<u32, (String, f64)>,  // (name, price)
}

impl ItemProcessor<CustomerOrder, EnrichedOrder> for EnrichmentProcessor {
    fn process(&self, item: CustomerOrder) -> ItemProcessorResult<EnrichedOrder> {
        let customer_name = self.customers
            .get(&item.customer_id)
            .cloned()
            .unwrap_or_else(|| "Unknown".to_string());

        let (product_name, unit_price) = self.products
            .get(&item.product_id)
            .cloned()
            .unwrap_or_else(|| ("Unknown".to_string(), 0.0));

        let total_price = unit_price * item.quantity as f64;

        Ok(Some(EnrichedOrder {
            order_id: item.order_id,
            customer_name,
            product_name,
            quantity: item.quantity,
            total_price,
        }))
    }
}

fn main() -> Result<(), BatchError> {
    // Build lookup tables
    let mut customers = HashMap::new();
    customers.insert(1, "Alice".to_string());
    customers.insert(2, "Bob".to_string());

    let mut products = HashMap::new();
    products.insert(101, ("Laptop".to_string(), 999.99));
    products.insert(102, ("Mouse".to_string(), 29.99));

    let processor = EnrichmentProcessor { customers, products };

    let reader = CsvItemReaderBuilder::<CustomerOrder>::new()
        .has_headers(true)
        .from_path("orders.csv")?;

    let writer = CsvItemWriterBuilder::new()
        .has_headers(true)
        .from_path("enriched_orders.csv")?;

    let step = StepBuilder::new("enrich-orders")
        .chunk(100)
        .reader(&reader)
        .processor(&processor)
        .writer(&writer)
        .build();

    let job = JobBuilder::new().start(&step).build();
    job.run().map(|_| ())
}
```

### Filtering Records with a Processor

Return `Ok(None)` from a processor to silently discard items. Filtered items are counted in
`StepExecution::filter_count` and are **not** passed to the writer.

```rust
use spring_batch_rs::{
    core::{
        item::{ItemProcessor, ItemProcessorResult},
        job::JobBuilder,
        step::StepBuilder,
    },
    item::{csv::CsvItemReaderBuilder, json::JsonItemWriterBuilder},
    BatchError,
};
use serde::{Deserialize, Serialize};
use std::env::temp_dir;

#[derive(Debug, Deserialize, Serialize, Clone)]
struct Person {
    name: String,
    age: u32,
}

/// Keeps only adults (age >= 18). Minors are filtered with `Ok(None)`.
#[derive(Default)]
struct AdultFilter;

impl ItemProcessor<Person, Person> for AdultFilter {
    fn process(&self, item: &Person) -> ItemProcessorResult<Person> {
        if item.age >= 18 {
            Ok(Some(item.clone())) // keep
        } else {
            Ok(None) // discard — counted in filter_count
        }
    }
}

fn main() -> Result<(), BatchError> {
    let csv_data = "name,age\nAlice,30\nBob,16\nCharlie,25\nDiana,15\nEve,42\nFrank,17\n";

    // 1. Build reader from inline CSV
    let reader = CsvItemReaderBuilder::<Person>::new()
        .has_headers(true)
        .from_reader(csv_data.as_bytes());

    // 2. Build JSON writer
    let output_path = temp_dir().join("adults.json");
    let writer = JsonItemWriterBuilder::<Person>::new().from_path(&output_path);

    // 3. Build and run step with filter processor
    let processor = AdultFilter::default();
    let step = StepBuilder::new("filter-adults")
        .chunk::<Person, Person>(10)
        .reader(&reader)
        .processor(&processor)
        .writer(&writer)
        .build();

    let job = JobBuilder::new().start(&step).build();
    job.run()?;

    // 4. Inspect filter statistics
    let step_exec = job.get_step_execution("filter-adults").unwrap();
    println!("Read:     {}", step_exec.read_count);    // 6
    println!("Filtered: {}", step_exec.filter_count);  // 3 (minors)
    println!("Written:  {}", step_exec.write_count);   // 3 (adults)

    Ok(())
}
```

Run the standalone example:

```bash
cargo run --example filter_records_from_csv_with_processor --features csv,json
```

<Aside type="tip">
  Filtering with `Ok(None)` does **not** count toward `skip_limit`. Use it for intentional business
  filtering (e.g. age, category, status) and use `Err(BatchError)` + `skip_limit` for error recovery.
</Aside>

## Performance Tips

<CardGrid>
  <Card title="Chunk Size" icon="rocket">
    Use larger chunks (500-1000) for CSV files to reduce I/O overhead
  </Card>
  <Card title="Buffering" icon="setting">
    CSV readers use buffered I/O by default for optimal performance
  </Card>
  <Card title="Memory Usage" icon="warning">
    For very large CSV files, use appropriate chunk sizes to control memory
  </Card>
</CardGrid>

## Complete Example: Sales Report Generator

This example demonstrates a complete sales processing pipeline:

```rust
use spring_batch_rs::{
    core::{job::JobBuilder, step::StepBuilder, item::ItemProcessor},
    item::{csv::CsvItemReaderBuilder, csv::CsvItemWriterBuilder},
    BatchError,
};
use serde::{Deserialize, Serialize};

#[derive(Deserialize, Clone)]
struct SaleRecord {
    date: String,
    product_id: u32,
    product_name: String,
    quantity: u32,
    unit_price: f64,
    region: String,
}

#[derive(Serialize)]
struct ProcessedSale {
    date: String,
    product_id: u32,
    product_name: String,
    quantity: u32,
    unit_price: f64,
    total_amount: f64,
    tax: f64,
    final_amount: f64,
    region: String,
    sales_tier: String,
}

struct SalesProcessor {
    tax_rate: f64,
}

impl ItemProcessor<SaleRecord, ProcessedSale> for SalesProcessor {
    fn process(&self, item: SaleRecord) -> ItemProcessorResult<ProcessedSale> {
        let total_amount = item.unit_price * item.quantity as f64;
        let tax = total_amount * self.tax_rate;
        let final_amount = total_amount + tax;

        let sales_tier = if total_amount < 100.0 {
            "Small"
        } else if total_amount < 1000.0 {
            "Medium"
        } else {
            "Large"
        }.to_string();

        Ok(Some(ProcessedSale {
            date: item.date,
            product_id: item.product_id,
            product_name: item.product_name,
            quantity: item.quantity,
            unit_price: item.unit_price,
            total_amount,
            tax,
            final_amount,
            region: item.region,
            sales_tier,
        }))
    }
}

fn main() -> Result<(), BatchError> {
    let reader = CsvItemReaderBuilder::<SaleRecord>::new()
        .has_headers(true)
        .from_path("sales_input.csv")?;

    let writer = CsvItemWriterBuilder::new()
        .has_headers(true)
        .from_path("sales_processed.csv")?;

    let processor = SalesProcessor { tax_rate: 0.08 };  // 8% tax

    let step = StepBuilder::new("process-sales")
        .chunk(500)
        .reader(&reader)
        .processor(&processor)
        .writer(&writer)
        .build();

    let job = JobBuilder::new().start(&step).build();

    println!("🚀 Starting sales processing job...");
    job.run()?;
    println!("✅ Sales processing completed!");

    Ok(())
}
```

## Next Steps

- [JSON Examples →](/examples/json/)
- [XML Examples →](/examples/xml/)
- [Database Examples →](/examples/database/)
- [Item Readers & Writers Overview →](/item-readers-writers/overview/)
