csv_plus 1.8.0 copy "csv_plus: ^1.8.0" to clipboard
csv_plus: ^1.8.0 copied to clipboard

Fast, complete CSV parser for Dart. Encode, decode, stream, query, and validate CSV data with automatic type inference and zero dependencies.

pub version pub points pub likes GitHub stars GitHub forks GitHub issues CI status Last commit License: MIT Dart

CSV Library for Dart & Flutter #

csv_plus is a fast, complete, zero-dependency Dart library for parsing, encoding, streaming, querying, and validating CSV data. It works in plain Dart and in Flutter apps, on the VM, Web (JS & WASM), and mobile. csv_plus reads and writes RFC 4180 CSV with automatic type inference, a DataFrame-style table layer, schema validation, and constant-memory streaming, and it is the fastest general-purpose CSV package for Dart on every workload we measure.

Find this useful? Star it on GitHub and 👍 like it on pub.dev. Stars and likes help other Dart & Flutter developers find a maintained, full-featured CSV library.

Overview #

csv_plus parses and generates comma-separated-values (CSV) text, along with tab-separated (TSV), pipe-delimited, and custom-delimiter formats. It decodes CSV into typed Dart values, encodes rows back to RFC 4180 output with correct quoting, streams large files with constant memory, and offers a table layer for filtering, sorting, grouping, aggregating, and validating tabular data. In short, it is a complete CSV serialization and deserialization toolkit for data processing and file handling in Dart and Flutter.

What you can do with it:

  • Decode CSV strings and files into typed rows (int, double, bool, String, null), or into a queryable table.
  • Encode rows, maps, and tables back to CSV, TSV, pipe-delimited, or Excel-flavored output with automatic quoting.
  • Stream very large CSV files row by row with a chunked, backpressure-aware transformer that never buffers the whole input.
  • Query, sort, group, aggregate, transform, and schema-validate tabular data with a DataFrame-style API.

Performance #

csv_plus is built for throughput: a byte-level (codeUnits) batch parser with no regex and no string allocation in the hot loop, first-byte type detection, and a per-call StringBuffer encoder. Type inference is guarded so speed never costs correctness.

It is the fastest general-purpose CSV package for Dart on every workload, on both JIT and AOT. The numbers below are a median of 5 runs against csv 8.0.0 and serial_csv 0.5.2, on 200k rows x 10 cols plain (14.3 MB) and 100k x 10 quote-heavy (18.4 MB), on the same machine:

Workload (JIT) csv 8.0.0 serial_csv csv_plus
Decode, strings 181.2 ms 325.8 ms (own fmt) 94.0 ms
Decode, typed 224.9 ms 111.7 ms (own fmt) 96.8 ms
Decode, quote-heavy 133.7 ms n/a 77.6 ms
Encode, typed rows 153.2 ms 141.8 ms 125.2 ms
decodeWithHeaders 178.5 ms n/a 95.4 ms

serial_csv reads and writes only its own strict format rather than general RFC 4180 CSV, so its decode rows (marked own fmt) are not directly comparable. The full tables with AOT numbers, the seeded data generators, and an edge-case comparison battery live in benchmark/compare/. Timings vary by hardware, so reproduce them on your own machine:

cd benchmark/compare && dart pub get && dart run bench.dart

Table of contents #

Key features #

Everything you need to read, write, stream, and analyze CSV, on every Dart & Flutter platform.

📥 Decoding
  • Typed decode with automatic int / double / bool / null / String inference
  • Data-loss guards: 007, +1, whitespace, and 16+ digit ids stay text; quoted fields are never inferred
  • String-only, integer, double, and boolean decoders that throw on bad input instead of inventing values
  • Lenient (decodeFlexible) mode: trims whitespace and recovers unmatched quotes
  • Header-aware rows (CsvRow) with row['name'] and row[0] access, or decodeToMaps for a list of header-keyed maps
  • Comment-line skipping (comment: '#') and row windowing (skipRows / maxRows) to drop preambles and read a slice
  • skipInitialSpace drops the padding an exporter leaves after a delimiter, so a, "b, c" reads as two fields rather than three
  • nullValues turns the spellings an exporter uses for a missing value (NULL, NA, N/A) into real nulls, without touching a quoted "NULL"
  • nullPlaceholder writes a sentinel for a null on the way out (NULL, or Postgres \N), so a file can round-trip its nulls
  • Opt-in date inference (parseDates), range-checked so 2024-13-45 stays text instead of rolling over, with dateOrder for numeric forms like 03/04/2024
  • Delimiter auto-detection, BOM handling, and the Excel sep= hint
  • Decode straight from bytes (decodeBytes) for a file picker, a bundled asset, or an HTTP body, including on web
  • Non UTF-8 files: CsvCharset.latin1 and CsvCharset.windows1252 for the encodings Excel on Windows writes
