Jackson ObjectMapper

About

ObjectMapper is a core class provided by the Jackson library, used for converting between Java objects and JSON. It allows us to:

  • Serialize Java objects to JSON

  • Deserialize JSON to Java objects

  • Work with Maps, Lists, and generic types

  • Configure inclusion/exclusion rules

  • Register modules for special data types like Java 8 date/time, Java records, etc.

Although Jackson is not a Spring-native library, Spring Boot auto-configures Jackson and provides seamless integration out-of-the-box, making ObjectMapper one of the most commonly used utilities in Spring-based applications.

Dependency

If we are using Spring Boot with web or REST modules, Jackson is included automatically via the spring-boot-starter-web dependency:

<dependency>
  <groupId>org.springframework.boot</groupId>
  <artifactId>spring-boot-starter-web</artifactId>
</dependency>

This transitively includes:

<dependency>
  <groupId>com.fasterxml.jackson.core</groupId>
  <artifactId>jackson-databind</artifactId>
</dependency>

If we are not using Spring Boot or want to include Jackson manually, add:

<dependency>
  <groupId>com.fasterxml.jackson.core</groupId>
  <artifactId>jackson-databind</artifactId>
  <version>2.17.0</version> <!-- Use latest stable -->
</dependency>

Spring Integration

How Spring Boot Uses Jackson Internally ?

Spring Boot uses Jackson as the default JSON processor. When we build a REST API or web application with spring-boot-starter-web, it:

  • Automatically includes the Jackson library (jackson-databind)

  • Registers a singleton ObjectMapper bean in the Spring context

  • Uses that bean for:

    • Serializing controller response objects (@ResponseBody)

    • Deserializing request bodies into Java objects (@RequestBody)

    • JSON conversion in RestTemplate, WebClient, and Feign clients

We can inject and use this same ObjectMapper throughout our application:

@Autowired
private ObjectMapper objectMapper;

Default Behavior in Spring Boot

When using Spring Boot, the default ObjectMapper is configured with sensible defaults.

Example Defaults

Feature
Default Behavior

Include null fields in JSON

Yes

Pretty-print JSON

No

Fail on unknown JSON properties

No (FAIL_ON_UNKNOWN_PROPERTIES is disabled)

Support for Java 8 Date/Time types

Yes (via jackson-datatype-jsr310)

Use of ISO-8601 for date serialization

Yes

Support for Java 8 Optional types

Yes

Snake case support

No (uses camelCase by default)

We can override these behaviors either:

  1. Through application properties

  2. By providing a custom ObjectMapper bean

Customizing ObjectMapper via application.yml

spring:
  jackson:
    # Include strategy for properties
    # Options: always, non_null, non_absent, non_default, non_empty
    default-property-inclusion: non_null  # Exclude null fields from JSON output

    # Serialization settings
    serialization:
      indent_output: true  # Pretty-print the JSON output (adds newlines and spaces)

    # Deserialization settings
    deserialization:
      fail-on-unknown-properties: false  # Do not fail if JSON has unknown fields

    # Date and time formatting
    date-format: yyyy-MM-dd'T'HH:mm:ss  # Custom global date-time format
    time-zone: Asia/Kolkata             # Time zone to use for date/time serialization

    # Naming strategy for property names
    # Options: SNAKE_CASE, LOWER_CAMEL_CASE (default), UPPER_CAMEL_CASE, KEBAB_CASE, etc.
    property-naming-strategy: SNAKE_CASE  # Converts Java fields like "firstName" to "first_name"

    # Enable/disable features (serialization/deserialization-level)
    mapper:
      # Allow use of getter methods as creators (if no constructor present)
      allow-getters-as-setters: true

    # Visibility control (used less frequently)
    visibility:
      # Property-level visibility configuration
      field: any                # Detect all fields regardless of access modifier
      getter: none              # Ignore getters
      is-getter: none           # Ignore "isX()" methods for booleans
      setter: any               # Detect all setters
      creator: any              # Detect constructors and factory methods

Internally, Spring Boot does this

Spring Boot binds these properties into a Jackson2ObjectMapperBuilder and applies them when initializing the ObjectMapper bean.

This is equivalent to doing this programmatically:

