# Processing CSV

version 6.0.0-RC-1

Groovy has an optional `groovy-csv` module which provides support for reading and writing CSV (RFC 4180) data. The classes are found in the `groovy.csv` package.

## 1. CsvSlurper

`CsvSlurper` parses CSV text into a list of maps, where each row becomes a map keyed by the column headers from the first row. Values are returned as strings.

    def csv = new CsvSlurper().parseText('name,age\nAlice,30\nBob,25')
    assert csv.size() == 2
    assert csv[0].name == 'Alice'
    assert csv[0].age == '30'
    assert csv[1].name == 'Bob'

Rows support dynamic property access using the header names:

    def csv = new CsvSlurper().parseText('''\
        name,city,country
        Alice,London,UK
        Bob,Paris,France'''.stripIndent())
    assert csv[0].city == 'London'
    assert csv[1].country == 'France'

### 1.1. Configuration

The separator character and quote character can be customised:

    def csv = new CsvSlurper().setSeparator((char) '\t').parseText('name\tage\nAlice\t30')
    assert csv[0].name == 'Alice'
    assert csv[0].age == '30'

Quoted fields follow RFC 4180 — fields containing the separator, newlines, or the quote character are enclosed in quotes, with embedded quotes doubled:

    def csv = new CsvSlurper().parseText('name,note\nAlice,"hello, world"\nBob,"say ""hi"""')
    assert csv[0].note == 'hello, world'
    assert csv[1].note == 'say "hi"'

### 1.2. CSV without a header row

When the CSV has no header row, call `setUseHeader(false)`. Each row is returned as a map keyed by generated column names `c1`, `c2`, and so on:

    def csv = new CsvSlurper().setUseHeader(false).parseText('1,2\n3,4')
    assert csv.size() == 2
    assert csv[0].c1 == '1'
    assert csv[1].c2 == '4'

Alternatively, supply explicit column names with `setColumns`:

    def csv = new CsvSlurper().setUseHeader(false).setColumns('x', 'y').parseText('1,2\n3,4')
    assert csv[0].x == '1'
    assert csv[1].y == '4'

When explicit column names are supplied and `useHeader` remains true, the first row is still consumed as a header row but the supplied names take precedence over the names it contains.

### 1.3. Typed parsing

`CsvSlurper` can parse CSV directly into typed objects using Jackson databinding. Standard Jackson annotations such as `@JsonProperty` and `@JsonFormat` are supported for column name mapping and type conversion. This is particularly useful for CSV since all values are strings — Jackson handles the conversion to numeric, date, and other types automatically:

    static class Sale {
        String customer
        BigDecimal amount
    }

    def sales = new CsvSlurper().parseAs(Sale, 'customer,amount\nAcme,1500.00\nGlobex,250.50')
    assert sales.size() == 2
    assert sales[0].customer == 'Acme'
    assert sales[0].amount == 1500.00
    assert sales[1].customer == 'Globex'

Headerless CSV can also be parsed into typed objects. Positional columns are derived from the target type’s properties; use `@JsonPropertyOrder` to control the expected column order:

    @JsonPropertyOrder(['customer', 'amount'])
    static class Order {
        String customer
        BigDecimal amount
    }

    def orders = new CsvSlurper().setUseHeader(false)
            .parseAs(Order, 'Acme,1500.00\nGlobex,250.50')
    assert orders.size() == 2
    assert orders[0].customer == 'Acme'
    assert orders[1].amount == 250.50

| CSV has no native types — every cell is text, so the untyped `parse`/`parseText` API always returns `String` values, including for date/time-looking columns. Use the typed `parseAs` API with `java.time.*` fields (see Typed date and time values) for `java.time.LocalDate`, `java.time.LocalTime`, `java.time.LocalDateTime`, and `java.time.OffsetDateTime` fidelity.

## 2. CsvBuilder

`CsvBuilder` converts collections of maps or typed objects to CSV. The keys of the first map are used as column headers.

    def data = [
        [name: 'Alice', age: 30],
        [name: 'Bob', age: 25]
    ]
    def csv = CsvBuilder.toCsv(data)
    assert csv.contains('name,age')
    assert csv.contains('Alice,30')
    assert csv.contains('Bob,25')

### 2.1. Typed writing

`CsvBuilder` can also write typed objects. Jackson annotations are supported for column naming and formatting:

    static class Product {
        String name
        BigDecimal price
    }

    def products = [new Product(name: 'Widget', price: 9.99),
                    new Product(name: 'Gadget', price: 24.50)]
    def csv = CsvBuilder.toCsv(products, Product)
    assert csv.contains('name,price')
    assert csv.contains('Widget,9.99')
    assert csv.contains('Gadget,24.5')

Columns for records appear in component declaration order:

    record Measurement(String name, BigDecimal value, String unit) {}

    def data = [new Measurement('temp', 21.5, 'C')]
    def csv = CsvBuilder.toCsv(data, Measurement)
    assert csv.startsWith('name,value,unit')
    assert csv.contains('temp,21.5,C')

For other classes the column order is alphabetical. Use `@JsonPropertyOrder` on the type to set an explicit column order.

### 2.2. Round-trip

CSV written by `CsvBuilder` can be read back with `CsvSlurper`:

    def original = [[name: 'Alice', age: '30'], [name: 'Bob', age: '25']]
    def csv = CsvBuilder.toCsv(original)
    def parsed = new CsvSlurper().parseText(csv)
    assert parsed[0].name == 'Alice'
    assert parsed[1].age == '25'

### 2.3. Typed date and time values

When a target type declares `java.time.*` fields, `CsvBuilder.toCsv` and the typed `CsvSlurper.parseAs` API round-trip temporal values with full fidelity:

    static class Event {
        java.time.LocalDate day
        java.time.LocalTime windowStart
        java.time.LocalDateTime updated
        java.time.OffsetDateTime created
        String label
    }

    def original = [new Event(
            day: java.time.LocalDate.of(1979, 5, 27),
            windowStart: java.time.LocalTime.of(7, 32, 0),
            updated: java.time.LocalDateTime.of(1979, 5, 27, 7, 32, 0),
            created: java.time.OffsetDateTime.parse('1979-05-27T07:32:00-08:00'),
            label: 'first')]

    def csv = CsvBuilder.toCsv(original, Event)
    def parsed = new CsvSlurper().parseAs(Event, csv)

    assert parsed[0].day == original[0].day                  // LocalDate
    assert parsed[0].windowStart == original[0].windowStart  // LocalTime
    assert parsed[0].updated == original[0].updated          // LocalDateTime
    assert parsed[0].created == original[0].created          // OffsetDateTime, non-UTC offset preserved
    assert parsed[0].label == original[0].label