Skip to content

Repository files navigation

csv

A high-quality, best-practice CSV library for Dart, inspired by PapaParse but built with Dart idioms in mind.

Upgrading from Version 7

Version 8 fixes an issue where using csv.decoder as a stream transformer produced an extra layer of nesting (#77).

  • CsvCodec has been renamed to Csv. A deprecated CsvCodec typedef is available for migration.
  • Csv no longer extends dart:convert's Codec class. If you need a Codec (e.g., for .fuse()), use asCodec().

If you rely on the version 6 API, please consult doc/README-v6.md.

Features

  • Stream-friendly: decoder and encoder are proper StreamTransformers — one row per stream event.
  • Easy Excel Compatibility: Built-in support for Excel-compatible CSVs (UTF-8 BOM, ; separator, \r\n line endings).
  • Auto-detection: Smartly detects delimiters and line endings.
  • Robust Parsing: Handles quoted fields, escaped quotes, and even malformed CSVs graciously (similar to PapaParse).
  • Performance: Optimized for speed and low memory usage.
  • Dynamic Typing: Optional automatic parsing of numbers and booleans (similar to PapaParse).

Delimiters

The Csv class and CsvDecoder support:

  • Field Delimiters: Can be single or multi-character strings (e.g., ,, ::, |).
  • Quote Character: Must be a single character. Defaults to ".
  • Escape Character: Must be a single character (if provided). Defaults to the quote character.
  • Line Delimiters: The decoder automatically handles \r, \n, and \r\n. The encoder allows specifying a custom lineDelimiter (defaults to \r\n).

⚠️ Important: Line Endings & Windows

The default line delimiter is \r\n (CRLF) as specified by RFC 4180.

Be careful on Windows! If you write the CSV string directly to a file using certain methods (like File.writeAsString without specifying strict options or redirecting stdout), Windows file handling might automatically convert \n to \r\n. Since the CSV already contains \r\n, this can result in double carriage returns: \r\r\n.

To avoid this, either:

  1. Use \n as the line delimiter:
    final codec =Csv(lineDelimiter:'\n');
    final csvString = codec.encode(data);
  2. Use a binary writer (e.g., File.openWrite()) which writes bytes exactly as they are.

Usage

Simple Example

import'package:csv/csv.dart';
voidmain() {
final data = [
['Name', 'Age', 'City'],
['Alice', 30, 'New York'],
['Bob', 25, 'London'],
];
// EncodefinalString csvString = csv.encode(data);
print(csvString);
// DecodefinalList<List<dynamic>> decodedData = csv.decode(csvString);
print(decodedData);
}

Stream Transformation (e.g., reading a file)

The decoder and encoder are StreamTransformers that emit one row per event, so stream.transform(csv.decoder).toList() gives you a flat List<List<dynamic>>.

import'dart:convert';
import'dart:io';
import'package:csv/csv.dart';
voidmain() async {
final file =File('data.csv');
// Each event in the stream is a single row (List<dynamic>).finalList<List<dynamic>> rows =await file
.openRead()
.transform(utf8.decoder)
.transform(csv.decoder)
.toList();
print(rows.first); // e.g. ['Name', 'Age', 'City']
}

Stream Read-Modify-Write Pipeline

import'dart:convert';
import'dart:io';
import'package:csv/csv.dart';
voidmain() async {
final input =File('input.csv');
final output =File('output.csv');
await input.openRead()
.transform(utf8.decoder)
.transform(csv.decoder)
.map((row) {
row.add('Processed');
return row;
})
.transform(csv.encoder)
.transform(utf8.encoder)
.pipe(output.openWrite());
}

Excel Compatible CSV

Excel often requires a UTF-8 BOM and ; as a separator to open files correctly in certain locales.

import'package:csv/csv.dart';
voidmain() {
final data = [
['Header1', 'Header2'],
['Value 1', 'Value 2'],
];
// Use the built-in excel codecfinalString excelCsv = excel.encode(data);
// This automatically adds the BOM and uses ';' as delimiter
}

Custom Configuration

import'package:csv/csv.dart';
voidmain() {
final myCodec =Csv(
fieldDelimiter:'\t',
lineDelimiter:'\n',
quoteMode:QuoteMode.strings, // Only quote strings, not numbers
escapeCharacter:'\\', // Use backslash for escaping
);
final encoded = myCodec.encode([['a', 1, true], ['b', 2.5, false]]);
// Output: "a"\t1\ttrue\n"b"\t2.5\tfalse
}

Dynamic Typing

Automatically convert values that look like numbers or booleans into their respective Dart types.

Note

Enabling dynamicTyping adds a small performance overhead (approx. 15% in benchmarks). For maximum performance on large files where you know the schema, using a manual decoderTransform or processing rows after decoding is faster.

import'package:csv/csv.dart';
voidmain() {
final input ='Name,Age,Active\nAlice,30,true';
// With dynamic typing enabledfinal codec =Csv(dynamicTyping:true);
final rows = codec.decode(input);
print(rows[0][1].runtimeType); // int (30)print(rows[0][2].runtimeType); // bool (true)
}

Advanced: Field Transformations

You can use the encoderTransform and decoderTransform hooks to process fields based on their value, column index, or header name to for example trim text, change decimal separators or format dates.

import'package:csv/csv.dart';
voidmain() {
final customCodec =Csv(
fieldDelimiter:';',
parseHeaders:true, // Required if you want 'header' name in the transform
decoderTransform: (value, index, header) {
// Change column 3 (index 2) to uppercaseif (index ==2) return value.toUpperCase();
// Convert 'Age' column to intif (header =='Age') returnint.tryParse(value) ?? value;
return value;
},
);
final input ='Name;City;Age\nAlice;London;30';
final decoded = customCodec.decode(input);
print(decoded[0]['Age']); // 30 (as int)print(decoded[0]['City']); // LONDON
}

Automated Delimiter Detection (including sep=;)

The library automatically detects common delimiters (,, ;, \t, |). It also respects the sep=; header common in some Excel-exported CSV files.

final decoded = csv.decode('sep=;\r\nA;B;C');
// Result: [['A', 'B', 'C']]

Map-like Row Access

If you want to access values by their header names, you can use either decodeWithHeaders() or the parseHeaders option on the codec. Both return CsvRow objects which behave like both a List and a Map.

import'package:csv/csv.dart';
voidmain() {
final fileContents ='id,name\n1,Alice\n2,Bob';
// Method 1: Using decodeWithHeaders (Recommended)// This automatically sets the parseHeaders flag and returns a properly typed List<CsvRow>.final rowsWithHeaders = csv.decodeWithHeaders(fileContents);
// Access by header name directlyprint(rowsWithHeaders[0]['name']); // Alice// Still accessible by indexprint(rowsWithHeaders[0][1]); // Alice// Method 2: Using Csv(parseHeaders: true)// This requires casting the returned rows to CsvRow manually.final codec =Csv(parseHeaders:true);
final dynamicRows = codec.decode(fileContents);
final row = dynamicRows[0] asCsvRow;
// Access by header nameprint(row['name']); // Alice// The first row of the file was used for headers and is not in the list.
}

Using asCodec() for Fusing

If you need a dart:convertCodec — for example, to use .fuse() — call asCodec():

import'dart:convert';
import'package:csv/csv.dart';
// A simple converter that adds a column to every row.classAddColumnConverterextendsConverter<List<List<dynamic>>, List<List<dynamic>>> {
@overrideList<List<dynamic>> convert(List<List<dynamic>> input) {
return input.map((row) => [...row, 'Processed']).toList();
}
}
voidmain() {
final processor =AddColumnConverter();
// Use asCodec() to get a dart:convert Codec, then fuse.final codec = csv.asCodec();
final sanitizingCodec = codec.decoder.fuse(processor).fuse(codec.encoder);
final inputCsv ='Name,Age\nAlice,30';
final outputCsv = sanitizingCodec.convert(inputCsv);
print(outputCsv);
// Output:// Name,Age,Processed// Alice,30,Processed
}

