1. Overview

Jackson’s ObjectMapper 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 an ObjectMapper 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: objectmapper-configuration-start.

If we want to reference the fully implemented lesson, we can import: objectmapper-configuration-end.

2. ObjectMapper Basic Configurations

Jackson’s ObjectMapper offers different configurations for JSON serialization and deserialization. Whether we’re tweaking output formatting, changing naming strategies, or handling unknown properties, ObjectMapper provides built-in methods to adapt to your application’s needs.

It’s also important to note that ObjectMapper is thread-safe after configuration, meaning we can reuse a single instance across multiple threads. This 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 ObjectMapper behaves by default when serializing an object:

@Test
void givenDefaultObjectMapperInstance_whenSerializingAnObject_thenReturnJson() throws JsonProcessingException {
    ObjectMapper mapper = new ObjectMapper();

    Campaign campaign = new Campaign("A1", "JJ", "");
    String result = mapper.writeValueAsString(campaign);

    System.out.println(result);
}

The output after running the test will be as we see below:

{"code":"A1","name":"JJ","description":"","closed":false}

As we can see, the output is not 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 the ObjectMapper. 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() throws JsonProcessingException {
    ObjectMapper customMapper = new ObjectMapper().enable(SerializationFeature.INDENT_OUTPUT);

    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. By default, Jackson throws an exception when trying to serialize a class with no properties. Let’s see how disabling FAIL_ON_EMPTY_BEANS changes that:

@Test
void givenDisableFeatureToggle_thenBehaviorIsAdjusted() throws JsonProcessingException {
    class EmptyClass {
    }

    ObjectMapper defaultMapper = new ObjectMapper();
    assertThrows(InvalidDefinitionException.class, () -> defaultMapper.writeValueAsString(new EmptyClass()));

    ObjectMapper customMapper = new ObjectMapper().disable(SerializationFeature.FAIL_ON_EMPTY_BEANS);
    String customOutput = customMapper.writeValueAsString(new EmptyClass());
    assertEquals("{}", customOutput);
}

Instead of failing, the custom mapper outputs an empty JSON object ({}).

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 the following example, we configure Jackson to ignore unknown properties when deserializing:

@Test
void givenConfigureFeatureToggle_thenBehaviorIsAdjusted() throws JsonProcessingException {
    String json = """
        {"code":"X1","name":"Extra","description":"-", "extraField":"ignored"}
        """;

    ObjectMapper defaultMapper = new ObjectMapper();
    assertThrows(UnrecognizedPropertyException.class, () -> defaultMapper.readValue(json, Campaign.class));

    ObjectMapper customMapper = new ObjectMapper().configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false);
    Campaign campaignResult = customMapper.readValue(json, Campaign.class);
    // no exception and the fields were set
    assertEquals("X1", campaignResult.getCode());
}

Here, the default mapper fails on the unknown property extraField, but the custom one ignores it and deserializes the rest correctly.

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.
    • WRITE_DATES_AS_TIMESTAMPS – disables this to serialize dates as ISO-8601 strings.
  • 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.
  • MapperFeature: Affects core ObjectMapper 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 ObjectMapper 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 are null or have empty/default values.

We can adjust this globally with the setSerializationInclusion() method. For example, excluding null properties:

@Test
void givenAnObjectWithNON_NULLFlagSet_whenSerializedTheObject_thenNullValuesExcluded() throws Exception {
    Campaign exampleCampaign = new Campaign("A1", "JJ", null);
    
    ObjectMapper defaultMapper = new ObjectMapper();
    String defaultOutput = defaultMapper.writeValueAsString(exampleCampaign);
    assertTrue(defaultOutput.contains("null"));
    
    ObjectMapper customMapper = new ObjectMapper().setSerializationInclusion(JsonInclude.Include.NON_NULL);
    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.

As setSerializationInclusion(), some other commonly used configuration helpers are:

  • setPropertyNamingStrategy(…) – change how Java property names map to JSON keys (e.g., camelCase to snake_case).
  • setVisibility(…) – control which fields, methods, or constructors Jackson can access, regardless of access modifiers.
  • setMixIns(…) – attach annotations to third-party classes without modifying their source.
  • setDateFormat(…) – 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 an ObjectMapper.

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. A common everyday example is registering the JavaTimeModule to correctly handle the Java 8+ Date/Time API, which we’ll explore in a dedicated lesson.

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 ObjectMapper 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 ObjectMapper 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() throws Exception {
    // Global: include everything (even nulls)
    ObjectMapper mapper = new ObjectMapper().setSerializationInclusion(JsonInclude.Include.ALWAYS);

    // code = null (class-level NON_NULL should drop it)
    // description = null (field-level ALWAYS should force include)
    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.