Jackson API beyond ObjectMapper

aka ObjectReader/ObjectWriter, JsonMapper.builder FTW

@cowtowncoder
5 min readNov 24, 2021

“Classic” Jackson API w/ ObjectMapper

The most often used class in the whole Jackson databind API is — without question — ObjectMapper. ObjectMapper has been around since Jackson 0.9.5 and has methods for reading and writing JSON (*), for converting structurally compatible values, and for configuring details of these operations.
Because of this, it is widely used in examples and its use is documented quite well.

Typical example of such “classic” Jackson usage could look like:

ObjectMapper mapper = new ObjectMapper();// Changes to defaults: no fail on reading unknown properties;
// do use default indenting ("pretty-printing") mapper.disable(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES);
mapper.enable(SerializationFeature.INDENT_OUTPUT);
// and then read, convert, modify, write:
MyValue v = mapper.readValue(new File("value.json"), MyValue.class);
Map<String, Object> asMap = mapper.convertValue(v, Map.class);
asMap.put("extra", 123);
String jsonModified = mapper.writeValueAsString(asMap);

This works just fine.

But over time the Jackson databind API has evolved to offer alternatives and additions to the “classic” ObjectMapper . Let’s see what these alternatives are, and why we would want to use them.

(*) and other supported formats, with different format backends

ObjectReader and ObjectWriter: light-weight and reconfigurable

One big challenge for using ObjectMapper is its thread-safety: if (and only if!) you FULLY configure it before any use (reading, writing or conversions) its use IS fully thread-safe — you can share mapper instances across threads, use concurrently. But you cannot change any of its configuration after first use — partly since configuration methods are not synchronized, but also since only some of configuration changes would take any effect after first use.
(side note: there is actually oneway to create differently configure mappers by using ObjectMapper.copy(), then configuring that instance before any use)

This is where ObjectReader and ObjectWriter classes come in: you can think of ObjectMapper as a factory for these objects: they are cheap to construct and their configuration can be safely changed in a thread-safe manner.
Both also only expose (re)configuration methods that are safe to use and eliminate any possibility of doing something that is dangerous (causing synchronization issues) or not work the way you’d expect.
Looking at the earlier example we can instead use:

MyValue v = mapper.readerFor(MyValue.class)
.without(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES)
.readValue(new File("value.json"));
Map<String, Object> asMap = mapper.convertValue(v, Map.class);
asMap.put("extra", 123);
String jsonModified = mapper.writer()
.with(SerializationFeature.INDENT_OUTPUT)
.writeValueAsString(asMap);

In this case difference is not big, but even here we could dynamically change indentation aspects on per-writer basis. Since Reader/Writer instances are cheap to create, use, we can create them dynamically —or, since they are fully thread-safe, alternatively pre-create and hold references to the fully configured alternatives. There is even a small performance benefit from creating type-bound instance of readers (“mapper.readerFor(type)”) as it can pre-fetch Deserializer instance(s) needed, instead of doing look-up for every readValue() call.
But the biggest benefit really is the safety: you know that anything that can be change via API is safe to change.

Configuration changes work in chained/fluent manner and calls do not need to start with an ObjectMapper:

ObjectReader defaultReader = mapper.reader(); // maybe with configs
ObjectReader valueReader = defaultReader.forType(Value.class);
ObjectWriter defaultWriter = mapper.writer();
ObjectReader indentingWriter = defaultWriter.withDefaultPrettyPrinter();

This lends itself quite well also to safe re-configurability as part of frameworks: framework may prevent changes to ObjectMapper but expose actual ObjectReader / ObjectWriter used for individual read/write operation. This can be important to prevent changes to things like configured mix-ins (only changeable via ObjectMapper) or security settings related to polymorphic deserialization.

Finally, there are also many configurable settings that are actually not even available via ObjectMapper: most notably active “View” (see @JsonView annotation) used when reading/writing.

Safe(r) construction with Builders (2.10+)