ObjectMapper mapper = new ObjectMapper();
mapper.setSerializationInclusion(JsonInclude.Include.NON_NULL);
mapper.enable(SerializationFeature.INDENT_OUTPUT);
mapper.setDateFormat(new SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss"));

Overriding the Default ObjectMapper

If we need full control, define our own @Bean of type ObjectMapper. Spring Boot will use our version instead of the default.

@Configuration
public class JacksonConfig {
    
    @Bean
    public ObjectMapper customObjectMapper() {
        ObjectMapper mapper = new ObjectMapper();
        mapper.setSerializationInclusion(JsonInclude.Include.NON_NULL);
        mapper.enable(SerializationFeature.INDENT_OUTPUT);
        mapper.registerModule(new JavaTimeModule());
        mapper.setPropertyNamingStrategy(PropertyNamingStrategies.SNAKE_CASE);
        return mapper;
    }
}

Note: If we do this, make sure to register required modules like JavaTimeModule, otherwise date/time parsing may break.

Use Cases

1. Serialization: Java Object to JSON

Convert a Java object into a JSON string.

Person person = new Person("John", 30);
String json = objectMapper.writeValueAsString(person);

Write to file or output stream:

objectMapper.writeValue(new File("person.json"), person);

Pretty-printing:

objectMapper.enable(SerializationFeature.INDENT_OUTPUT);
String prettyJson = objectMapper.writeValueAsString(person);

2. Deserialization: JSON to Java Object

Convert a JSON string to a Java object.

String json = "{\"name\":\"John\",\"age\":30}";
Person person = objectMapper.readValue(json, Person.class);

From file:

Person person = objectMapper.readValue(new File("person.json"), Person.class);

3. Convert Between Java Objects (Object to Object Mapping)

Convert one type to another using intermediate JSON conversion. Useful for mapping DTOs to entities.

UserDTO dto = new UserDTO("john.doe", "Admin");
UserEntity entity = objectMapper.convertValue(dto, UserEntity.class);

4. Convert to and from Map

POJO Class

public class User {
    private String name;
    private int age;

    // Constructors, getters, setters
}

Convert POJO to Map

User user = new User("Alice", 25);
Map<String, Object> map = objectMapper.convertValue(user, new TypeReference<>() {});
System.out.println(map);
{name=Alice, age=25}

Convert Map to POJO

<String, Object> map = new HashMap<>();
map.put("name", "Bob");
map.put("age", 30);

User user = objectMapper.convertValue(map, User.class);
System.out.println(user.getName());  // Bob
System.out.println(user.getAge());   // 30

5. Handling Collections and Generics

Deserialize a JSON array into a list of objects:

String json = "[{\"name\":\"John\",\"age\":30},{\"name\":\"Jane\",\"age\":25}]";

List<Person> people = objectMapper.readValue(
    json, new TypeReference<List<Person>>() {}
);

Deserialize a nested structure:

String json = "{\"group\":\"team1\",\"members\":[{\"name\":\"John\"},{\"name\":\"Jane\"}]}";

Group group = objectMapper.readValue(json, Group.class);

6. Deserialization with Unknown or Partial JSON

POJO Class

public class Product {
    private String name;
    private double price;

    // Constructors, getters, setters
}

JSON with Extra Field (unknown)

String json = """
{
    "name": "Laptop",
    "price": 999.99,
    "warranty": "2 years"  // Not present in Product class
}
""";

Default Behavior (in Spring Boot)

By default, Spring Boot configures ObjectMapper to ignore unknown fields, so this will work fine.

Product product = objectMapper.readValue(json, Product.class);
System.out.println(product.getName());   // Laptop
System.out.println(product.getPrice());  // 999.99

Manually Ensuring it Ignores Unknown Fields

If we are not in Spring Boot, or just want to be sure:

objectMapper.configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false);

Partial JSON

String partialJson = """
{
    "name": "Phone"
}
""";

If price is missing, it will be set to its default value (e.g., 0.0 for double).

Product product = objectMapper.readValue(partialJson, Product.class);
System.out.println(product.getName());   // Phone
System.out.println(product.getPrice());  // 0.0

Serialization Inclusion Strategies

Serialization inclusion strategies determine what data gets included in the JSON output. This is important to reduce unnecessary data in API responses, especially when dealing with large objects or REST services.

Options

Strategy
Description

Include.ALWAYS

Always include the property (default)

Include.NON_NULL

Exclude null values

Include.NON_EMPTY

Exclude empty values (empty string, empty list, etc.)

Include.NON_DEFAULT

Exclude fields with default values

Include.NON_ABSENT

Exclude nulls and Java 8 Optional.empty()

Global Configuration

objectMapper.setSerializationInclusion(JsonInclude.Include.NON_NULL);

Field-Level Configuration

public class User {
    @JsonInclude(JsonInclude.Include.NON_EMPTY)
    private List<String> tags;
}

Spring Boot Configuration (application.yml)

spring:
  jackson:
    default-property-inclusion: non_null

Use Case Example

You may not want to return null or empty fields to reduce response payload size:

{
  "name": "John",
  "email": null
}

With NON_NULL, email will be excluded.

Date and Time Handling

Working with dates and times in Java can be tricky, especially when serializing and deserializing JSON in REST APIs. Jackson provides powerful capabilities for formatting and parsing date/time types, and Spring Boot makes this integration seamless—but only if you configure it correctly.

