Writing CSV with Jackson
“How to use jackson-dataformat-csv, part 2”
Now that we know how to read CSV with Jackson…
Ok, so the assumption is that you have read my earlier post (Reading CSV with Jackson) — if not, it would probably make sense to do that first as I will be expanding on some of the aspects without repeating explanations.
But here’s a brief recap:
- While Jackson started as a JSON library, it has expanded via so-called “format modules” to support a dozen other formats, including Comma-Separated Values (CSV)
- CSV is quite different from JSON in multiple ways: it is tabular, positional text format. This means that there are some additional constructs to use when reading and/or writing CSV, compared to JSON handling. Most of Jackson API is the same, however; the main addition is concept of
CsvSchema
, used to bind positional nature of values in CSV documents (columns) to JSON-style named properties.
Simple, “untyped” writing of CSV data
So, first things first. As with reading, there is a low-tech option where you can simply write rows of simple columns using String
, Number
and Boolean
values with no CsvSchema
specified (in which case a default instance is used; one that specifies “comma used as the value separator character “ and so on).
If so, you can either construct the whole input value first and use regular Jackson writeValue()
method like so:
final CsvMapper CSV_MAPPER = new CsvMapper();
final Object[] value = new Object[] { // could use List as well
new Object[] { "foo", 13, true },
new Object[] { "bar", 28, false };
String csv = CSV_MAPPER.writeValueAsString(value);// foo,13,true
// bar,28,false
or, you can use SequenceWriter
to write content row by row:
try (StringWriter strW = new StringWriter()) {
SequenceWriter seqW = MAPPER.writer()
.writeValues(strW);
seqW.write(new Object[] {"foo", 13, true });
seqW.write(Arrays.asList("bar", 28, false ));
seqW.close();
String csv = strW.toString();
// same as above
}
You can even use CsvSchema
for customizing output options like so:
CsvSchema schema = CsvSchema.emptySchema()
.withQuoteChar('\'') // instead of double-quote
.withColumnSeparator(';') // instead of comma
.withLineSeparator("\r\n") // instead of \n
;
String csv = CSV_MAPPER.writer(schema)
.writeValueAsString(value);
// foo;13;true
// bar;28;false
and if you want to write a “header row”, that is written like any other row:
try (StringWriter strW = new StringWriter()) {
SequenceWriter seqW = CSV_MAPPER.writer()
.writeValues(strW);
seqW.write(Arrays.asList("name", "age", "validated"));
seqW.write(Arrays.asList(bar", 28, false ));
// ... and so on
}
So this is the simplest way to safely produce CSV-encoded content like CSV files.
Con POJOs, por favor
But as with reading, it is often helpful to use a model for writing content — your data may be in form of value objects (POJOs), for example.
To work with Java value objects you will need to define and use a CsvSchema
instance to define which POJO properties are written in which (positional) columns.
As we learned in “Reading CSV with Jackson”, there are alternative means to get an instance:
// manually create by adding columns in order:
CsvSchema schema = CsvSchema.builder()
.addColumn("name")
.addColumn("age")
.addColumn("validated")
.build();// or read from POJO
@JsonPropertyOrder({ "name", "age", "validated" }) // important!
// ^^^ without annotation properties ordered alphabetically
public class Person {
public String name;
public int age;
public boolean validated;public Person(String n, int a, boolean v) {
name = n;
age = a;
validated = v;
}
}
CsvSchema altSchema = CSV_MAPPER.schemaFor(Person.class)
And with that, you might use something like so:
try (StringWriter strW = new StringWriter()) {
// NOTE! Below will introspect and apply schema!
SequenceWriter seqW = MAPPER.writerWithSchemaFor(Person.class)
.writeValues(strW);
seqW.write(new Person("Bob", 37, false))
seqW.write(new Person("Jeff", 28, true))
}// Bob,37,false
// Jeff,28,true
Note, however, that if you want to change other CsvSchema
settings like ability to write the header row, you will need to create and modify schema separately:
final CsvSchema schema = CSV_MAPPER.schemaFor(Person.class)
.withHeader();
// and with write sequence from above, we'd get:
//
// name,age,validated
// Bob,37,false
// Jeff,28,true
Special output: Arrays/Lists
Although most CSV content is limited by format to consist of scalar values, there is one specific additional type supported by Jackson CSV module: Arrays/Lists/Sets of scalar values. For example:
@JsonPropertyOrder({ "name", "value", "tags" })
public class Metric {
public String name;
public double value;
public Collection<String> tags;
}final CsvSchema schema = CSV_MAPPER.schemaFor(Metric.class)
.withHeader();
try (StringWriter strW = new StringWriter()) {
SequenceWriter seqW = MAPPER.writer(schema)
.writeValues(strW);
seqW.write(new Metric("latency", 0.2, Arrays.asList( "http", "rest")));
}// name,value,tags
// latency,0.2,http;rest
As importantly, reader will also then recognize use of specific “Array value separator” and decode array/List entries as expected.
Anything else? Oh yes, configuration
Above is actually most of what you need to know about writing CSV data with Jackson, with one exception: there are a few configuration options you may want to change, to change the way content is output.
There are 2 ways to configure output:
- Use specifically configured
CsvSchema
- Configure
CsvMapper
with one or moreCsvGenerator.Feature
s
Configuration using CsvSchema
Configuration options available through CsvSchema
include:
- Column value separator (default: comma); may want to change to, say, semicolon — change with
schema.withColumnSeparator()
- Line separator (default: linefeed (\n)) — change with
schema.withLineSeparator()
- Escape character, if any (default: none) — change with
schema.withEscapeChar()
- Whether comments (line starting with
#
) are allowed (and if so, skipped/ignored; default: not allowed) — enable withschema.withComments()
- “Null value” to use when Java
null
written (default value:""
(empty String)) — change withschema.withNullValue()
- Array element separate (default:
;
(semicolon)) to use in case where a column is expected to contain Array value (NOTE: expectation of Array value must come from POJO property target, NOTCsvSchema
column definition) — change withschema.withArrayElementSeparator()
- “Any property” name (default: none): in case CSV row has more entries than defined columns (as specified by header row or
CsvSchema
), additional entries may be exposed as “any” properties with a fixed name. This is usually used in conjunction with Jackson@JsonAnySetter
annotation, to collect set of extra information — change withschema.withAnyPropertyName()
(there may be some others; see Javadocs for CsvSchema
for more)
Configuration using CsvGenerator.Feature’s
These configuration features can be enabled on CsvMapper
like so:
CsvMapper mapper = CsvMapper.builder()
.enable(CsvGenerator.Feature.ALWAYS_QUOTE_EMPTY_STRINGS)
.build();
This includes CsvGenerator.Feature
s:
STRICT_CHECK_FOR_QUOTING
(default: false): whether the whole output value is checked to determine if quoting (surrounding value in double quotes) is needed — if disabled, only sampling of first N characters is used, and longer values are automatically quoted without checkingOMIT_MISSING_TAIL_COLUMNS
(default: false): if last column values are not written during serialization, is it ok to just ignore them (and separators between values), or should placeholders (empty Strings) be writtenALWAYS_QUOTE_STRINGS
(default: false): Should String values always be quoted, regardless of possible needALWAYS_QUOTE_EMPTY_STRINGS
(default: false): Should empty String values always be quoted or not?ESCAPE_QUOTE_CHAR_WITH_ESCAPE_CHAR
(default: false) Should quote character itself be escaped using “escape character” (true) or by writing quote character twice (false)ESCAPE_CONTROL_CHARS_WITH_ESCAPE_CHAR
(default: false) Should Unicode/Ascii control characters (including linefeed/carriage return) be escaped (prepended with escape character,\
by default) (true) or be included as-is (false)
And That’s All, Folks!
I hope this gives an idea of both basic generation of CSV content with Jackson CSV format module and some pointers for additional configurability.