wombat2k/fastcsv
A high-performance CSV parser and writer for Swift
Overview
FastCSV is a high-performance CSV parser and writer for Swift. The parser processes large CSV files with minimal memory overhead through streaming and zero-copy techniques. The writer provides Codable round-tripping — read CSV into structs, transform, write back out.
Features
Reading
- Decodable support — decode CSV rows directly into Swift structs, only materializing the columns you need
- Column mapping — map CSV headers to struct properties at the call site, no CodingKeys required
- High-performance parsing with zero-copy techniques
- Low memory footprint through chunked streaming — constant memory regardless of file size
- Three API tiers — typed structs via
Decodable, dictionary access by column name, or raw array iteration - Configurable delimiters supporting standard CSV, TSV, and custom formats
- Quote handling with optional optimization for quote-free data
- Error recovery allowing processing to continue despite malformed rows
- UTF-8 BOM detection and automatic removal
Writing
- Encodable support — write Swift structs directly to CSV with automatic header derivation
- RFC 4180 quoting — fields containing delimiters, quotes, or newlines are quoted automatically
- Multiple output targets — write to file path, URL, or string
- Row-by-row or batch — streaming writes via
CSVWriteror one-shot via static methods - Round-trip fidelity — read, transform, and write back with full type preservation
Installation
Add FastCSV to your project using Swift Package Manager:
dependencies: [
.package(url: "https://github.com/wombat2k/FastCSV.git", from: "1.0.0")
]Quick Start
Define a struct, map the CSV columns to your property names, and iterate:
import FastCSV
struct BusRoute: Decodable {
let route: String
let name: String
let monthTotal: Int
}
var rows = try FastCSV.makeRows(
BusRoute.self,
fromPath: "ridership.csv",
columnMapping: [
"routename": "name",
"MonthTotal": "monthTotal",
]
)
try rows.forEach { route in
print("\(route.name): \(route.monthTotal)")
}Rows are decoded lazily — one at a time, not all at once. Memory stays constant regardless of file size.
Column Mapping
The columnMapping parameter maps CSV header names to struct property names:
columnMapping: ["routename": "name"]This means: the CSV column routename fills the struct property name.
You only need entries for columns whose names differ from your properties. Columns that already match (like route above) can be left out. Extra CSV columns are ignored.
If you prefer baking the mapping into the type, Swift's standard CodingKeys works too:
struct BusRoute: Decodable {
let route: String
let name: String
enum CodingKeys: String, CodingKey {
case route
case name = "routename"
}
}
// No columnMapping needed
var rows = try FastCSV.makeRows(BusRoute.self, fromPath: "ridership.csv")Iterating
forEach — process every row
Use forEach when you want to iterate through all rows. The callback receives a decoded struct directly:
try rows.forEach { route in
print(route.name)
}Use return to skip rows (not continue — you're inside a closure). Note that forEach always reads every row in the file, even when individual iterations return early.
for-in — stop early with break
Use for-in when you need to stop before the end. Each element is a Result<T, Error>:
for result in rows {
let route = try result.get()
print(route.name)
break
}The Result type also enables per-row error handling:
for result in rows {
switch result {
case .success(let route): print(route.name)
case .failure(let error): print("Skipping: \(error)")
}
}Input Sources
All reading APIs accept file paths, URLs, in-memory Data, or String:
var rows = try FastCSV.makeRows(T.self, fromPath: "/path/to/file.csv")
var rows = try FastCSV.makeRows(T.self, fromURL: url)
var rows = try FastCSV.makeRows(T.self, fromData: csvData)
var rows = try FastCSV.makeRows(T.self, fromString: "name,age\nAlice,30\n")Raw Access
When you don't have a struct or don't know the schema:
// By position
let arrayRows = try FastCSV.makeArrayRows(fromPath: "data.csv")
for row in arrayRows {
let name = try row[0].string
let age = try row[1].int
}
// By column name
let dictRows = try FastCSV.makeDictionaryRows(fromPath: "data.csv")
for row in dictRows {
let name = try row["name"]!.string
}CSVValue provides typed accessors: .string, .int, .double, .float, .bool, .date, .decimal. Use the IfPresent variants (.stringIfPresent, .intIfPresent, etc.) when a field might be empty — they return nil instead of throwing.
Optional struct fields also decode empty CSV values as nil:
struct Person: Decodable {
let name: String
let age: Int? // empty CSV field → nil
}Writing CSV
Encodable structs
Headers are derived automatically from property names (or CodingKeys):
struct Output: Encodable {
let name: String
let age: Int
}
let people = [Output(name: "Alice", age: 30), Output(name: "Bob", age: 25)]
try FastCSV.writeRows(people, toPath: "output.csv")
let csv = try FastCSV.writeString(people)String arrays
try FastCSV.writeRows(
[["Alice", "30"], ["Bob", "25"]],
headers: ["name", "age"],
toPath: "output.csv"
)Row-by-row streaming
let writer = CSVWriter()
try writer.writeHeaders(["name", "age"])
try writer.writeRow(["Alice", "30"])
if let csv = writer.toString() {
print(csv)
}CSVWriter also accepts a file path or URL in its initializer for streaming to disk.
Configuration
Custom delimiters
let tsv = CSVConfig(delimiter: CSVFormat.tsv.delimiter)
var rows = try FastCSV.makeRows(T.self, fromPath: "data.tsv", config: tsv)Supported formats: CSV, TSV, semicolon-separated, or custom field/row/quote delimiters.
No-quotes optimization
Skip quote detection for a ~9% speed boost when your data has no quoted fields:
let config = CSVConfig(assumeNoQuotes: true)Custom headers
For files without a header row:
var rows = try FastCSV.makeRows(
T.self,
fromPath: "data.csv",
hasHeaders: false,
headers: ["name", "age", "city"]
)Examples
The Examples directory contains runnable examples using real CTA bus ridership data (40K rows). Each example is a standalone executable target:
cd Examples
swift run Filtering
swift run Aggregation
swift run Writing
swift run RawAccessPerformance
FastCSV is optimized for high-throughput scenarios. Benchmarked against a 1.4GB NHS prescription dataset (10.3 million rows, 11 columns):
| API | Rows/sec | Notes | |-----|----------|-------| | Array iterator (no quotes) | 1.2M | Raw CSVValue access, 6 fields per row | | Array iterator (standard) | 1.1M | Same access pattern, with quote detection | | Decodable + columnMapping | 477K | Full struct decoding, 6 typed fields |
Memory stays constant regardless of file size — peak was 8.5MB (0.6% of the 1.4GB file).
The Decodable path is roughly 2x slower than raw array access with equivalent field access. This overhead comes from Swift's Codable protocol machinery (dynamic dispatch, KeyedDecodingContainer, CodingKey resolution per field per row) and is inherent to any Decoder implementation. For maximum throughput on very large files, use the array or dictionary iterators directly.
Requirements
- macOS: 13.0+
- iOS: 15.0+
- Swift: 6.2+
License
MIT License — see LICENSE for details.
Package Metadata
Repository: wombat2k/fastcsv
Default branch: main
README: README.md