DEV Community

Zion Onwujuba
Zion Onwujuba

Posted on

Reading a CSV using OpenCSV and Kotlin

CSVs are a great tool for storing data in easily readable and structured format. Accessing that data in Kotlin can allow their capabilities to grow even more.

In this article, we will learn how we can use the free Java library, OpenCSV, to read a CSV and populate a Kotlin class with the data from the CSV.

Starting with CSVs and Digital Dreams

Let's start with our CSV. We will use the template below as our example.

Name Age Language
Tim 25 HTML
Steve 80 Swift
Bill 42 .NET

This csv needs to be accessible by our code, so save it into the codebase. In this example, it will be saved under csv/languages.csv.

With this, we can import OpenCSV into the build file of the build automation tool of our choice (The latest version and import code can be found here). Then we need to import the noArg plugin. OpenCSV requires that the class that will be populated by the CSV data contain a constructor with no arguments. Without this plugin, we can get some nasty errors.

With all of this now added, we can writing the class that will be populated by the CSV.

Writing the Class

Now we need to write our class that can be populated by the CSV data. We can do that with a class like this:

import com.opencsv.bean.CsvBindByName
@NoArg
data class CSVToObject(
    @CsvBindByName(column = "Name", required = true)
    val name: String,
    @CsvBindByName(column = "Age", required = true)
    val age: String,
    @CsvBindByName(column = "Language", required = true)
    val language: String,
    ) 
Enter fullscreen mode Exit fullscreen mode

We can break the code down into pieces:

@NoArg
Enter fullscreen mode Exit fullscreen mode

This annotation calls the noArg plugin and binds our class to it to enforce our class being initialized with a constructor with no arguments.

@CsvBindByName(column = "Name", required = true)
val name: String,
@CsvBindByName(column = "Age", required = true)
val age: String,
@CsvBindByName(column = "Language", required = true)
val language: String,
Enter fullscreen mode Exit fullscreen mode

The @CsvBindByName annotation binds the argument in our class to the header name in the CSV. The required parameter is optional and just enforces that each cell under a specific header must be filled. We then create an argument that will be bound with the annotation, type casting it to a string.

So in this example, with the header names being name, age, and language, we are telling the OpenCSV that for each row in the CSV, the data for in the name column for that row will be populated into the name argument.

So the first row will create an object with the name argument populated with "Tim", the age argument populated with "25", and the language argument populated with "HTML".

This annotation isn't required. Instead of binding arguments by header name, you can use @CsvBindByPosition and (with the first column being 0) assign arguments based on their position.

Here's how that would look with our example:

@CsvBindByPosition(position = 0, required = true)
val name: String,
@CsvBindByName(position = 1, required = true)
val age: String,
@CsvBindByName(position = 2, required = true)
val language: String,
Enter fullscreen mode Exit fullscreen mode

Now that we have our class we can start reading our CSV and populating our class with its data.

Reading Time!

Now to read our file, we need to make it readable. That involves turning it into a character stream. We can do that by using Java's Path interface to create a URI:

 val csvPath : Path = Paths.get(
      ClassLoader.getSystemResource("csv/langauges.csv").toURI()); 
Enter fullscreen mode Exit fullscreen mode

Then use Java's file interface and reader object to create a character stream:

val reader : Reader = Files.newBufferedReader(path) 
Enter fullscreen mode Exit fullscreen mode

Finally we can build a javaBean, using CsvToBeanBuilder and assigning the csvBean to our CSVToObject class.

val csvBean = CsvToBeanBuilder<CSVToObject>(reader)
            .withFieldAsNull(CSVReaderNullFieldIndicator.BOTH)
            .withIgnoreLeadingWhiteSpace(true)
            .withThrowExceptions(false)
            .build()
Enter fullscreen mode Exit fullscreen mode
  • .withFieldAsNull(CSVReaderNullFieldIndicator.BOTH) tells the csv parser to consider empty separators and empty quotes in the csv as null.
  • .withIgnoreLeadingWhiteSpace(true) tells the parser to ignore any leading whitespace found in the csv cells.
  • .withThrowExceptions(false) tells the parser that if there are any errors in parsing, store them in a list of exceptions. This can be made true if you want the build to stop if there is a CsvException.
  • .build() builds our CSV bean

Now with the csvBean variable we can parse it to get a list of CSVToObject objects.

val errorRows:  MutableList<CsvException> = csvBean.capturedExceptions
val successfulRows: MutableList<CSVToObject> = csvBean.parse()
Enter fullscreen mode Exit fullscreen mode

In the errorRows variable, all the rows that were not able to be built will have an CsvException. If we run errorRows.elementAt(0).message it will return the error message explaining why the first row couldn't be built.

In the successfulRows variable, all the rows that were able to be built will be stored. And if we run successfulRows.elementAt(0).name it will return "Tim".

And with that you'll have a list of objects that you can do whatever you want with. Now that the objects are populated, all the normal rules apply and you can use them however you please. Happy coding!

Top comments (0)