Writing CSV with Jackson

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.

--

--

--

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

Love podcasts or audiobooks? Learn on the go with our new app.

Recommended from Medium

Why should I close the win10 update?

Symfony 4.2 curated new features

Life with Adobe Launch (1/2): The rocky history of Adobe Tag Managers

Part 3 of Building Workflow Driven .NET Applications with Elsa 2

AWS Export configuration as code (CloudFormation | Terraform)

New Strategies: a full-stack approach to utilizing an oracle system creatively.

How to Delete a Field in Drupal Using Devel PHP module

Streaming Events from SQL Server to Event Hub in Azure using Debezium

Get the Medium app

A button that says 'Download on the App Store', and if clicked it will lead you to the iOS App store
A button that says 'Get it on, Google Play', and if clicked it will lead you to the Google Play store
@cowtowncoder

@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

More from Medium

Java Consultant Tip #2: Use OpenTelemetry Java Agent to learn a new application

Easy mass-logging with AspectJ maven plugin

Visiting Reactor Netty

An epic tale: comparing JDBC and R2DBC in a real-world scenario