Why Special Handling Is Needed

Java 8 introduced a new Date-Time API under java.time.* packages like:

  • LocalDate

  • LocalDateTime

  • ZonedDateTime

  • Instant

These types are not supported natively by Jackson without additional configuration. Jackson treats them differently than older types like java.util.Date, and without proper setup, they are either:

  • Serialized as arrays (e.g. [2024, 6, 20])

  • Serialized as timestamps

  • Or cause deserialization errors

Solution

To handle Java 8 time types correctly:

  1. Register JavaTimeModule

  2. Disable writing as timestamps

ObjectMapper mapper = new ObjectMapper();
mapper.registerModule(new JavaTimeModule());
mapper.disable(SerializationFeature.WRITE_DATES_AS_TIMESTAMPS);

This enables human-readable ISO-8601 strings like:

"2024-06-20T15:45:00"

instead of:

[2024, 6, 20, 15, 45, 0]

Field-Level Formatting

Use @JsonFormat to define a custom date format for individual fields:

public class Event {
    @JsonFormat(pattern = "yyyy-MM-dd")
    private LocalDate date;

    @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss")
    private LocalDateTime dateTime;
}

This ensures consistent formatting even if the global ObjectMapper is shared across the application.

Global Formatting in Spring Boot

Spring Boot allows configuring date/time formats centrally using application.yml or application.properties.

YAML Configuration

spring:
  jackson:
    date-format: yyyy-MM-dd'T'HH:mm:ss
    time-zone: Asia/Kolkata

Effect

This:

  • Sets the format for all java.util.Date and Calendar

  • Does not apply to Java 8 java.time.* types unless you register JavaTimeModule

To apply it for both, combine Spring config + JavaTimeModule registration.

Timezone Handling

When serializing or deserializing time-zone-aware types like ZonedDateTime or OffsetDateTime, Jackson can:

  • Use default system time zone

  • Apply a specific time zone via config

  • Respect explicit time zone in the string

Set Global Time Zone

mapper.setTimeZone(TimeZone.getTimeZone("Asia/Kolkata"));

Or via Spring config:

spring:
  jackson:
    time-zone: Asia/Kolkata

Deserialization Considerations

When parsing strings, Jackson expects them to match the format defined by:

  • @JsonFormat (if present)

  • application.yml format (if configured)

  • ISO-8601 (default fallback for JavaTimeModule)

Mismatched formats will cause JsonParseException.

Field Naming Strategy

Field naming strategies control how Java field names are mapped to JSON keys.

Default: camelCase

firstName → "firstName"

snake_case:

mapper.setPropertyNamingStrategy(PropertyNamingStrategies.SNAKE_CASE);
firstName → "first_name"

Spring Boot Configuration

spring:
  jackson:
    property-naming-strategy: SNAKE_CASE

Use Case

Aligns backend field names with frontend naming conventions, especially in REST APIs.

Working with JSON Tree (JsonNode)

Use JsonNode when working with dynamic, partially known, or schema-less JSON structures.

Parsing JSON as Tree

String json = "{\"name\":\"Alice\", \"age\":30}";
JsonNode root = objectMapper.readTree(json);
String name = root.get("name").asText();     // "Alice"
int age = root.get("age").asInt();           // 30

Modifying Tree

((ObjectNode) root).put("name", "Bob");

Traversing Nested Nodes

JsonNode address = root.path("address");
String city = address.path("city").asText();

Creating a Tree Manually

ObjectNode node = objectMapper.createObjectNode();
node.put("username", "john");
node.put("active", true);

Use Case

Perfect for generic filters, form submissions, or when schema is not static.

Annotations for Fine-Grained Control

Jackson provides annotations to control serialization/deserialization behavior per class or field.

Common Annotations

Annotation
Purpose

@JsonProperty

Rename JSON property

@JsonIgnore

Exclude field

@JsonInclude

Conditional inclusion

@JsonFormat

Format dates

@JsonAlias

Accept multiple names

@JsonCreator

Constructor/factory-based deserialization

@JsonValue

Serialize enum or custom object as a value

Examples

public class Product {
    @JsonProperty("item_name")
    private String name;

    @JsonIgnore
    private String internalCode;

    @JsonFormat(pattern = "yyyy-MM-dd")
    private LocalDate releaseDate;

    @JsonAlias({"cost", "price_value"})
    private double price;
}

Use Case

  • Maintain compatibility with legacy JSON.

  • Adjust naming or formatting without changing the class model.

  • Control visibility per API contract.

Working with JSON Tree (JsonNode)

Use JsonNode when working with dynamic, partially known, or schema-less JSON structures.

Parsing JSON as Tree

