How do I export my DTO into CSV with its original variable naming and declaration order?

For the purpose of this short article, we will be using OpenCSV 5.7.1 with Kotlin. The End Goal So first and foremost, let us define what we want. We want to convert a list of our DTO, into a CSV. data class SomeCoolDto( val someString: String, val someInt: Int, ) val listOfCoolDtos = listOf( SomeCoolDto( someString = "hey", someInt = 7874 ) ) Should turn into: "someString","someInt" "hey","7874" Try 1.0 : Let’s use @CsvBindByName At first glance, I’m thinking, hey, this annotation seems pretty similar to what we need. We just specify column name and voila! data class SomeCoolDto( @CsvBindByName(column = "some string") val someString: String, @CsvBindByName(column = "some int") val someInt: Int, ) val listOfCoolDtos = listOf( SomeCoolDto( someString = "hey", someInt = 7874 ) ) fun toByteArray(listOfDto: List): ByteArray { val outputStream = ByteArrayOutputStream() OutputStreamWriter(outputStream).use { writer -> val csvWriter = StatefulBeanToCsvBuilder(writer) .build() csvWriter.write(listOfDto) } return outputStream.toByteArray() } //This is how we can verify our CSV val bytes = toByteArray(listOfCoolDtos) println(bytes.toString(UTF_8).trimIndent()) And if we actually run the above code, we get the following: "SOME INT","SOME STRING" "7874","hey" We’re getting somewhere, but it still needs a little help. Here’s what’s missing: Hey, why are my headers all capitalised! I want it to follow my variable naming which is camel cased. And why isn’t the order of my variable declaration followed! It should be someString followed by someInt just like how it is in my DTO. I like to keep things dynamic too. If I change my variable name of my DTO, I want to only change it at one place. Basic Don’t repeat yourself (DRY) principles. Try 2.0 : Let’s use @CsvBindByPosition Which brings us to our second try. I spotted something that might potentially be useful: @CsvBindByPosition . This is how our new DTO will look like with our new annotations: data class SomeCoolDto( @CsvBindByPosition(position = 0) val someString: String, @CsvBindByPosition(position = 1) val someInt: Int, ) Similarly, we can use our toByteArray function above, which gives us this output: "hey","7874" But looks like our headers are missing. This means we might need to handle the headers and write it to our CSV on our own. Try 2.1 : @CsvBindByPosition + appending headers Let’s have our very own generateCsvHeaders function which takes in our class object, and spits out a list of it’s declared fields, in ascending CsvBindByPosition index positions. fun generateCsvHeaders(clazz: Class): List { return clazz.declaredFields .sortedBy { field -> ( field.getAnnotation(CsvBindByPosition::class.java)?.position ?: field.getAnnotation( CsvBindAndSplitByPosition::class.java, )?.position ) ?: Int.MAX_VALUE } .mapNotNull { field -> field.name } } We then tweak our toByteArray function to include our new headers. fun toByteArray(listOfDto: List): ByteArray { val outputStream = ByteArrayOutputStream() OutputStreamWriter(outputStream).use { writer -> val csvWriter = StatefulBeanToCsvBuilder(writer) .build() //2 New lines to add headers here val headers = generateCsvHeaders(SomeCoolDto::class.java) writer.appendLine(headers.joinToString(",")) csvWriter.write(listOfDto) } return outputStream.toByteArray() } Output: someString,someInt "hey","7874" This is almost like what we want. But remember, we want dynamic. In the future if we change the order of variable declaration, we want our CSV headers to change accordingly as well, without needing to change any other variables (in this case position index value). Last try: ColumnPositionMappingStrategy + appending headers Now, our final form. Our DTO still looks like the original, clean and annotation free. data class SomeCoolDto( val someString: String, val someInt: Int, ) We then have a generateCsvHeaders function which gives us a list of the headers according to the variable declaration order. fun generateCsvHeaders(clazz: Class): List { return clazz.declaredFields .mapNotNull { field -> field.name } } We then use ColumnPositionMappingStrategy to denote the mapping of the CSV columns with the order declaration. Additionally, the headers are also written at the top of the CSV. fun toByteArray(listOfDto: List): ByteArray { val outputStream = ByteArrayOutputStream() // Get headers val headers = generateCsvHeaders(SomeCoolDto::class.java) // Define column mapping strategy val s

Feb 23, 2025 - 12:52
 0
How do I export my DTO into CSV with its original variable naming and declaration order?

For the purpose of this short article, we will be using OpenCSV 5.7.1 with Kotlin.

The End Goal

So first and foremost, let us define what we want. We want to convert a list of our DTO, into a CSV.

data class SomeCoolDto(
    val someString: String,
    val someInt: Int,
)

val listOfCoolDtos = listOf(
    SomeCoolDto(
        someString = "hey", someInt = 7874
    )
)

Should turn into:

"someString","someInt"
"hey","7874"

Try 1.0 : Let’s use @CsvBindByName

At first glance, I’m thinking, hey, this annotation seems pretty similar to what we need. We just specify column name and voila!

data class SomeCoolDto(
    @CsvBindByName(column = "some string")
    val someString: String,
    @CsvBindByName(column = "some int")
    val someInt: Int,
)

