Writing CSV with Jackson

“How to use jackson-dataformat-csv, part 2”

@cowtowncoder
5 min readOct 21, 2021

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:

  1. Use specifically configured CsvSchema
  2. Configure CsvMapper with one or more CsvGenerator.Features

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 with schema.withComments()
  • “Null value” to use when Java null written (default value: "" (empty String)) — change with schema.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, NOT CsvSchema column definition) — change with schema.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 with schema.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 checking
  • OMIT_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 written
  • ALWAYS_QUOTE_STRINGS (default: false): Should String values always be quoted, regardless of possible need
  • ALWAYS_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.

--

--

@cowtowncoder

Open Source developer, most known for Jackson data processor (nee “JSON library”), author of many, many other OSS libraries for Java, from ClassMate to Woodstox