kotlin-csv

repository·main·Indexed 20 days ago

https://github.com/jsoizo/kotlin-csv

A pure Kotlin Multiplatform library for reading and writing CSV files across JVM, JS, Wasm, and Native platforms. It features a type-safe DSL for configuration, support for custom CsvDialects, and streaming capabilities via Sequence. Key features include header support (returning rows as LinkedHashMap), nullable reads/writes, and configurable field-count policies to handle rows with excess or insufficient fields.

Tokens
9.2K
Snippets
33
Records
46
Agent score
65%

What's inside kotlin-csv

  1. How CSV Dialects work

    main

    A CsvDialect is a value object that defines the structural characters of the CSV format. It is shared between readers and writers to ensure consistency. It consists of four properties:

    • delimiter: The character separating fields.
    • quoteChar: The character used to wrap fields.
    • escapeChar: The character used to escape special characters.
    • lineTerminator: The character(s) defining the end of a row.

    You can use built-in dialects like CsvDialect.RFC4180 or CsvDialect.TSV, or define a custom one.

    val customWriter = csvWriter {
        dialect = CsvDialect(delimiter = ';', escapeChar = '\\')
    }
  2. How statelessness works in CsvReader and CsvWriter

    main

    Both CsvReader and CsvWriter are stateless. All configuration is captured in an immutable Config data class held by the instance.

    Concurrency Rules:

    • You can safely reuse the same instance across multiple calls and threads.
    • The returned Sequence follows the standard cold-sequence contract: iterating a single sequence from multiple threads is unsafe.
    • However, separate read or write calls on the same instance are independent and can proceed concurrently.
  3. Understand exception timing in Sequence vs Eager APIs

    main

    The timing of when exceptions are thrown depends on whether you are using lazy (Sequence-returning) or eager APIs:

    Lazy/Sequence APIs

    For APIs that return a Sequence (such as CsvReader.read, Sequence.withHeader, or the I/O-layer read(...) { ... }), exceptions are not thrown when the sequence is created. Instead, they surface during the terminal operation that drives iteration (e.g., .toList(), .forEach(), or .first()) once the iterator reaches the offending row.

    Eager APIs

    For eager APIs (such as CsvReader.readAll), exceptions propagate immediately from the call site.

  4. Install kotlin-csv

    main

    Add the dependency to your build system. The multiplatform artifact resolves JVM, JS, Kotlin/Wasm, and supported Kotlin/Native variants automatically.

    ### Gradle (Kotlin DSL)
    ```kotlin
    implementation("com.jsoizo:kotlin-csv:2.0.0")

    Gradle (Groovy DSL)

    implementation 'com.jsoizo:kotlin-csv:2.0.0'

    Maven

    <dependency>
      <groupId>com.jsoizo</groupId>
      <artifactId>kotlin-csv-jvm</artifactId>
      <version>2.0.0</version>
    </dependency>
    <dependency>
      <groupId>com.jsoizo</groupId>
      <artifactId>kotlin-csv-js</artifactId>
      <version>2.0.0</version>
    </dependency>
  5. Upgrade from kotlin-csv 1.x to 2.0

    main

    The v2 migration involves several key changes:

    • Package Layout: Public surface moved from client/ and dsl.context/ to reader/, writer/, exceptions/, and the package root.
    • Configuration: Mutable *Context holders are replaced by immutable CsvReaderConfig and CsvWriterConfig data classes. Format characters (delimiter, quote, etc.) are now grouped in a CsvDialect object.
    • I/O Pattern: Function-style I/O is preferred. Instead of open { ... } blocks, use reader.readFromFile(file) { rows -> ... } or writer.writeToFile(rows, file). The lambda owns the resource lifecycle.
    • Core Data Type: The library is now sequence-first. read returns Sequence<List<String>> and write accepts Sequence<List<String>>.
    • Charset: Charset is no longer a configuration field; it is passed as an argument to JVM-specific I/O overloads. commonMain and JS are UTF-8 only.
    • Async: openAsync and writeAllAsync are removed. Use withContext(Dispatchers.IO) to wrap synchronous calls.
  6. Write CSV data

    main

    Use csvWriter() to create a reusable, stateless writer instance.

    Key features:

    • Output Formats: Write to Strings (eagerly) or Files.
    • Nullable Writes: Use writeAllNullable to emit null values as unquoted empty fields.
    • Streaming: Supports both List<List<String>> and Sequence<List<String>> as row sources.
    import com.jsoizo.kotlincsv.csvWriter
    import com.jsoizo.kotlincsv.writer.WriteQuoteMode
    import com.jsoizo.kotlincsv.writer.writeToFile
    import java.io.File
    
    val writer = csvWriter()
    val rows = listOf(
        listOf("a", "b", "c"),
        listOf("d", "e", "f"),
    )
    
    // To a String (eager)
    val csv: String = writer.writeAll(rows)
    
    // To a File
    writer.writeToFile(rows, File("out.csv"))
    
    // Nullable writes
    val nullableCsv = csvWriter {
        quoteMode = WriteQuoteMode.ALL
    }.writeAllNullable(listOf(listOf(null, "", "value")))
  7. Manage resources when reading or writing files

    main

    To prevent resource leaks, use the lambda-based I/O extensions. These extensions open the resource, hand it to your block, and automatically close it when the block returns or throws an exception.

    Important Contract: You must consume the returned Sequence inside the block. Returning the sequence from the block will result in a leaked handle to a closed source, causing IOException or garbage data during later iteration.

    For cases where you need all data at once, use the eager readAll... overloads instead of manually calling .toList() inside a block.

    // Correct: consume inside the block
    reader.readFromFile(file) { rows ->
        rows.take(100).forEach { println(it) }
    }
    
    // Correct: use eager overload for full materialization
    val rows: List<List<String>> = reader.readAllFromFile(file)
  8. Read CSV data

    main

    Use csvReader() to create a reusable, stateless reader instance. You can read from Strings (eagerly) or Files (using a lambda to manage resources).

    Key features:

    • Header Support: Use .withHeader() to return rows as LinkedHashMap objects, preserving column order.
    • Nullable Reads: Use readAllNullable and configure nullFieldIndicator to distinguish between empty strings and actual null values.
    • Streaming: readFromFile provides a cold Sequence<List<String>>, allowing for efficient processing of large files via short-circuiting operations like take(n).
    import com.jsoizo.kotlincsv.csvReader
    import com.jsoizo.kotlincsv.reader.readFromFile
    import com.jsoizo.kotlincsv.reader.withHeader
    import com.jsoizo.kotlincsv.reader.CsvNullFieldIndicator
    import java.io.File
    
    val reader = csvReader()
    
    // From a String (eager)
    val rows: List<List<String>> = reader.readAll("a,b,c\nd,e,f")
    
    // From a File (streaming/resource-managed)
    reader.readFromFile(File("data.csv")) { rows ->
        rows.forEach { println(it) }
    }
    
    // With a header row
    reader.readFromFile(File("data.csv")) { rows ->
        val records = rows.withHeader().toList()
        println(records.first()["id"])
    }
    
    // Nullable reads
    val nullableRows = csvReader {
        nullFieldIndicator = CsvNullFieldIndicator.EMPTY_SEPARATORS
    }.readAllNullable("\"empty\",\"null\"\n\"\",")
  9. Update dependencies for kotlin-csv 2.0

    main

    The groupId and artifactId remain the same. Use the following for your build system:

    Gradle (Kotlin DSL)

    implementation("com.jsoizo:kotlin-csv:2.0.0")

    Gradle (Groovy DSL)

    implementation 'com.jsoizo:kotlin-csv:2.0.0'

    Maven

    <dependency>
      <groupId>com.jsoizo</groupId>
      <artifactId>kotlin-csv-jvm</artifactId>
      <version>2.0.0</version>
    </dependency>

    kscript

    @file:DependsOn("com.jsoizo:kotlin-csv-jvm:2.0.0")

    Note: kotlinx-io-core is a transitive dependency. Multiplatform projects will resolve JVM, JS, and supported Kotlin/Native variants automatically.

    implementation("com.jsoizo:kotlin-csv:2.0.0")
  10. Construct a CsvReader or CsvWriter

    main

    You can create reader and writer instances using DSL builders or by passing a pre-built Config object.

    • Use csvReader { ... } to build a CsvReader.
    • Use csvWriter { ... } to build a CsvWriter.
    • Use csvReader(config) or csvWriter(config) if you already have a configuration object.
    val reader = csvReader { 
        // configuration DSL
    }
    
    val writer = csvWriter { 
        // configuration DSL
    }
  11. Configure CSV Writer options

    main

    Configure the csvWriter using a DSL block. Options control the output dialect, line terminators, and quoting behavior.

    val writer = csvWriter {
        dialect = CsvDialect.RFC4180
        outputLastLineTerminator = true
        quoteMode = WriteQuoteMode.CANONICAL
    }
  12. Configure CSV Reader options

    main

    Configure the csvReader using a DSL block. Options control how the parser handles dialects, empty lines, field mismatches, and null values.

    val reader = csvReader {
        dialect = CsvDialect.RFC4180
        skipEmptyLine = true
        excessFieldsRowBehaviour = ERROR
        insufficientFieldsRowBehaviour = ERROR
        nullFieldIndicator = CsvNullFieldIndicator.NEITHER
    }