Published on

Working With CSVs In Kotlin

Authors

I recently had a requirement where I had to take a bunch of records and build a report out of them. The easiest format to represent this report in is a CSV as this is viewable with no program if need be or it can simply be opened in a text editor. If the given report is used for accounting purposes it can also be imported into software like Xero fairly easily.

Rolling out your own CSV builder is fairly straightforward. But in the same way, you can roll out your own XML or JSON marshaller, it is better to use battle-tested tooling. This tooling will already likely handle edge cases and also simplify the task of catering for field additions or removals.

I ended up using Jackson's CSV marshaller for this purpose. In addition to the usual Jackson dependencies we will also need the following:

//csv
implementation "com.fasterxml.jackson.dataformat:jackson-dataformat-csv:2.11.3"

implementation "com.fasterxml.jackson.module:jackson-module-kotlin"

If you use Spring, Spring will pull in the other Jackson dependencies for you.

Now that we have the dependencies lets use a helper class to ease working with Jackson for CSVs as it can be a bit fiddly. After a bit of Googling and tinkering I came up with the following helper and extensions which ease the process of working with CSVs in Kotlin:

package com.example

import com.fasterxml.jackson.databind.DeserializationFeature
import com.fasterxml.jackson.databind.ObjectWriter
import com.fasterxml.jackson.databind.SerializationFeature
import com.fasterxml.jackson.dataformat.csv.CsvMapper
import com.fasterxml.jackson.datatype.jsr310.JavaTimeModule
import com.fasterxml.jackson.module.kotlin.KotlinModule
import java.io.File
import java.io.FileWriter

object CSVHelper {

    val mapper: CsvMapper = CsvMapper().apply {
        registerModules(KotlinModule(), JavaTimeModule())
            .configure(SerializationFeature.WRITE_DATES_AS_TIMESTAMPS, false)
            .configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false)
    }

    fun <C> writer(javaClass: Class<C>, withHeader: Boolean = true): ObjectWriter {
        if (withHeader) {

            return mapper.writer(mapper.schemaFor(javaClass).withHeader())
        }
        return mapper.writer(mapper.schemaFor(javaClass).withoutHeader())
    }


    inline fun <reified T> writeCsvFile(data: Collection<T>, fileName: String, withHeader: Boolean = true): File {
        val tempFile = createTempFile(prefix = fileName, suffix = ".csv")

        FileWriter(tempFile).use { writer ->
            writer(T::class.java, withHeader)
                .writeValues(writer)
                .writeAll(data)
                .close()
        }

        return tempFile
    }


    inline fun <reified T> writeCsvString(data: T, withHeader: Boolean = true): String {
        return writer(data!!::class.java, withHeader).writeValueAsString(data)
    }

}

inline fun <reified T> T.toCSV(withHeader: Boolean = true): String = CSVHelper.writeCsvString(this, withHeader)

inline fun <reified T> Collection<T>.toCSV(withHeader: Boolean = true): String {
    if (this.isEmpty()) {
        return ""
    }
    val firstEntry = this.first().toCSV(withHeader)
    if (this.size == 1) {
        return firstEntry
    }
    val rest = this.drop(1).joinToString("\n") { it.toCSV(false) }

    return "$firstEntry\n$rest"
}

References