📤 Encoding
  • RFC 4180 output with automatic, correct quoting
  • Three quote modes: only-when-necessary, always, and strings-only
  • Encode rows, maps, uniform-typed grids, and tables
  • Custom delimiter, quote, escape, and line-ending configuration
  • Optional UTF-8 BOM for Excel compatibility
  • encodeToBytes for a file write, a web download, or a request body
🌊 Streaming
  • Chunked StreamTransformer for constant-memory decode and encode
  • Real backpressure: a slow consumer never buffers the whole file
  • Correct across chunk boundaries that split mid-field, mid-escape, mid-CRLF, or mid-delimiter
  • bindBytes decodes and encodes UTF-8 byte streams directly
📊 Table, query & transform
  • CsvTable: a 2D structure with headers, 50+ methods
  • Filter, sort (stable), take, skip, distinct, and range
  • Aggregate: sum, avg, min, max, count, and groupBy
  • Add, remove, rename, reorder, and transform columns
  • Schema validation and coercion: check or convert column types, nullability, patterns, and custom validators
🛡️ Reliability & platform
  • One documented parsing semantics across batch and streaming, enforced by a conformance suite
  • Optional strict mode: throws CsvParseException with row and column on malformed input
  • Typed exceptions: CsvException and subtypes
  • Zero dependencies, pure Dart: VM, Web (dart2js + wasm), and Flutter mobile
  • dart:io isolated behind a separate import so the core works everywhere

Limitations #

  • ❌ Month and day names (3 April 2024, Apr 3 2024): numeric dates are covered by dateOrder, but named months are not. Use a decoderTransform.

Roadmap #

The API is stable and the feature set is complete; new features are driven by user requests on the issue tracker. Shipped milestones are in the changelog.

Error handling #

Malformed-but-openable CSV degrades gracefully by default: text after a closing quote is appended to the field (Excel behavior), and an unterminated quote consumes the rest of the input. Pass strict: true to turn those into a typed, catchable CsvParseException that carries the row, column, and offset:

try {
  final rows = CsvCodec(CsvConfig(strict: true)).decode('"a"x,b');
} on CsvParseException catch (e) {
  print('Parse error at row ${e.row}, column ${e.column}: ${e.message}');
}

The typed decoders (decodeIntegers, decodeDoubles, decodeBooleans) throw CsvParseException on a cell they cannot convert rather than inventing a 0 or false. Schema violations throw CsvValidationException.

Example #

A complete, runnable set of samples lives in the example/ directory (basic, table, streaming, file IO, and advanced). Clone the repository and run them, or copy any snippet from Getting started below.

Working with spreadsheets rather than plain CSV? excel_plus reads and writes .xlsx files and uses this package for its CSV import and export.

Installation #

dart pub add csv_plus
# or, in a Flutter app:
flutter pub add csv_plus

Then import it:

import 'package:csv_plus/csv_plus.dart';

Getting started #

Encode and decode #

final codec = CsvCodec();

// Encode.
final csv = codec.encode([
  ['name', 'age', 'score'],
  ['Alice', 30, 95.5],
  ['Bob', 25, 88.0],
]);

// Decode: types are inferred automatically.
final rows = codec.decode(csv);
// rows[1] == ['Alice', 30, 95.5]  (String, int, double)

Header-aware rows #

final people = codec.decodeWithHeaders(csv);
print(people.first['name']); // Alice
print(people.first['age']);  // 30  (int, not String)

Type inference and typed decoders #

// Inference is guarded so identifier-like data is not corrupted.
codec.decode('id,qty\n007,3');
// ['id', 'qty'], ['007', 3]  (007 stays a String; 3 becomes an int)

// Or force a whole grid to one type. These throw on a bad cell instead of
// inventing a value; pass emptyAs to fill blanks.
codec.decodeStrings(csv);            // List<List<String>>
codec.decodeIntegers('1,2\n3,4');    // List<List<int>>
codec.decodeDoubles('1.5,2.5');      // List<List<double>>
codec.decodeBooleans('true,0');      // List<List<bool>>  (true/false/1/0)
codec.decodeFlexible('  a , b ');    // lenient: trims, recovers bad quotes

Date and time inference #

Dates stay text by default, because 03/04/2024 means two different days depending on where the file came from. Turn on parseDates and any field in ISO-8601 form becomes a real DateTime:

final codec = CsvCodec(const CsvConfig(parseDates: true));

codec.decode('when,who\n2024-01-31,Alice');
// ['when', 'who'], [DateTime(2024, 1, 31), 'Alice']

