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. 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'

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')

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