val listOfCoolDtos = listOf(
    SomeCoolDto(
        someString = "hey", someInt = 7874
    )
)

fun toByteArray(listOfDto: List<SomeCoolDto>): ByteArray {
    val outputStream = ByteArrayOutputStream()

    OutputStreamWriter(outputStream).use { writer ->
        val csvWriter = StatefulBeanToCsvBuilder<SomeCoolDto>(writer)
            .build()

        csvWriter.write(listOfDto)
    }
    return outputStream.toByteArray()
}

//This is how we can verify our CSV
val bytes = toByteArray(listOfCoolDtos)
println(bytes.toString(UTF_8).trimIndent())

And if we actually run the above code, we get the following:

"SOME INT","SOME STRING"
"7874","hey"

We’re getting somewhere, but it still needs a little help. Here’s what’s missing:

  1. Hey, why are my headers all capitalised! I want it to follow my variable naming which is camel cased.
  2. And why isn’t the order of my variable declaration followed! It should be someString followed by someInt just like how it is in my DTO.
  3. I like to keep things dynamic too. If I change my variable name of my DTO, I want to only change it at one place. Basic Don’t repeat yourself (DRY) principles.

Try 2.0 : Let’s use @CsvBindByPosition

Which brings us to our second try. I spotted something that might potentially be useful: @CsvBindByPosition .

This is how our new DTO will look like with our new annotations:

data class SomeCoolDto(
    @CsvBindByPosition(position = 0)
    val someString: String,
    @CsvBindByPosition(position = 1)
    val someInt: Int,
)

Similarly, we can use our toByteArray function above, which gives us this output:

"hey","7874"

But looks like our headers are missing. This means we might need to handle the headers and write it to our CSV on our own.

Try 2.1 : @CsvBindByPosition + appending headers

Let’s have our very own generateCsvHeaders function which takes in our class object, and spits out a list of it’s declared fields, in ascending CsvBindByPosition index positions.

fun generateCsvHeaders(clazz: Class<*>): List<String> {
    return clazz.declaredFields
        .sortedBy { field ->
            (
                    field.getAnnotation(CsvBindByPosition::class.java)?.position ?: field.getAnnotation(
                        CsvBindAndSplitByPosition::class.java,
                    )?.position
                    ) ?: Int.MAX_VALUE
        }
        .mapNotNull { field ->
            field.name
        }
}

We then tweak our toByteArray function to include our new headers.

fun toByteArray(listOfDto: List<SomeCoolDto>): ByteArray {
    val outputStream = ByteArrayOutputStream()

    OutputStreamWriter(outputStream).use { writer ->
        val csvWriter = StatefulBeanToCsvBuilder<SomeCoolDto>(writer)
            .build()

        //2 New lines to add headers here
        val headers = generateCsvHeaders(SomeCoolDto::class.java)
        writer.appendLine(headers.joinToString(","))

        csvWriter.write(listOfDto)
    }
    return outputStream.toByteArray()
}

Output:

someString,someInt
"hey","7874"

This is almost like what we want. But remember, we want dynamic. In the future if we change the order of variable declaration, we want our CSV headers to change accordingly as well, without needing to change any other variables (in this case position index value).

Last try: ColumnPositionMappingStrategy + appending headers

Now, our final form. Our DTO still looks like the original, clean and annotation free.

data class SomeCoolDto(
    val someString: String,
    val someInt: Int,
)

We then have a generateCsvHeaders function which gives us a list of the headers according to the variable declaration order.

fun generateCsvHeaders(clazz: Class<*>): List<String> {
    return clazz.declaredFields
        .mapNotNull { field ->
            field.name
        }
}

We then use ColumnPositionMappingStrategy to denote the mapping of the CSV columns with the order declaration. Additionally, the headers are also written at the top of the CSV.

fun toByteArray(listOfDto: List<SomeCoolDto>): ByteArray {
    val outputStream = ByteArrayOutputStream()

    // Get headers
    val headers = generateCsvHeaders(SomeCoolDto::class.java)

    // Define column mapping strategy
    val strategy = ColumnPositionMappingStrategy<SomeCoolDto>()
    strategy.type = SomeCoolDto::class.java
    strategy.setColumnMapping(*headers.toTypedArray())

    OutputStreamWriter(outputStream).use { writer ->
        val csvWriter = CSVWriter(writer)
        // Write headers
        csvWriter.writeNext(headers.toTypedArray())

        val beanToCsv = StatefulBeanToCsvBuilder<SomeCoolDto>(writer)
            .withMappingStrategy(strategy)
            .build()
        beanToCsv.write(listOfDto)
    }
    return outputStream.toByteArray()
}

This will give us the following output:

"someString","someInt"
"hey","7874"

The best part about this, in the future, say our new colleague adds a new variable, or even switch the order declaration. No code change is needed! The headers and column data will all fall in place perfectly.

Inspired by Franz Wong who wrote a different variation but in Java here:
https://dev.to/franzwong/writing-csv-file-with-opencsv-without-capitalized-headers-and-follows-declaration-order-207e