codec.decode('at\n2024-01-31T09:30:00Z');   // a UTC DateTime
codec.decode('at\n2024-01-31 09:30:00');    // a space separator works too

A value has to start with YYYY-MM-DD. A time part may follow after a T or a space, with optional fractional seconds and a Z or +05:30 offset. A value with no offset reads as local time; one with an offset reads as UTC.

Everything else stays text, including the cases that trip up other parsers:

codec.decode('a,b,c,d\n03/04/2024,2024-13-45,20240131,"2024-01-31"');
// ['03/04/2024', '2024-13-45', 20240131, '2024-01-31']

2024-13-45 is the interesting one. DateTime.parse quietly rolls it over to 14 February 2025; csv_plus range-checks every field first, so an impossible date stays text instead of becoming the wrong one. Quoted fields are never inferred, and a DateTime encodes back to a form that decodes to the same value, so a round trip is lossless.

Numeric dates like 03/04/2024

A CSV carries no locale, so that value is 3 April in most of the world and 4 March in the United States, and nothing in the file says which. csv_plus will not guess. Tell it the order your file uses and it will read them:

const au = CsvConfig(parseDates: true, dateOrder: CsvDateOrder.dayFirst);
const us = CsvConfig(parseDates: true, dateOrder: CsvDateOrder.monthFirst);

