Object Mapping is key in programming for converting objects and data between formats, such as JSON (JavaScript Object Notation). Serialization converts an object to a raw data format (sometimes a JSON string), while Deserialization converts raw data back into an object.
One of the most popular libraries used for object mapping in Java is Jackson. This powerful library simplifies both serialization and deserialization processes, making it an essential tool for Java developers working with JSON data.
Configuring ObjectMapper
@Bean
public ObjectMapper objectMapper() {
return new ObjectMapper();
}
Registering a new ObjectMapper bean without custom configuration as shown above uses default behaviours and may work, but its effectiveness depends on your use case. A mismatch between configuration and expectations can lead to errors, especially when handling specialized data types.
One common issue in Java is the handling of time-related fields. This challenge arises from the evolution of time handling across different Java versions, which can lead to discrepancies in how time data is serialized and deserialized. Fortunately, Jackson’s SerializationFeature and DeserializationFeature classes offer customizable configurations to manage these behaviors and ensure that time-related fields are handled properly according to your needs.
Below is a more detailed configuration
@Bean
public ObjectMapper objectMapper() {
ObjectMapper objectMapper = new ObjectMapper();
objectMapper.registerModule(new JavaTimeModule());
objectMapper.configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, true);
objectMapper.configure(DeserializationFeature.ACCEPT_EMPTY_ARRAY_AS_NULL_OBJECT, true);
objectMapper.configure(SerializationFeature.WRITE_DATES_AS_TIMESTAMPS, false);
objectMapper.setPropertyNamingStrategy(PropertyNamingStrategies.LOWER_CAMEL_CASE);
objectMapper.setSerializationInclusion(JsonInclude.Include.NON_NULL);
return objectMapper;
}
1. Deserialization in Jackson: Handling Unrecognized Properties
When deserializing JSON, you may encounter UnrecognizedPropertyException if the JSON contains properties not present in the target object. To handle this, Jackson’s ObjectMapper provides the DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES setting. By default, this is set to true, and setting it to false allows Jackson to ignore unrecognized properties and continue deserialization.
Note that FAIL_ON_UNKNOWN_PROPERTIES differs from FAIL_ON_IGNORED_PROPERTIES, which handles properties explicitly marked as @JsonIgnore and controls whether those properties are ignored during deserialization.
2. Configuring Jackson’s Property Naming Strategy in Java
Ensuring consistency between Java objects and the JSON format is key, and choosing the right naming strategy (e.g., KebabCase, CamelCase, or SnakeCase) is crucial for proper configuration. To configure the naming strategy in Jackson, you can set it on the ObjectMapper using the setPropertyNamingStrategy method. For example, if you want to use CamelCase (lower camel case) for the property names in the serialized JSON, you can configure it like this:objectMapper.setPropertyNamingStrategy(PropertyNamingStrategies.LOWER_CAMEL_CASE);
Choosing the appropriate naming strategy depends on the external systems you’re working with, as some APIs or databases may have specific requirements for how property names are formatted.
3. Serialization: Inclusion of Null and Empty Properties
By default, Jackson serializes all properties of an object, including those with null or empty values. However, in many cases, you may not want to include such properties in your serialized JSON. For example, when sending data to an external system, including null or empty values can result in unnecessary data transfer.objectMapper.setSerializationInclusion(JsonInclude.Include.NON_NULL);
4. Serialization: Formatting Date Fields
Jackson allows you to control how Date fields are serialized. By default, Jackson serializes Date fields as numeric timestamps (i.e., milliseconds since the epoch). However, in some cases, you may prefer to serialize dates in a human-readable textual format (such as YYYY-MM-DD or MM/DD/YYYY).objectMapper.configure(SerializationFeature.WRITE_DATES_AS_TIMESTAMPS, false);
This ensures that Date fields are serialized as ISO-8601 formatted strings (e.g., 2024-12-28T10:00:00Z) rather than numeric timestamps.
5. Registering Modules in Jackson: Joda, JavaTime, and JDK Modules
In Jackson, module registration allows you to extend the default serialization and deserialization capabilities by adding support for custom data types. This is especially useful when working with libraries or frameworks that require special handling for certain data structures. Three common modules frequently used to enhance Jackson’s functionality are:
- JodaModule: This module adds support for Joda-Time types (such as
DateTime,LocalDate, etc.) in Jackson. If your project uses Joda-Time for date and time handling, you’ll need to register this module to properly serialize and deserialize Joda types. - Jdk8Module: The JDK modules provide support for Java 8 features like
java.util.Optional,java.time(viaJavaTimeModule), and others. These modules are especially important when integrating legacy systems that may use Java 8 features or have specific serialization needs. - JavaTimeModule: This module is essential for handling Java 8’s
java.timeAPI (e.g.,LocalDateTime,LocalDate,ZonedDateTime) in Jackson. If you’re working with time-related fields, you must register the JavaTimeModule. Failure to do so may lead to errors such as:Type definition error: [simple type, class java.time.LocalDateTime].
Here’s how you can register the JavaTimeModule in your ObjectMapper:objectMapper.registerModule(new JavaTimeModule());
This ensures that Jackson can correctly serialize and deserialize Java time types such as LocalDateTime and Instant.
6. Using Multiple Jackson ObjectMapper Beans for Different Configurations
When consuming data from multiple external systems with different strictness or naming strategies (e.g., CamelCase vs. SnakeCase), a single Jackson configuration may not suffice. In such cases, creating multiple ObjectMapper beans with tailored configurations ensures each system’s requirements are met without conflicts.
Here’s an example of how you could define multiple ObjectMapper beans with different configurations:
@Bean
public ObjectMapper camelCaseObjectMapper() {
ObjectMapper objectMapper = new ObjectMapper();
objectMapper.setPropertyNamingStrategy(PropertyNamingStrategies.LOWER_CAMEL_CASE);
// Additional configuration…
return objectMapper;
}
@Bean
public ObjectMapper snakeCaseObjectMapper() {
ObjectMapper objectMapper = new ObjectMapper();
objectMapper.setPropertyNamingStrategy(PropertyNamingStrategies.SNAKE_CASE);
// Additional configuration…
return objectMapper;
}
This approach ensures flexibility, allowing you to handle different naming strategies, serialization features, and other configurations required by each external system.
Reference(s)
– https://github.com/FasterXML
Leave a comment