Controlling batch size: When the codec's decoder is used as a stream transformer, rows from the same input chunk are emitted together in a single batch (a List<List<dynamic>>). You can limit the maximum number of rows per batch with maxRowsPerBatch:

// Each stream event contains at most 100 rows.final codec = csv.asCodec(maxRowsPerBatch:100);
// For single-row events (one row per stream event), use 1:final singleRowCodec = csv.asCodec(maxRowsPerBatch:1);

PapaParse Features

This library incorporates many good ideas from PapaParse, such as:

  • Handling misplaced quotes gracefully.
  • Auto-detecting delimiters based on frequency and consistency.
  • Handling various line ending styles automatically in the decoder.
  • Support for sep= headers.
  • Header Parsing: Efficiently mapping headers to row indices (similar to header: true in PapaParse).

Installation

Add this to your pubspec.yaml:

dependencies:
csv: ^8.0.0

The Codec Problem & asCodec()

This section explains why Csv does not extend dart:convert's Codec, and what asCodec() provides as an escape hatch.

Background

Dart's Codec<S, T> provides two converters:

ComponentType
decoderConverter<T, S>
encoderConverter<S, T>

A Converter<S, T> has two roles:

  1. Batch: T convert(S input) — convert an entire input at once.
  2. Stream: Stream<T> bind(Stream<S>) — transform a stream, where each event is of type T.