I briefly mentioned that there are ObjectMapper settings that cannot be changed on per-call basis, and as such are not available through ObjectReader or ObjectWriter. Because of this, changing those settings remains challenging from API perspective: users must use “config-then-use” pattern rigorously to avoid issues. They may also be surprised to find that attempts to, say, “add one more mix-in” will just plain Not Work when attempted (but without exception or other error indication).
I wrote in depth about this problem (as well as solution I’ll show next) in Jackson 3.0: Immutability w/ Builders so if interested, you may read that first.

As part of Jackson 3.0 work, a replacement was designed and implemented: use of Builder-style construction. And while initially the idea was to only use this with 3.0, it soon became clear that back-porting in 2.x could be useful in making eventual transition easier, and bring some (although not all) benefits: specifically, developers who follow builder-style construction (along with using ObjectReader/ObjectWriter) can ensure their configuration usage is safe and correct.

So let’s look at another example:

ObjectMapper mapper = JsonMapper.builder() // more on this later
.enable(MapperFeature.USE_STD_BEAN_NAMING)
// can also define baseline settings for things we can reconfig:
.disable(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES)
// but mix-ins must be set before use:
.addMixIn(MixIn.class, MyValue.class)
.build();

So: you can both set things that can only be configured (before use) on mapper (such as mix-in mappings and MapperFeatures) and also define default settings for more dynamic features (like SerializationFeature and DeserializationFeature).

But there is more! You can also safely configure settings of the underlying stream factory (JsonFactory and format-specific alternatives). While we will look at format-specific variants in the next section, the basic idea is that you can pass this factory to ObjectMapper builder:

JsonFactory jsonF = JsonFactory.builder()
// configure
.build();
ObjectMapper mapper = JsonMapper.builder(jsonF)
.enable(...)
.build();

There are also some settings to configure streaming-level features, accessible either via JsonFactory builder or ObjectMapper builder.

Format-specific mapper subtypes (XmlMapper et al)

The last area of extensibility that has grown over time is that of format-specific handling: this is also written about in-depth in “Jackson 3.0 immutability with Builders”. As background, initially (during Jackson 1.x) all format-specific variations in processing were handled completely at Streaming API level, by subtypes of JsonFactory and parser/generators (JsonParser, JsonGenerator subtypes): ObjectMapper itself was completely format-agnostic. This means that usage followed pattern of:

ObjectMapper yamlMapper = new ObjectMapper(new YAMLFactory());
MyValue v = yamlMapper.readValue(new File("config.yaml"),
MyValue.class);

This worked fine for most format backends, initially, but over time 2 challenges became obvious:

  1. There is need to expose format-specific configuration settings
  2. Some formats (initially XML) require overriding some of ObjectMapper functionality directly

As a result, with 2.10 and later, there are specific format-specific subtypes for ALL format backends were added (originally only some existed, such as XmlMapper, CsvMapper and ProtobufMapper), including JsonMapper you have seen in code samples above.
With addition of Builder-style construction it is now possible to construct and configure format-specific mappers conveniently (no casting needed) and safely (only configuration options relevant are exposed):

ObjectMapper xmlMapper = XmlMapper.builder()
.defaultUseWrapper(false) // use "unwrapped" Lists in XML
.disable(ToXmlGenerator.WRITE_XML_DECLARATION)
.build();
// note: we could have held on to XmlMapper, but actual use
// is via readValue(), writeValue() methods

In this case, option to use (or not) of “wrappers” for Lists is specific to XML and not available for builders of other mapper types.
Same pattern is used with other mappers, most notably with CSV (CsvMapper) and Properties (JavaPropsMapper): and even JSON-backed mapper (JsonMapper) is being retrofitted to work with some features only applicable to JSON:

ObjectMapper jsonMapper = JsonMapper.builder()
// non-standard setting for unquoted NaN values:
.disable(JsonWroteFeatire.WRITE_NAN_AS_STRINGS)
.build();

That’s All, Folks!

There are other closely related abstractions, and before closing on this post, here are couple of quick pointers:

And that’s it until next post!

--

--

@cowtowncoder
@cowtowncoder

Written by @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

Responses (2)