CsvCodec(au).decode('when
03/04/2024');  // 3 April 2024
CsvCodec(us).decode('when
03/04/2024');  // 4 March 2024

/, - and . all separate, day and month may be one or two digits, and a trailing HH:mm or HH:mm:ss is kept. A two-digit year follows the spreadsheet convention: up to 68 is this century, 69 and above the last one. Range checking still applies, so under monthFirst a value like 25/12/2024 stays text rather than rolling over. ISO-8601 is recognised whichever order you set, and the default, CsvDateOrder.iso, leaves ambiguous values alone.

Query and transform with CsvTable #

final table = CsvTable.parse('name,age,city\nAlice,30,NYC\nBob,25,LA\nEve,35,NYC');

// Filter (returns a new table).
final adults = table.where((row) => (row['age'] as int) >= 30);

// Sort in place (stable).
table.sortBy('age');

// ...or get a sorted copy without touching the source.
final byAge = table.sortedBy('age');

// Export.
print(table.toCsv());
print(table.toFormattedString()); // pretty-printed aligned table

Aggregate and group #

print(table.avg('age'));   // 30.0
print(table.sum('age'));   // 90
print(table.max('age'));   // 35

// Group rows by a column value into sub-tables.
final byCity = table.groupBy('city'); // {NYC: CsvTable, LA: CsvTable}

Stream large files #

import 'package:csv_plus/io.dart';

// Constant memory, any file size.
await for (final row in CsvFile.stream('huge.csv')) {
  process(row);
}

Any string or byte stream works, with backpressure handled for you:

final rows = codec.decoder.bindBytes(byteStream); // Stream<List<int>>

Bytes, pickers and assets #

A file picker, a bundled asset, and an HTTP response all hand you bytes rather than a path, and on Flutter web there is no path at all. Decode them directly:

// file_picker, with withData: true so bytes are populated on every platform
final rows = const CsvCodec().decodeBytes(result.files.single.bytes!);

// a bundled asset
final data = await rootBundle.load('assets/products.csv');
final table = const CsvCodec().decodeBytesToTable(data.buffer.asUint8List());

// straight to JSON
final json = jsonEncode(const CsvCodec().decodeBytesToMaps(bytes));

The byte order mark, the sep= hint and delimiter detection all apply, exactly as they do for a string. Going the other way, encodeToBytes returns UTF-8 bytes ready for File.writeAsBytes, a download, or a request body.

Files that are not UTF-8, which is what Excel on Windows writes, take a charset:

final rows = const CsvCodec().decodeBytes(
  bytes,
  charset: CsvCharset.windows1252,
);

CsvCharset.utf8 (the default), .latin1 and .windows1252 are built in, with no extra dependency. See reading bytes and encodings.

Read and write files #

import 'package:csv_plus/io.dart';

final table = await CsvFile.read('data.csv');
await CsvFile.write('out.csv', table);
await CsvFile.append('out.csv', [['Zoe', 41]]);

Configuration and presets #

final excel = CsvCodec.excel(); // ';' delimiter + UTF-8 BOM
final tsv = CsvCodec.tsv();      // tab-separated
final pipe = CsvCodec.pipe();    // pipe-separated

// Or configure fully.
final custom = CsvCodec(CsvConfig(
  fieldDelimiter: '::',
  quoteMode: QuoteMode.always,
  skipEmptyLines: true,
));

Comments and row windowing #

Real-world exports often carry a comment preamble or more rows than you want to load. Skip comment lines, drop leading rows, and cap the result without a second pass:

const csv =
    '# export 2026-07-17\n'
    'name,score\n'
    'Alice,95\n'
    'Bob,88\n'
    'Eve,73';

final codec = CsvCodec(CsvConfig(
  comment: '#',   // drop lines beginning with '#'
  hasHeader: true,
  maxRows: 2,     // read at most two data rows
));

codec.decode(csv);        // [['Alice', 95], ['Bob', 88]]
codec.decodeToMaps(csv);  // [{name: Alice, score: 95}, {name: Bob, score: 88}]

comment is detected only at the start of a line, so a # inside a quoted or mid-field value stays content. skipRows drops leading rows before the header is read (handy for a titled preamble); maxRows bounds the data rows and lets the batch decoders stop early. All three apply across every decode path, including the streaming decoder.

Strict mode #

// Throw on structurally malformed input instead of recovering.
final strict = CsvCodec(CsvConfig(strict: true));
strict.decode('"unterminated'); // throws CsvParseException

Schema validation and coercion #

final schema = CsvSchema(columns: [
  CsvColumnDef(name: 'email', type: String, required: true, pattern: r'@'),
  CsvColumnDef(name: 'age', type: int, nullable: false),
]);

// Validate...
final errors = table.validate(schema);   // List<CsvValidationException>
final ok = table.conformsTo(schema);      // bool

// ...or coerce: convert each column to its declared type (int, double, num,
// bool, String, DateTime). Throws CsvParseException (with row and column) on a
// value that will not convert, or a null in a non-nullable column.
final typed = codec.decodeWithSchema('email,age\na@b.com,42', schema);
typed.rawData.first; // ['a@b.com', 42]  (age is an int, not "42")
final coerced = table.coerce(schema); // or coerce an existing table

Maps and two-column CSV #

codec.encodeMap({'host': 'localhost', 'port': 8080});
codec.decodeMap('host,localhost\nport,8080'); // {host: localhost, port: 8080}

dart:convert integration #

final adapter = codec.asCodec(); // Codec<List<List<dynamic>>, String>
final rows = adapter.decode('a,b\n1,2');
final piped = adapter.fuse(utf8); // fuse with other codecs

csv_plus vs csv #

csv_plus and the csv package cover similar ground; csv_plus adds speed, a table layer, and stricter correctness.

csv_plus csv
Decode speed (typed, JIT) 97 ms 225 ms
Parsing semantics One truth across batch & streaming, conformance-tested Batch and streaming
Type inference Guarded (007, +1, big ids stay text) Coerces (may corrupt ids)
Table / query / schema layer Yes No
Streaming backpressure Yes Basic
Dependencies Zero Zero

Numbers are from the reproducible benchmark above.

FAQ #

Is csv_plus a drop-in for the csv package? No, the APIs differ, but the concepts map directly (codec, typed decode, headers, streaming). Most migrations are a small, mechanical change.

Which platforms are supported? Dart VM, Web (both JavaScript and WebAssembly), and mobile (Android & iOS) via Flutter, plus desktop. It is pure Dart with no dart:io in the core path.

Does it handle large files without running out of memory? Yes. The streaming decoder and encoder process input in chunks with real backpressure, so memory stays constant regardless of file size.

Will type inference corrupt my ids or codes? No. Values with leading zeros, a leading plus, surrounding whitespace, or more than 15 digits stay strings, and quoted fields are never inferred, on every platform including the web.

Does it support TSV, pipe-delimited, and Excel CSV? Yes, via CsvCodec.tsv(), CsvCodec.pipe(), CsvCodec.excel(), or a custom CsvConfig with any single or multi-character delimiter.

Support and feedback #

  • Found a bug or want a feature? Open an issue on the issue tracker.
  • Questions and ideas are welcome via GitHub Discussions.
  • Pull requests are welcome; see the repository for contribution guidelines.

About #

csv_plus is an open-source, MIT-licensed, zero-dependency CSV library for Dart and Flutter, built around a byte-level parser and a chunked streaming transformer for speed and low memory on large files.

csv_plus is created and owned by Nurullah Al Masum.

Contributors #

csv_plus grows with its community; every contributor is listed here:

csv_plus contributors

Want to help? Pull requests are welcome; see Support and feedback.

40
likes
160
points
10.3k
downloads
screenshot

Documentation

Documentation
API reference

Publisher

verified publisheralmasum.dev

Weekly Downloads

Fast, complete CSV parser for Dart. Encode, decode, stream, query, and validate CSV data with automatic type inference and zero dependencies.

Repository (GitHub)
View/report issues
Contributing

Topics

#csv #csv-parser #serialization #deserialization #encoding

License

MIT (license)

More

Packages that depend on csv_plus