The type parameter T must serve both roles. For most codecs this is fine. For example, utf8 is a Codec<String, List<int>>: batch conversion produces a List<int>, and each stream event is also a List<int> (a chunk of bytes). Concatenating chunks of bytes is natural.

Why CSV breaks this

For a CSV decoder, the two roles need different types:

RoleNeeds
Batch (convert())Returns List<List<dynamic>> — all rows at once
Stream (each event)Should be List<dynamic> — one row at a time

There is no single type T that works for both. If we use Converter<String, List<List<dynamic>>> (as version 7 did), then:

  • convert("a,b\nc,d")[['a','b'], ['c','d']] ✓ correct
  • stream.transform(decoder).toList()List<List<List<dynamic>>>extra nesting!

Each stream event is a batch of rows, and .toList() collects those batches into yet another list — the "extra []" reported in #77.

This is a fundamental limitation of dart:convert's type system, not a bug in this library. Versions 3.2 through 6 worked around it by not being a Codec at all (see commit 235d898). Version 7 reintroduced Codec and the problem came back.

The solution in version 8

Version 8 takes a hybrid approach:

  • By default, CsvDecoder and CsvEncoder are StreamTransformerBase subclasses. Stream usage works correctly — one row per event.
  • asCodec() returns a real Codec<List<List<dynamic>>, String> adapter for when you need .fuse() or other Codec-specific APIs. The returned codec wraps the same parsing/encoding logic, but follows Dart's Converter type contract (with the inherent stream nesting trade-off).
// Default: stream works correctlyfinal rows =await file.openRead()
.transform(utf8.decoder)
.transform(csv.decoder)
.toList();
// rows is List<List<dynamic>> ✓// asCodec(): for fuse() and other Codec APIsfinal fused = csv.asCodec().decoder.fuse(someConverter);
// asCodec() with batch size limitfinal codec = csv.asCodec(maxRowsPerBatch:100);

If you use asCodec().decoder as a stream transformer, you will get the extra nesting — that is inherent to Dart's Converter type contract and cannot be avoided. Rows from the same input chunk are grouped together into a single batch. Use maxRowsPerBatch to limit the batch size, or use csv.decoder directly for streams.

About

A dart csv to list codec / converter

Topics

Resources

Stars

103 stars

Watchers

2 watching

Forks

Releases

Packages

Used by

Contributors

Languages