DEV Community

Hakim
Hakim

Posted on

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
    )
)
Enter fullscreen mode Exit fullscreen mode

Should turn into:

"someString","someInt"
"hey","7874"
Enter fullscreen mode Exit fullscreen mode

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())
Enter fullscreen mode Exit fullscreen mode

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

"SOME INT","SOME STRING"
"7874","hey"
Enter fullscreen mode Exit fullscreen mode

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,
)
Enter fullscreen mode Exit fullscreen mode

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

"hey","7874"
Enter fullscreen mode Exit fullscreen mode

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
        }
}
Enter fullscreen mode Exit fullscreen mode

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()
}
Enter fullscreen mode Exit fullscreen mode

Output:

someString,someInt
"hey","7874"
Enter fullscreen mode Exit fullscreen mode

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,
)
Enter fullscreen mode Exit fullscreen mode

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
        }
}
Enter fullscreen mode Exit fullscreen mode

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()
}
Enter fullscreen mode Exit fullscreen mode

This will give us the following output:

"someString","someInt"
"hey","7874"
Enter fullscreen mode Exit fullscreen mode

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

Speedy emails, satisfied customers

Postmark Image

Are delayed transactional emails costing you user satisfaction? Postmark delivers your emails almost instantly, keeping your customers happy and connected.

Sign up

Top comments (0)

A Workflow Copilot. Tailored to You.

Pieces.app image

Our desktop app, with its intelligent copilot, streamlines coding by generating snippets, extracting code from screenshots, and accelerating problem-solving.

Read the docs

👋 Kindness is contagious

Please leave a ❤️ or a friendly comment on this post if you found it helpful!

Okay