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,
)
We can break the code down into pieces:
@NoArg
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,
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,
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());
Then use Java's file interface and reader object to create a character stream:
val reader : Reader = Files.newBufferedReader(path)
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()
-
.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 aCsvException
. -
.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()
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)