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