Customize Jackson ObjectMapper in Spring Boot: Serialization & Deserialization
Learn how to customize Jackson ObjectMapper in Spring Boot for advanced serialization and deserialization.
20+ years shipping production Java in banking & fintech. Written from production experience, not tutorials.
- ✓Java 17+ (Spring Boot 3.x) or Java 11+ (Spring Boot 2.x)
- ✓Basic Spring Boot project with spring-boot-starter-web dependency
- ✓Familiarity with REST APIs and JSON structure
โข Use Jackson2ObjectMapperBuilder or @Bean to customize ObjectMapper in Spring Boot 2.x/3.x
โข Annotate fields with @JsonProperty, @JsonFormat, and @JsonIgnore for fine-grained control
โข Register custom serializers/deserializers via SimpleModule for non-trivial transformations
โข Configure global settings like FAIL_ON_UNKNOWN_PROPERTIES in application.properties or YAML
โข Leverage mix-ins to modify third-party classes without changing their source
Think of Jackson ObjectMapper as a universal translator that converts your Java objects (like customer records) into JSON (a language APIs speak) and back. Customizing it is like teaching the translator slang, abbreviations, and special rules so it doesn't mangle your data when talking to other systems.
Jackson is the default JSON processing library in Spring Boot since version 1.2.0, and for good reason โ it's fast, flexible, and battle-tested. But the default ObjectMapper configuration is a one-size-fits-all solution that will bite you in production. I've seen it happen: a LocalDateTime serialized as an array of integers, a null field causing a 500 error, or a circular reference blowing up the stack. These aren't hypotheticals; they're scars from late-night war rooms.
In this guide, we'll go beyond the basics. You'll learn how to customize Jackson ObjectMapper in Spring Boot 3.x (and 2.x) for both serialization and deserialization. We'll cover global configuration via application.properties, programmatic customization with Jackson2ObjectMapperBuilder, custom serializers and deserializers for complex domains like payment-processing, and mix-ins for third-party classes. We'll also dissect a real production incident where a misconfigured mapper caused a billing outage. By the end, you'll have a battle-ready ObjectMapper that handles edge cases, legacy formats, and strict API contracts without breaking a sweat.
Understanding Spring Boot's Default ObjectMapper
When you add spring-boot-starter-web to your project, Spring Boot auto-configures an ObjectMapper bean with sensible defaults. But "sensible" is relative. In Spring Boot 2.x, the default mapper does NOT include JavaTimeModule for Java 8 date/time types โ that's a landmine. It also allows unknown properties during deserialization by default (FAIL_ON_UNKNOWN_PROPERTIES = false), which can silently swallow API contract violations. In Spring Boot 3.x, the defaults improved slightly, but you still need to explicitly configure date formats and naming strategies for most real-world apps.
Let's inspect the default behavior. Create a simple controller that returns a DTO with a LocalDateTime field.
LocalDateTime serialized as an array crash a downstream fraud detection service because it expected a string. We added a global integration test that serializes every DTO and asserts key fields' JSON types. It caught 3 more similar issues before deployment.What the Official Docs Won't Tell You
The official Spring Boot documentation tells you about spring.jackson.* properties and Jackson2ObjectMapperBuilder, but it glosses over the real-world pitfalls. Here's what I've learned from 15+ years of production battles:
First, Jackson2ObjectMapperBuilder is your best friend for programmatic configuration. But don't just chain methods blindly โ order matters. For example, calling after modules()modulesToInstall() will overwrite the default modules. Use once with a list.modules()
Second, the @JsonFormat annotation on a field overrides global settings, but only if Jackson can find the annotation. If you use Lombok's @Builder or records, ensure the annotation is on the getter or constructor parameter, not just the field. I've spent hours debugging why a @JsonFormat(pattern = "yyyy-MM-dd") was ignored โ Lombok generated the getter without the annotation.
Third, never use ObjectMapper directly in a @PostConstruct of a configuration class. Spring Boot may not have fully initialized the auto-configured mapper yet. Instead, create a @Bean method that returns a configured ObjectMapper and inject it where needed.
Finally, the spring.jackson.property-naming-strategy property in application.properties is powerful but limited. For complex naming strategies (e.g., snake_case for some fields, camelCase for others), you need a custom PropertyNamingStrategy or mix-ins.
@JsonFormat on a LocalDate field in a Lombok DTO. The annotation was on the field, but Lombok's @Data generated getters without it. We switched to @Getter on the field and added @JsonFormat there. Lesson: always test serialization of Lombok DTOs explicitly.Jackson2ObjectMapperBuilder for programmatic customization, but be aware of method ordering and Lombok/record annotation propagation.Global Configuration via application.properties
The simplest way to customize Jackson is through application.properties (or YAML). Spring Boot exposes a set of spring.jackson.* properties that control the ObjectMapper globally. This is ideal for cross-cutting concerns like date format, null handling, and property naming strategy. However, these properties are limited to what Spring Boot exposes โ for advanced customization, you'll need programmatic configuration.
Here's a battle-tested configuration for a REST API that communicates with external services:
spring.jackson.default-property-inclusion=non_null globally. This reduced JSON payload sizes by 30% on average, cutting network latency and database storage costs. However, we had to add @JsonInclude(Include.ALWAYS) on a few fields that clients explicitly required as null.application.properties for simple global settings like date format and null handling, but be aware of thread-safety issues with SimpleDateFormat.Custom Serializers for Complex Types
When global configuration isn't enough, you need custom serializers. This is common in payment-processing systems where you need to mask sensitive data like credit card numbers or format monetary amounts with precision. Writing a custom serializer gives you full control over the JSON output.
Let's create a serializer for a MonetaryAmount class that outputs a formatted string with currency code.
@JsonView annotation to expose the full PAN only in internal admin endpoints. This satisfied PCI DSS compliance without duplicating DTOs.Custom Deserializers for Legacy Formats
Deserialization is where most production incidents happen. You receive JSON from an external API that uses a legacy format โ dates as timestamps, enums as integers, or nested objects as flat strings. A custom deserializer can normalize this data into your domain model without changing the external contract.
Consider a legacy CRM that sends dates as Unix timestamps in milliseconds. We need to deserialize them into LocalDateTime.
Mix-Ins: Modifying Third-Party Classes
Mix-ins are Jackson's secret weapon for modifying serialization/deserialization of classes you don't control โ think library classes, generated code, or legacy domain objects. Instead of subclassing or modifying the source, you create an abstract class with annotations and tell ObjectMapper to apply it to the target class.
Imagine you use a third-party library that has a Customer class with a getSsn() method. You want to exclude SSN from JSON output without touching the library's code.
@JsonSerialize(using = MonetaryAmountSerializer.class) to a third-party Money class from a payment library. This saved us from writing a wrapper class and ensured consistent formatting across all endpoints.Testing Your ObjectMapper Configuration
You can't trust what you don't test. In production, a misconfigured ObjectMapper can silently corrupt data or throw exceptions at the worst moment. I always add integration tests that verify serialization and deserialization of every DTO that crosses a service boundary. Use @JsonTest from Spring Boot to load only the Jackson infrastructure, or write plain unit tests with a configured ObjectMapper.
Here's a comprehensive test for our custom configuration:
JacksonTester or ObjectMapper directly in tests to catch configuration issues early.Performance Tuning for High-Throughput APIs
In high-throughput systems like real-time analytics or payment processing, ObjectMapper performance matters. Jackson is fast, but misconfiguration can introduce bottlenecks. Here are lessons from optimizing ObjectMapper for 10,000+ requests per second:
First, avoid @JsonFormat on fields that are serialized millions of times. Each invocation uses reflection to read the annotation. Instead, use a custom serializer or global configuration. Second, enable ObjectMapper's WRITE_SINGLE_ELEM_ARRAYS_UNWRAPPED sparingly โ it can confuse clients. Third, use @JsonInclude(Include.NON_EMPTY) instead of NON_NULL to also exclude empty collections, reducing payload size.
Finally, consider using AfterburnerModule for zero-reflection serialization. It generates bytecode at runtime that accesses fields directly, bypassing reflection. In Spring Boot 3.x, you can add it as a dependency and register it as a module.
AfterburnerModule reduced CPU usage by 15% and p99 latency by 20ms. We also moved all @JsonFormat annotations to a custom serializer for the date fields, cutting reflection calls by 80%.AfterburnerModule, prefer global configuration over annotations, and disable unnecessary features to reduce reflection overhead.The Midnight Billing Outage: A Jackson Date Serialization Disaster
LocalDateTime serialized as [2024, 3, 15, 14, 30, 0] instead of "2024-03-15T14:30:00".JavaTimeModule for LocalDateTime and other java.time types. The team had not added spring.jackson.serialization.write-dates-as-timestamps=false to application.properties.spring.jackson.serialization.write-dates-as-timestamps=false to application.properties and created a @Bean that registered JavaTimeModule with ObjectMapper. Also added integration tests that verified date serialization format.- Never assume default ObjectMapper settings are production-ready; always explicitly configure date/time serialization.
- Add integration tests for JSON serialization of all DTOs that cross service boundaries.
- Use
application.propertiesor a dedicatedJacksonConfigclass to enforce formatting standards across the team.
spring.jackson.serialization.write-dates-as-timestamps property. Ensure JavaTimeModule is registered. Verify @JsonFormat annotations on date fields.spring.jackson.deserialization.fail-on-unknown-properties setting. If true, add @JsonIgnoreProperties(ignoreUnknown = true) to the DTO or add the missing property.spring.jackson.default-property-inclusion setting. If set to non_null, null fields are excluded. Use @JsonInclude(Include.ALWAYS) on specific fields if needed.@JsonManagedReference and @JsonBackReference on bidirectional relationships. Alternatively, disable FAIL_ON_SELF_REFERENCES and use @JsonIdentityInfo.curl -X GET http://localhost:8080/invoice | jq '.createdAt'Check if JavaTimeModule is registered: grep -r "JavaTimeModule" src/main/java/@Bean returning Jackson2ObjectMapperBuilder with .modulesToInstall(new JavaTimeModule())| File | Command / Code | Purpose |
|---|---|---|
| DefaultMapperController.java | @RestController | Understanding Spring Boot's Default ObjectMapper |
| JacksonConfig.java | @Configuration | What the Official Docs Won't Tell You |
| application.properties | spring.jackson.serialization.write-dates-as-timestamps=false | Global Configuration via application.properties |
| MonetaryAmountSerializer.java | public class MonetaryAmountSerializer extends StdSerializer | Custom Serializers for Complex Types |
| UnixTimestampDeserializer.java | public class UnixTimestampDeserializer extends StdDeserializer | Custom Deserializers for Legacy Formats |
| CustomerMixin.java | public abstract class CustomerMixin { | Mix-Ins |
| JacksonConfigTest.java | @SpringBootTest | Testing Your ObjectMapper Configuration |
| PerformanceConfig.java | @Configuration | Performance Tuning for High-Throughput APIs |
Key takeaways
Jackson2ObjectMapperBuilder for programmatic configuration, custom serializers/deserializers for complex types, and mix-ins for third-party classes.JacksonTester or ObjectMapper directly to catch configuration issues before deployment.Interview Questions on This Topic
How would you configure Jackson to serialize LocalDateTime as ISO-8601 string in Spring Boot?
spring.jackson.serialization.write-dates-as-timestamps=false to application.properties. Also register JavaTimeModule by creating a @Bean that uses Jackson2ObjectMapperBuilder and calls .modulesToInstall(new JavaTimeModule()). For per-field control, use @JsonFormat(pattern = "yyyy-MM-dd'T'HH:mm:ss.SSS'Z'").Frequently Asked Questions
20+ years shipping production Java in banking & fintech. Written from production experience, not tutorials.
That's Spring Boot. Mark it forged?
3 min read · try the examples if you haven't