String json = "{\"name\":\"Alice\", \"age\":30}";
JsonNode root = objectMapper.readTree(json);
String name = root.get("name").asText();     // "Alice"
int age = root.get("age").asInt();           // 30

Modifying Tree

((ObjectNode) root).put("name", "Bob");

Traversing Nested Nodes

JsonNode address = root.path("address");
String city = address.path("city").asText();

Creating a Tree Manually

ObjectNode node = objectMapper.createObjectNode();
node.put("username", "john");
node.put("active", true);

Use Case

Perfect for generic filters, form submissions, or when schema is not static.

Exception Handling

Jackson can throw several checked and runtime exceptions during JSON processing. It’s essential to handle these gracefully, especially in REST APIs.

Common Exceptions

Exception
Cause

UnrecognizedPropertyException

JSON has fields not found in the POJO

MismatchedInputException

Type mismatch (e.g., expecting object but got array)

JsonMappingException

Problems mapping between JSON and object

JsonParseException

Malformed JSON syntax

JsonProcessingException

Parent class for all exceptions

Spring REST Exception Handler

@ControllerAdvice
public class GlobalExceptionHandler {

    @ExceptionHandler(JsonProcessingException.class)
    public ResponseEntity<String> handleJsonException(JsonProcessingException e) {
        return ResponseEntity.badRequest().body("Invalid JSON: " + e.getOriginalMessage());
    }
}

Custom Serializers and Deserializers

In most applications, Jackson’s default serialization and deserialization behavior is sufficient. However, certain situations call for custom logic, such as:

  • Encrypting/masking sensitive data

  • Transforming legacy formats

  • Applying business rules during serialization or deserialization

  • Interacting with non-standard or inconsistent APIs

Jackson makes this easy with JsonSerializer<T> and JsonDeserializer<T> interfaces.

1. Custom Serializer

Goal: Control how a Java object or field is written to JSON.

Steps:

a) Create a serializer class:

public class MaskingSerializer extends JsonSerializer<String> {
    @Override
    public void serialize(String value, JsonGenerator gen, SerializerProvider serializers)
            throws IOException {
        gen.writeString("***"); // Replace actual value with ***
    }
}

b) Use it in our model:

public class User {
    private String name;

    @JsonSerialize(using = MaskingSerializer.class)
    private String ssn; // Sensitive data
}

Output:

{
  "name": "John",
  "ssn": "***"
}

2. Custom Deserializer

Goal: Control how JSON is parsed into a Java object or field.

Steps:

a) Create a deserializer class:

public class UppercaseDeserializer extends JsonDeserializer<String> {
    @Override
    public String deserialize(JsonParser p, DeserializationContext ctxt)
            throws IOException {
        return p.getText().toUpperCase();
    }
}

b) Use it in our model:

public class Product {
    private String code;

    @JsonDeserialize(using = UppercaseDeserializer.class)
    private String category;
}

Input:

{
  "code": "PRD-101",
  "category": "electronics"
}

Output:

product.getCategory(); // "ELECTRONICS"

3. Apply to Entire Class

We can serialize/deserialize an entire class with custom logic if we need control beyond a field level.

Example: Serialize full object to flat string

public class Coordinates {
    public double lat;
    public double lng;
}

Custom Serializer:

public class CoordinatesSerializer extends JsonSerializer<Coordinates> {
    @Override
    public void serialize(Coordinates value, JsonGenerator gen, SerializerProvider serializers)
            throws IOException {
        gen.writeString(value.lat + "," + value.lng);
    }
}

Use:

@JsonSerialize(using = CoordinatesSerializer.class)
public class Coordinates { ... }

Output:

"12.9716,77.5946"

4. Global Registration

Instead of annotating each model field or class, we can register custom (de)serializers globally.

@Configuration
public class JacksonConfig {

    @Bean
    public ObjectMapper objectMapper() {
        ObjectMapper mapper = new ObjectMapper();

        SimpleModule module = new SimpleModule();
        module.addSerializer(String.class, new MaskingSerializer());
        module.addDeserializer(String.class, new UppercaseDeserializer());

        mapper.registerModule(module);
        return mapper;
    }
}

This affects every String field across the application. Be cautious with global registration.

5. Use with Spring Boot's Default ObjectMapper

If we don’t want to override Spring Boot’s default ObjectMapper but want to add custom behavior, use a Jackson2ObjectMapperBuilderCustomizer:

@Bean
public Jackson2ObjectMapperBuilderCustomizer customizer() {
    return builder -> {
        SimpleModule module = new SimpleModule();
        module.addSerializer(String.class, new MaskingSerializer());
        builder.modules(module);
    };
}

This allows customization without replacing the default Spring Boot configuration.

Last updated

Was this helpful?