Open-source project
tototoshi/scala-csv avatar
tototoshi/scala-csv

scala-csv reads and writes CSV in the shape your Scala code wants

CSV Reader/Writer for Scala

707 stars148 forksScalaNOASSERTION

At a glance

What is it?
scala-csv is a small Scala library for CSV, with four ways to read a file, a header-to-map mode, a writer that overwrites by default and format and encoding chosen at construction. It has no published releases and a licence file that disagrees with the README.
Who is it for?
Adopt scala-csv when a Scala job needs CSV in and out and you want Scala collections rather than Java iterators, and accept that you are depending on a library with no tagged release to pin. Skip it if you need quoted-field, dialect or streaming-encoding behaviour the documented API does not describe, or if your build requires a release artifact, since the repository publishes no GitHub releases and the documented coordinate is version 2.0.0.
Can I use it commercially?
Check first. The repository uses a licence we do not classify automatically, so read its LICENSE file before any commercial use.
Is it still maintained?
Yes. The repository last received commits 4 days ago.
What is it written in?
Mainly Scala, according to GitHub's language statistics.

Answers come from the project's GitHub data, last synced on October 23, 2026, and from our analysis. They are not legal advice.

Editorial analysis

Four ways to read, and only one of them is eager

The library exposes one reader type, CSVReader, and four documented ways to pull rows out of it. The eager one is all(), which returns the whole file as List[List[String]]. The lazy one is the iterator property, which hands back an Iterator[Seq[String]] that the console shows as a non-empty iterator, so a large file is not loaded into memory before you touch the first row. There is also toStream, which produces a Stream[List[String]], the lazy list that existed before the collections redesign and that is still the natural fit when you want a partially consumed head and a lazy tail. And there is readNext(), which returns an Option[List[String]]: Some for a row, None once the file is exhausted, and None rather than an exception, which is the difference between this method and calling next on a bare iterator. That last distinction is visible in the documented behaviour, where calling next on the exhausted iterator raises a NoSuchElementException with a stack trace pointing into CSVReader.scala. The choice between foreach and readNext for line at a time reading is also documented as having two available ways, and readNext is the one that signals the end of the file without an exception.

allWithHeaders turns the first row into map keys

For files whose first line names the columns, there is a dedicated call, allWithHeaders(), which returns List[Map[String,String]]. Given a file whose first row is Foo,Bar,Baz, the result is a list of maps pairing each header with the value from the matching column, so a row of a,b,c becomes Foo to a, Bar to b, Baz to c. This is the mode that most spreadsheet-shaped exports need, and the choice is in the method rather than in a configuration flag, so you keep two different read paths for two different file shapes. There is a trade worth naming, though, and it is not documented in the project notes. The header row is consumed as data, and the return type is Map, so a file with two columns of the same name produces a map with one key, and the column order is no longer recoverable from the value alone. If the order of columns matters to your downstream code, or if a file might have a duplicated header, the positional List[List[String]] form is the safer read, because it preserves exactly what was in the file.

The writer overwrites by default, which is the trap

CSVWriter mirrors the reader's shape, with open for the constructor, writeAll for a batch and writeRow for one row at a time. The behaviour that deserves a careful read is the default. The documentation states plainly that the default behaviour of CSVWriter#open is overwriting, and that appending to a file that already exists requires setting the append flag to true. That means a writer opened on an existing file replaces its contents, and the code that follows it looks entirely ordinary. For a job that regenerates a report on every run, the default is what you want. For anything that opens a file, writes a couple of rows and closes, the default silently destroys what was there. The append form takes the flag as a named argument, and the same file can then be written to row by row with writeRow. The open call accepts either a path string or a File, and the writer is closed explicitly at the end, which is the same discipline the reader examples follow.

Format and encoding are decided when you open the file

The library builds on a CSVFormat type, and both open methods take it implicitly, so the format is a property of the reader or writer instance rather than a call site. The documented way to change it is to define your own format, and the example does it by extending DefaultCSVFormat and overriding the delimiter. In the console session in the documentation, that produces output where the separator is a hash rather than a comma, and the fields come back quoted. The other knob is encoding. UTF-8 is the default, and the reader takes an encoding name as a second argument, so a file in ISO-8859-1 is read by passing that name to open. The practical reading of this design is that the format travels with the handle. A reader and a writer that need to agree on delimiter and encoding have to be configured with the same format value, and nothing in the documented API checks that they do. If you parse with one dialect and write with another, you get a file that looks fine and reads back wrong, which is the kind of bug that survives a long time in a pipeline.

Adding the dependency and building from source

The build definition is a single line in build.sbt, and the artifact coordinate is the usual GitHub user and repository name pair with the Scala binary version appended.

scala
libraryDependencies += "com.github.tototoshi" %% "scala-csv" % "2.0.0"

