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'

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'