Search code examples
kotlinparsinggenericscsv-parsercsvmapper

Use Generics in Kotlin class for parsing CSV


I'm using CSVMapper in my kotlin code to parse a CSV file.

Here's a sample code:

data class UserDto(name: String): Serializable {
}

class CSVMigrator {
    private val csvFile: String
    private val csvMapper = CsvMapper().registerModule(
        KotlinModule.Builder()
            .withReflectionCacheSize(512)
            .configure(KotlinFeature.NullToEmptyCollection, false)
            .configure(KotlinFeature.NullToEmptyMap, false)
            .configure(KotlinFeature.NullIsSameAsDefault, false)
            .configure(KotlinFeature.StrictNullChecks, false)
            .build()
    )

    public constructor(filePath: String) {
        this.csvFile = filePath
    }

    private inline fun <reified T> getIterator(reader: Reader): MappingIterator<T>? {
        val schema = CsvSchema
            .emptySchema()
            .withLineSeparator("\n")
            .withColumnSeparator(',')
            .withQuoteChar('"')
            .withHeader()

        return csvMapper
            .readerFor(T::class.java)
            .without(StreamReadFeature.AUTO_CLOSE_SOURCE)
            .with(schema)
            .readValues<T>(reader)
    }

    fun readFromFile() {
        FileReader(csvFile).use { reader ->
            val iterator = getIterator<UserDto>(reader)
            if (iterator != null) {

                var data = mutableListOf<UserDto>()
                while (iterator.hasNext()) {
                    try {
                        val lineElement = iterator.next()
                        print(lineElement)
                    } catch (e: RuntimeJsonMappingException) {
                        println("Iterator Exception: " + e.localizedMessage)
                    }
                }
            }
        }
    }
}

In the above code, I've hard-coded the UserDto class to parse the CSV to a model. But I want to make this class generic (as in below code) such that I can parse any CSV based on the data class.

class CSVMigrator<X: Serializable> {
}

And I want to change UserDto with Generic class "X" everywhere.
I'm getting the below error when I replace my data class with "X" in the line val iterator = getIterator<X>(reader)

The error is

Cannot use 'X' as reified type parameter. Use a class instead.

Solution

  • Class type parameters cannot be reified, so they cannot be passed to generic methods with reified type parameters.

    You should store a Class<X> in CSVMigrator, or a TypeReference if X itself can be a generic type. Then write a non-reified version of getIterator and pass those to a different overload of readerFor that takes Class<T>/TypeReference.

    class CSVMigrator<X: Serializable>(
        val clazz: Class<X>,
    //  or:
    //  val typeReference: TypeReference<X>,
    )
    
    private fun <T> getIterator(
        reader: Reader,
        clazz: Class<T>,
    //  or:
    //  typeReference: TypeReference<T>,
    ): MappingIterator<T>? {
        val schema = CsvSchema
            .emptySchema()
            .withLineSeparator("\n")
            .withColumnSeparator(',')
            .withQuoteChar('"')
            .withHeader()
    
        return csvMapper
            .readerFor(clazz /* or typeReference */)
            .without(StreamReadFeature.AUTO_CLOSE_SOURCE)
            .with(schema)
            .readValues<T>(reader)
    }
    

    Usage:

    val iterator = getIterator(this.clazz /* or this.typeReference */)
    

    When creating a CSVMigrator, pass in the Class or TypeReference.

    CSVMigrator(UserDto::class.java)
    // or: (not sure if Jackson for Kotlin has an easier to create a TypeReference)
    CSVMigrator(object: TypeReference<SomeGenericType<String>>() {})