For working on the library itself, the documented development flow clones the repository, enters the directory and starts sbt.

bash
$ git clone https://github.com/tototoshi/scala-csv.git
$ cd scala-csv
$ sbt
> test

The repository root carries build.sbt, a project directory for the sbt build definition, a src directory for the code, a doctest.sbt, which suggests the documentation examples are exercised as tests, and a LICENSE.txt alongside the README. The absence of a release history is the part to plan around. The repository has no GitHub releases, and the version you add to build.sbt is 2.0.0, a number that appears in the dependency line rather than in a tagged release you can inspect. The last push was on 2026-10-19, so the code is being worked on, but there is no version history to consult and no changelog in the tree.

The licence statement and the licence file disagree

There is a discrepancy worth resolving before this goes into a dependency list. The README ends with a licence section pointing at Apache 2.0, and the repository root holds a file named LICENSE.txt rather than a file named LICENSE. The repository metadata classifies the licence as something GitHub cannot map to a standard identifier, which is what it does with custom licence text. Those two statements sit awkwardly together, because Apache 2.0 is a well known licence that GitHub recognises, so a custom file and a recognised identifier are not describing the same thing. The likely explanation is that the file holds a modified Apache 2.0, an appendix or a notice on top of the standard text, but the documentation does not say so, and this is a matter for your own legal review rather than something this article can settle. The practical instruction is simple: read LICENSE.txt before you depend on the library commercially, and treat the README's link as a pointer rather than as the licence itself.

Where a hand-rolled parser or a Java library fits better

scala-csv is small and it does one thing, which is its strength and its boundary. The alternative approaches differ in what they hand you and what they require of you. A hand written parser in your own code gives you exactly the shape you need and nothing else, at the cost of owning quoting rules, embedded delimiters, line endings and encoding edge cases that are notoriously fiddly and are exactly the cases the standard library libraries spend their effort on. A Java CSV library gives you a mature implementation with explicit format objects you construct and pass in, but the results arrive as Java collections and iterators, so a Scala codebase ends up converting at the boundary. scala-csv sits in between, returning List, Option and Stream directly and taking the format implicitly so that a reader and a writer in the same scope agree on it. The gap is the same in any direction. The documented API does not describe dialect handling beyond the delimiter, and there is no stated support for reading a CSV with a header row of duplicated names or for recovering a column order once you have asked for maps. If your files are simple, plain and machine-generated, this library is a good fit. If they come from a spreadsheet with a quirky export, the missing detail is where your time will go.

Editorial conclusion

Adopt scala-csv when a Scala job needs CSV in and out and you want Scala collections rather than Java iterators, and accept that you are depending on a library with no tagged release to pin. Skip it if you need quoted-field, dialect or streaming-encoding behaviour the documented API does not describe, or if your build requires a release artifact, since the repository publishes no GitHub releases and the documented coordinate is version 2.0.0. Before you adopt it, check LICENSE.txt against the Apache 2.0 statement in the README, and set append = true on CSVWriter.open for any file you must not overwrite, because the default is to overwrite.

Frequently asked questions

How do I add scala-csv to an sbt project?

Add one line to build.sbt: libraryDependencies += "com.github.tototoshi" %% "scala-csv" % "2.0.0". The %% suffix selects the artifact built for your Scala binary version.

How do I read a CSV file with headers into maps in scala-csv?

Open the file with CSVReader.open and call allWithHeaders(), which returns List[Map[String,String]] keyed by the first row. The positional form all() returns List[List[String]] and preserves the column order as it appears in the file.

How do I append rows to an existing file with CSVWriter?

Pass the append flag when opening: CSVWriter.open("a.csv", append = true). The default behaviour of CSVWriter#open is to overwrite the file, so the flag has to be set explicitly for appending.

Can scala-csv read a file that is not UTF-8?

Yes. UTF-8 is the default, and the reader takes an encoding name as a second argument, so CSVReader.open(filepath, "ISO-8859-1") reads a file in that encoding.

How do I change the delimiter used by scala-csv?

Both open methods take a CSVFormat implicitly, so you define your own format, for example an object extending DefaultCSVFormat that overrides the delimiter, and that format is then used by the reader or writer in scope.

Official sources

  1. Issues
  2. README
  3. tototoshi/scala-csv on GitHub
Add this badge to your README

If you maintain this project, the badge below links readers to this analysis and shows its maintenance status from the daily GitHub snapshot. Paste the markdown into your README; add ?metric=license or ?metric=stars to the image URL for a different field.

Add this badge to your README

markdown
[![Hysen Labs](https://hysenlabs.com/badge/tototoshi-scala-csv.svg)](https://hysenlabs.com/projects/tototoshi-scala-csv)

Was this useful?