1. Overview
Jackson’s JsonMapper class is the easiest way to parse JSON in Java. Eventually, we may want to tweak how JSON is produced or parsed, make it easier to read, tolerate extra fields, pick a different date format, or change the naming style of properties. These adjustments are common in many day-to-day tasks, especially when building RESTful APIs, microservices, or simply working with JSON data in our applications.
In this lesson, we’ll see how to configure a JsonMapper for some common scenarios and the difference between global and annotation-based configuration.
The relevant module we need to import when starting this lesson is: jsonmapper-configuration-start.
If we want to reference the fully implemented lesson, we can import: jsonmapper-configuration-end.
2. JsonMapper Basic Configurations
Jackson’s JsonMapper offers different configurations for JSON serialization and deserialization. Whether we’re tweaking output formatting, changing naming strategies, or handling unknown properties, JsonMapper provides built-in methods to adapt to our application’s needs.
Note: JsonMapper is a concrete subclass of ObjectMapper – the class most Jackson 2 users will recognize. In Jackson 3, configuration moved to the builder pattern, and JsonMapper is the recommended concrete type for JSON processing. In previous lessons, we used ObjectMapper directly for basic parsing. That still works, but since ObjectMapper doesn’t expose the builder API in Jackson 3, we switch to JsonMapper now that we need to configure the mapper.
It’s also important to note that JsonMapper instances are immutable once built, making them inherently thread-safe. We can safely reuse a single instance across multiple threads, which is especially relevant in container-based or web applications (e.g., Spring Boot), where creating and discarding mappers on every request would be inefficient.
3. Default Behavior
Before diving into the configuration mechanisms, let’s open our JacksonUnitTest class and explore how JsonMapper behaves by default when serializing an object:
@Test
void givenDefaultObjectMapperInstance_whenSerializingAnObject_thenReturnJson() {
JsonMapper mapper = JsonMapper.builder().build();
Campaign campaign = new Campaign("A1", "JJ", "");
String result = mapper.writeValueAsString(campaign);
System.out.println(result);
}
Running the test produces the following output:
{"code":"A1","name":"JJ","description":"","closed":false}
As we can see, the output isn’t formatted in any way, and the fields of the string reflect the object attributes that we serialized.
4. Feature Toggling
Feature toggling is the primary way to configure a JsonMapper. In Jackson 3, these methods are called on the builder during construction. The three main methods are:
- enable(Feature) – activates a specific feature
- disable(Feature) – deactivates a specific feature
- configure(Feature, boolean) – enables or disables a feature dynamically
These methods give fine-grained control over JSON processing, letting us override defaults globally without relying on annotations.
4.1. enable()
The enable() method activates a specific feature. In this example, we enable pretty-printing so the output JSON is formatted with line breaks and indentation:
@Test
void givenEnableFeatureToggle_thenBehaviorIsAdjusted() {
JsonMapper customMapper = JsonMapper.builder().enable(SerializationFeature.INDENT_OUTPUT).build();
String result = customMapper.writeValueAsString(new Campaign("A1", "JJ", ""));
System.out.println(result);
assertTrue(result.contains("\n"));
}
Here we enable SerializationFeature.INDENT_OUTPUT, which makes the JSON more human-readable without changing the data:
{
"code" : "A1",
"name" : "JJ",
"description" : "",
"closed" : false
}
4.2. disable()
The disable() method deactivates a feature. In Jackson 3, some behaviors that were strict by default in Jackson 2 are now lenient by default — meaning they are effectively pre-disabled. This includes FAIL_ON_EMPTY_BEANS, which now allows empty classes to serialize to {} without an exception.
We can re-enable this strict behavior with enable() when needed:
@Test
void givenDisableFeatureToggle_thenBehaviorIsAdjusted() {
class EmptyClass {}
JsonMapper defaultMapper = JsonMapper.builder().build();
String defaultOutput = defaultMapper.writeValueAsString(new EmptyClass());
assertEquals("{}", defaultOutput);
JsonMapper strictMapper = JsonMapper.builder().enable(SerializationFeature.FAIL_ON_EMPTY_BEANS).build();
assertThrows(InvalidDefinitionException.class, () -> strictMapper.writeValueAsString(new EmptyClass()));
}
The default builder produces an empty JSON object for the empty class, while the strict builder throws an InvalidDefinitionException. The enable() and disable() methods work symmetrically: use enable() to activate a feature and disable() to deactivate it.
4.3. configure()
The configure() method is more flexible: it can both enable and disable a feature dynamically using a boolean flag. This is useful when the configuration may depend on runtime conditions.
In Jackson 3, unknown properties are ignored by default. The following example uses configure() on the builder to enable strict mode, where an unknown field causes an exception:
@Test
void givenConfigureFeatureToggle_thenBehaviorIsAdjusted() {
String json = """
{"code":"X1","name":"Extra","description":"-", "extraField":"ignored"}
""";
JsonMapper defaultMapper = JsonMapper.builder().build();
Campaign campaignResult = defaultMapper.readValue(json, Campaign.class);
// no exception: Jackson 3 ignores unknown properties by default
assertEquals("X1", campaignResult.getCode());
JsonMapper strictMapper = JsonMapper.builder()
.configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, true)
.build();
assertThrows(UnrecognizedPropertyException.class, () -> strictMapper.readValue(json, Campaign.class));
}
Here, the default mapper deserializes the JSON successfully despite the extra field extraField, while the strict mapper throws an UnrecognizedPropertyException. The configure() method accepts a boolean flag, making it convenient for toggling behavior conditionally.
4.4. Feature Types
Jackson groups its feature toggles into several enums, each responsible for a different aspect of JSON processing. Knowing these categories helps us understand what can be customized and where to look when we need to change behavior.
- SerializationFeature: Controls how Java objects are serialized to JSON.
Examples: – INDENT_OUTPUT – pretty-prints the JSON. – FAIL_ON_EMPTY_BEANS – throws an exception when serializing a class with no properties (disabled by default in Jackson 3).
- DeserializationFeature: Governs how JSON is parsed into Java objects.
Examples: – FAIL_ON_UNKNOWN_PROPERTIES – ignore or fail on extra JSON fields. – ACCEPT_SINGLE_VALUE_AS_ARRAY – allows a single value where an array is expected.
- DateTimeFeature (new in Jackson 3): Controls date and time serialization behavior, split out from SerializationFeature.
Examples: – WRITE_DATES_AS_TIMESTAMPS – serializes dates as numeric timestamps instead of ISO-8601 strings.
- MapperFeature: Affects core JsonMapper behavior, often related to introspection and visibility.
Examples: – AUTO_DETECT_FIELDS – controls whether non-accessor fields are automatically detected. – DEFAULT_VIEW_INCLUSION – defines default behavior for serialization views.
- JsonParser.Feature and JsonGenerator.Feature: Lower-level control of how JSON content is parsed and written (less commonly used in high-level code).
Examples: – ALLOW_COMMENTS – allows JSON with // or / / comments. – QUOTE_FIELD_NAMES – controls whether JSON keys are quoted.
These enums are the main reference points for exploring all available features. The Javadocs provide full lists, default values, and explanations.
5. Helper Configuration Methods
Beyond feature toggling, the JsonMapper also provides several helper methods for configuration needs that go beyond simple on/off switches. These methods handle scenarios such as controlling which properties are included, adjusting naming strategies, or managing visibility. They complement the feature toggles we covered earlier by offering more fine-grained customization.
For instance, Jackson includes all properties of a Java object during serialization by default, even if they’re null or have empty/default values.
We can adjust this globally with the changeDefaultPropertyInclusion() method on the builder. For example, excluding null properties:
@Test
void givenAnObjectWithNON_NULLFlagSet_whenSerializedTheObject_thenNullValuesExcluded() {
Campaign exampleCampaign = new Campaign("A1", "JJ", null);
JsonMapper defaultMapper = JsonMapper.builder().build();
String defaultOutput = defaultMapper.writeValueAsString(exampleCampaign);
assertTrue(defaultOutput.contains("null"));
JsonMapper customMapper = JsonMapper.builder()
.changeDefaultPropertyInclusion(v -> v.withValueInclusion(JsonInclude.Include.NON_NULL))
.build();
String output = customMapper.writeValueAsString(exampleCampaign);
System.out.println(output);
assertFalse(output.contains("null"));
}
Other useful inclusion rules are:
- Include.NON_EMPTY – excludes null, empty strings, and empty collections.
- Include.NON_DEFAULT – excludes properties with their default Java values.
Besides changeDefaultPropertyInclusion(), some other commonly used configuration helpers are:
- propertyNamingStrategy(…) – change how Java property names map to JSON keys (e.g., camelCase to snake_case).
- visibility(…) – control which fields, methods, or constructors Jackson can access, regardless of access modifiers.
- addMixIn(…) – attach annotations to third-party classes without modifying their source.
- defaultDateFormat(…) – define a global DateFormat for legacy java.util.Date values.
6. Modules and Custom Components
Besides toggling built-in features, Jackson allows us to extend or enhance its functionality by registering modules and plugging in custom components.
A module is a packaged set of configurations, serializers, deserializers, and other components that we can attach to a JsonMapper.
We can register modules in several ways:
- registerModule(Module module) – Register a single module.
- registerModules(Module… modules) – Register multiple modules at once.
- findAndRegisterModules() – Automatically discover and register all modules available on the classpath.
Jackson also allows advanced customization by replacing key components in the serialization/deserialization pipeline:
- setSerializerProvider() – Provide a custom serializer lookup and configuration strategy.
- setSerializerFactory() – Control how serializers are constructed.
- setDeserializationContext() – Customize deserialization behavior at a low level.
These capabilities are generally used for complex or highly specialized scenarios. In Jackson 3, support for the Java 8+ Date/Time API (LocalDate, LocalDateTime, and similar types) is built in, so no module registration is needed for common date/time handling. The findAndRegisterModules() method remains useful for auto-discovering third-party modules on the classpath.
At this stage, we won’t go into full detail on building or configuring custom modules, but it’s useful to know that Jackson’s architecture supports deep customization when needed.
7. Annotation-Based Configuration
Up to this point, we’ve focused on global JsonMapper configuration, using feature toggles and helper methods to define application-wide rules. Global settings are powerful, but sometimes too broad. In many cases, we need fine-grained control so that only specific classes or fields behave differently.
To address this, Jackson provides a complementary annotation-based mechanism. Annotations can be applied directly to model classes or fields, giving precise control over how those particular elements are serialized or deserialized.
Some of the most commonly used annotations include:
- @JsonInclude(JsonInclude.Include.NON_NULL): exclude null properties.
- @JsonNaming(PropertyNamingStrategies.SnakeCaseStrategy.class): apply a different naming strategy (e.g., snake_case).
- @JsonIgnoreProperties(ignoreUnknown = true): ignore extra JSON fields.
- Other fine-tuning annotations such as @JsonPropertyOrder, @JsonProperty, @JsonIgnore, and @JsonFormat.
Jackson follows a local-over-global principle: when both a global JsonMapper setting and an annotation are present, the annotation takes precedence — but only for the specific class or field where it’s applied.
Let’s open the CampaignWithAnnotations class and add some annotations:
@JsonIgnoreProperties(ignoreUnknown = true)
@JsonInclude(JsonInclude.Include.NON_NULL)
public class CampaignWithAnnotations {
// ...
@JsonInclude(JsonInclude.Include.ALWAYS)
private String description;
// ...
}
Now let’s define a test case to see how annotation precedence works when combined with global configuration:
@Test
void givenGlobalAlways_thenClassNonNull_andFieldAlways_shouldRespectAnnotationPrecedence() {
JsonMapper mapper = JsonMapper.builder()
.changeDefaultPropertyInclusion(v -> v.withValueInclusion(JsonInclude.Include.ALWAYS))
.build();
CampaignWithAnnotations campaign = new CampaignWithAnnotations(null, "JJ", null);
String json = mapper.writeValueAsString(campaign);
System.out.println(json);
assertTrue(json.contains("\"description\":null"));
assertFalse(json.contains("\"code\":"));
}
As we can see, the annotations take effect without further setup and override the global configuration where applied: the class-level NON_NULL rule excludes code, while the field-level ALWAYS rule ensures description is included, even when null.
In practice, most applications use a combination of both approaches: global settings to enforce consistent rules system-wide, and annotations for exceptions or class-specific adjustments. Jackson’s flexibility allows both mechanisms to coexist cleanly, with annotations always taking precedence where defined.
8. Conclusion
In this lesson, we explored the various ways to configure Jackson’s JsonMapper. We started by observing the default serialization behavior and then progressively introduced feature toggles, helper configuration methods, and module registration. We also examined how annotation-based configuration provides fine-grained, class-level control that follows a local-over-global precedence principle. Together, these mechanisms give us a flexible toolkit for tailoring JSON processing to our application’s specific needs.