What is Jackson, and Why Does Every Java Project Use It?
Jackson is a Java library for processing JSON โ converting Java objects to JSON strings (serialisation) and JSON strings back to Java objects (deserialisation). It is not part of the JDK, not part of the Java EE / Jakarta EE spec, and not written by Spring โ it is an independent open-source library that became the industry default because it is fast, flexible, and deeply integrated into every major Java framework.
Before Jackson, Java had no standard way to handle JSON. Projects used
string concatenation, home-grown parsers, or the Jakarta EE JSON-P/JSON-B
APIs (which are correct but verbose and less widely adopted). Jackson solved
the problem once and solved it well โ its ObjectMapper can
convert any Java object to JSON and back with zero configuration for the
common case, and precise control when needed.
/*
* Before Jackson โ manual JSON construction (real pre-2010 code):
*
* String json = "{\"name\":\"" + user.getName() + "\","
* + "\"email\":\"" + user.getEmail() + "\"}";
*
* Problem: escaping, null handling, nested objects, arrays โ all manual.
* No type safety, no refactoring support, wrong JSON on one typo.
*
* With Jackson:
*/
ObjectMapper mapper = new ObjectMapper();
String json = mapper.writeValueAsString(user); // โ {"name":"Ana","email":"ana@example.com"}
User parsed = mapper.readValue(json, User.class); // back to User
spring-boot-starter-web pulls in
jackson-databind as a transitive dependency. Spring Boot
auto-configures an ObjectMapper bean and registers it as
the HttpMessageConverter for application/json
โ every @RestController method that returns an object uses
Jackson under the hood. You've been using it since page one of this
Bible; this page explains how it actually works.
| Module | What it does |
|---|---|
jackson-core |
Low-level streaming API (JsonParser,
JsonGenerator) โ you rarely use this directly |
jackson-databind |
ObjectMapper and data binding โ this is "Jackson"
for 99% of use cases |
jackson-annotations |
All the @Json* annotations โ
pulled in transitively by databind |
ObjectMapper โ The Core, and Why You Don't new It Per Request
// ObjectMapper is THREAD-SAFE after configuration โ create once, reuse everywhere
@Bean
public ObjectMapper objectMapper() {
return new ObjectMapper()
.registerModule(new JavaTimeModule()) // LocalDate, LocalDateTime, etc.
.disable(SerializationFeature.WRITE_DATES_AS_TIMESTAMPS) // ISO-8601 strings, not epoch longs
.disable(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES) // ignore extra JSON fields
.enable(MapperFeature.ACCEPT_CASE_INSENSITIVE_PROPERTIES); // "Name" binds to "name"
}
// Basic operations
ObjectMapper mapper = new ObjectMapper();
// Serialise โ object to JSON string
String json = mapper.writeValueAsString(user);
String prettyJson = mapper.writerWithDefaultPrettyPrinter().writeValueAsString(user);
// Deserialise โ JSON string to object
User user = mapper.readValue(json, User.class);
// Generic types โ List, Map, Page<T> require TypeReference
List<User> users = mapper.readValue(json, new TypeReference<List<User>>() {});
// Convert between types (e.g. Map โ POJO, useful for dynamic structures)
Map<String, Object> map = mapper.convertValue(user, new TypeReference<>() {});
User fromMap = mapper.convertValue(map, User.class);
ObjectMapper construction is expensive โ it scans the
classpath, builds internal caches for every type it encounters, and
registers modules. Creating a new instance per HTTP request in a high-
traffic API measurably degrades throughput. The instance itself is
fully thread-safe after configuration; share one instance (or use the
Spring-managed bean) across the entire application.
Core Annotations
public class UserDto {
@JsonProperty("full_name") // serialises as "full_name" in JSON, even though the field is "name"
private String name;
@JsonIgnore // excluded from both serialisation AND deserialisation
private String passwordHash;
@JsonAlias({"mail", "emailAddress"}) // accepts any of these names when deserialising; serialises as "email"
private String email;
@JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss", timezone = "UTC")
private LocalDateTime createdAt;
@JsonInclude(JsonInclude.Include.NON_NULL) // field omitted from JSON when value is null
private String middleName;
@JsonAnyGetter // serialises a Map's entries as top-level JSON properties
private Map<String, Object> extra = new HashMap<>();
}
// Class-level annotations
@JsonIgnoreProperties(ignoreUnknown = true) // class-level equivalent of FAIL_ON_UNKNOWN_PROPERTIES=false
@JsonNaming(PropertyNamingStrategies.SnakeCaseStrategy.class) // camelCase โ snake_case globally for this class
public class ExternalApiResponse {
private String firstName; // serialised as "first_name" automatically
private String lastName; // serialised as "last_name" automatically
}
Setting @JsonInclude(NON_NULL) on a single class controls
only that class. Applying it globally via
mapper.setDefaultPropertyInclusion(JsonInclude.Include.NON_NULL)
affects every type the mapper serialises. Spring Boot exposes this as
spring.jackson.default-property-inclusion=non_null in
application.properties โ cleaner than subclassing
ObjectMapper.
Records and Jackson โ Java 17+ Idiom
Java records are immutable by construction โ no setters, all fields set via
the canonical constructor. Jackson supports records natively from
jackson-databind 2.12+ (which corresponds to Spring Boot 2.4+
โ all Boot 3.x versions qualify).
// Records deserialise via constructor โ no default constructor needed, no setters needed
public record UserResponse(
Long id,
String name,
@JsonProperty("email_address") String email, // annotations on record components work
@JsonFormat(pattern = "yyyy-MM-dd") LocalDate dob
) {}
Jackson maps JSON keys to constructor parameters by name. Constructor
parameter names are only available in bytecode if the
-parameters compiler flag was used (same caveat as
@PathVariable in Spring MVC โ see
Spring MVC). Without it, Jackson
needs @JsonCreator + @JsonProperty on every
constructor parameter to know the mapping. Spring Boot's auto-
configured ObjectMapper registers
ParameterNamesModule automatically and enables
-parameters via its Maven/Gradle plugin โ so records
"just work" in a standard Boot project. Outside Boot, register it
explicitly:
mapper.registerModule(new ParameterNamesModule());
Date and Time โ The Most Common Production Gotcha
// Without JavaTimeModule: LocalDateTime throws InvalidDefinitionException at runtime
// With JavaTimeModule + WRITE_DATES_AS_TIMESTAMPS disabled: ISO-8601 strings
// Maven dependency (not pulled in by spring-boot-starter-web automatically in all versions)
<!-- pom.xml -->
<dependency>
<groupId>com.fasterxml.jackson.datatype</groupId>
<artifactId>jackson-datatype-jsr310</artifactId>
<!-- version managed by Spring Boot parent -->
</dependency>
// Register the module โ or set it in application.properties (Spring Boot)
ObjectMapper mapper = new ObjectMapper()
.registerModule(new JavaTimeModule())
.disable(SerializationFeature.WRITE_DATES_AS_TIMESTAMPS);
# application.properties โ the Spring Boot way: no Java config needed
spring.jackson.serialization.write-dates-as-timestamps=false
spring.jackson.time-zone=UTC
| Type | Without module | With module (ISO-8601) |
|---|---|---|
LocalDate | Exception | "2024-01-15" |
LocalDateTime | Exception | "2024-01-15T10:30:00" |
ZonedDateTime | Exception | "2024-01-15T10:30:00+01:00" |
Instant | Exception | "2024-01-15T09:30:00Z" |
Without disabling this feature, Jackson serialises
LocalDateTime.of(2024, 1, 15, 10, 30) as the array
[2024,1,15,10,30] or a numeric epoch โ not the
human-readable ISO-8601 string any frontend or external API expects.
This is one of the most common "why is the date showing as a number"
bugs in new Spring projects.
Spring Boot Auto-Configuration โ What Boot Does by Default
Spring Boot auto-configures a single ObjectMapper bean via
JacksonAutoConfiguration. Knowing what it sets by default
tells you what you don't need to configure manually โ and what to override
when the default is wrong for your use case.
| Feature | Boot default | Why |
|---|---|---|
FAIL_ON_UNKNOWN_PROPERTIES |
false | Additive API versioning โ clients that send unknown fields don't break |
WRITE_DATES_AS_TIMESTAMPS |
true (pre-Boot 3.x) / configurable | Always set explicitly via properties โ don't rely on the default |
MapperFeature.DEFAULT_VIEW_INCLUSION |
false | Fields without @JsonView are excluded when a view is active |
ParameterNamesModule |
Registered | Records and constructor injection work without @JsonCreator |
JavaTimeModule |
Registered if on classpath | Auto-detects jackson-datatype-jsr310 |
# The cleanest way to customise Boot's ObjectMapper: application.properties
# Avoids subclassing or replacing the auto-configured bean entirely
spring.jackson.serialization.write-dates-as-timestamps=false
spring.jackson.deserialization.fail-on-unknown-properties=false
spring.jackson.default-property-inclusion=non_null
spring.jackson.time-zone=UTC
spring.jackson.property-naming-strategy=SNAKE_CASE # camelCase โ snake_case globally
If you declare a @Bean of type ObjectMapper,
Boot's auto-configuration backs off entirely โ you own the whole
configuration. A safer alternative is
Jackson2ObjectMapperBuilderCustomizer: it lets you add
modules, set features, or change naming strategy
on top of Boot's defaults rather than replacing them,
which means Boot's own modules (ParameterNames, JavaTime) stay
registered:
@Bean
public Jackson2ObjectMapperBuilderCustomizer jacksonCustomizer() {
return builder -> builder
.modules(new JavaTimeModule())
.featuresToDisable(SerializationFeature.WRITE_DATES_AS_TIMESTAMPS)
.propertyNamingStrategy(PropertyNamingStrategies.SNAKE_CASE);
}
Custom Serializers and Deserializers
Use custom serializers when Jackson's default output for a type is wrong for your API contract โ sensitive data masking, legacy format compatibility, or types Jackson can't handle automatically (e.g. third-party value objects).
// Custom serializer โ masks a credit card number to show only last 4 digits
public class MaskedCardSerializer extends JsonSerializer<String> {
@Override
public void serialize(String value, JsonGenerator gen, SerializerProvider provider)
throws IOException {
if (value != null && value.length() > 4) {
gen.writeString("****-****-****-" + value.substring(value.length() - 4));
} else {
gen.writeString(value);
}
}
}
// Attach to a specific field via annotation
public class PaymentDto {
@JsonSerialize(using = MaskedCardSerializer.class)
private String cardNumber;
}
// Custom deserializer โ parses a non-standard date format from a legacy API
public class LegacyDateDeserializer extends JsonDeserializer<LocalDate> {
private static final DateTimeFormatter FMT =
DateTimeFormatter.ofPattern("dd/MM/yyyy");
@Override
public LocalDate deserialize(JsonParser p, DeserializationContext ctx)
throws IOException {
return LocalDate.parse(p.getText(), FMT);
}
}
public record LegacyOrderDto(
Long id,
@JsonDeserialize(using = LegacyDateDeserializer.class) LocalDate orderDate
) {}
// Register serializers globally on the mapper (instead of per-field annotations)
SimpleModule module = new SimpleModule();
module.addSerializer(String.class, new MaskedCardSerializer());
mapper.registerModule(module);
@JsonView โ Different Fields for Different Contexts
@JsonView lets you define named "views" and annotate fields with
the views in which they should appear. A public API endpoint and an admin
endpoint can return the same object but with different fields โ without
creating two separate DTOs.
// Define view marker interfaces (no methods needed โ they're just type tokens)
public class Views {
public interface Public {}
public interface Admin extends Public {} // Admin extends Public โ inherits Public fields
}
public class UserDto {
@JsonView(Views.Public.class)
private Long id;
@JsonView(Views.Public.class)
private String name;
@JsonView(Views.Admin.class) // only visible in Admin view
private String email;
@JsonView(Views.Admin.class) // only visible in Admin view
private LocalDateTime lastLogin;
}
// In a Spring MVC controller โ activate the view per endpoint
@GetMapping("/api/users/{id}")
@JsonView(Views.Public.class) // returns: {id, name}
public UserDto getPublic(@PathVariable Long id) { ... }
@GetMapping("/admin/users/{id}")
@JsonView(Views.Admin.class) // returns: {id, name, email, lastLogin}
public UserDto getAdmin(@PathVariable Long id) { ... }
@JsonView is the right tool when two endpoints return the
same underlying data with different visibility โ same service call,
same object, different serialisation. Use separate DTOs when the
data itself differs โ a list view that calls a
different query than the detail view, or an admin view that joins
additional tables. Overusing @JsonView on a complex
object with many views produces an unmaintainable annotation forest.
Jackson in Tests
// In @WebMvcTest โ ObjectMapper is auto-configured and injectable
@WebMvcTest(UserController.class)
class UserControllerTest {
@Autowired private MockMvc mockMvc;
@Autowired private ObjectMapper objectMapper; // same instance as the app uses
@Test
void createUser_serialisesCorrectly() throws Exception {
var request = new CreateUserRequest("Ana", "ana@example.com", "pass1234");
mockMvc.perform(post("/api/users")
.contentType(MediaType.APPLICATION_JSON)
.content(objectMapper.writeValueAsString(request))) // use the injected mapper
.andExpect(status().isCreated());
}
}
// Pure unit test โ no Spring context needed for serialisation logic
class UserDtoSerializationTest {
private final ObjectMapper mapper = new ObjectMapper()
.registerModule(new JavaTimeModule())
.disable(SerializationFeature.WRITE_DATES_AS_TIMESTAMPS);
@Test
void passwordHash_isNeverSerialised() throws Exception {
var user = new UserDto(1L, "Ana", "$2a$10$abc...");
String json = mapper.writeValueAsString(user);
assertThat(json).doesNotContain("passwordHash");
assertThat(json).doesNotContain("$2a$"); // assert the actual hash value isn't there either
}
@Test
void dates_serialiseAsIso8601() throws Exception {
var dto = new EventDto(LocalDateTime.of(2024, 1, 15, 10, 30));
String json = mapper.writeValueAsString(dto);
assertThat(json).contains("\"2024-01-15T10:30:00\""); // ISO-8601, not an array or epoch
}
}
A pure unit test of serialisation (no Spring context, no MockMvc) runs in milliseconds and catches the most common Jackson bugs: missing modules, wrong date format, fields that should be hidden appearing in output. These bugs are typically only caught by manual API testing or in production when a client complains about the response shape โ a trivial unit test catches them at build time.
Interview Questions
Q: What is Jackson and what is ObjectMapper?
Jackson is the de-facto Java library for JSON processing.
ObjectMapper is its main entry point โ it serialises Java
objects to JSON strings and deserialises JSON strings back to Java objects.
In Spring Boot it's auto-configured as a singleton bean and used
automatically by every @RestController.
Q: What does @JsonIgnore do?
It excludes a field from both serialisation (Java โ JSON) and
deserialisation (JSON โ Java). Common use: password hashes, internal
audit fields, or any data that must never leave the server boundary.
Use @JsonIgnoreProperties at class level to ignore specific
fields by name, or to ignore unknown fields from an incoming JSON body.
Q: Why does LocalDateTime sometimes serialise as an array or a number?
Without JavaTimeModule registered and
WRITE_DATES_AS_TIMESTAMPS disabled, Jackson falls back to
serialising date/time types as their internal representation โ an array
of fields or an epoch value. Registering JavaTimeModule and
disabling timestamps produces ISO-8601 strings instead.
Q: Why is creating a new ObjectMapper per request a performance problem, and what's the correct pattern?
ObjectMapper construction is expensive โ it scans the
classpath, registers modules, and builds internal type caches.
ObjectMapper is fully thread-safe after configuration, so the
correct pattern is one shared instance (a Spring bean, or a static final
field in a utility class). Creating a new instance per request measurably
degrades throughput under load.
Q: How does Spring Boot configure Jackson by default, and how do you customise it without replacing the auto-configured bean entirely?
Boot's JacksonAutoConfiguration creates the
ObjectMapper bean and registers modules including
ParameterNamesModule and JavaTimeModule (if on
classpath). Declaring your own @Bean ObjectMapper replaces
the auto-configuration entirely, losing those defaults. The safe
alternative is Jackson2ObjectMapperBuilderCustomizer โ it
adds to Boot's defaults instead of replacing them, and is the right choice
for enabling features, adding modules, or changing naming strategy.
Q: What does FAIL_ON_UNKNOWN_PROPERTIES=false enable, and when would you want it set to true?
With false (Boot's default), incoming JSON that contains
fields the Java class doesn't have is silently ignored โ a v1 client
sending a v2 payload doesn't fail. This is what makes additive API
versioning safe. You'd want true in a strict contract
scenario โ for example, when deserialising a configuration file where
an unknown key almost certainly means a typo, and silently ignoring it
would hide the error.
Q: What is @JsonView and when is it preferable to separate DTOs?
@JsonView controls field visibility by context โ the same
object can serialise differently for a public endpoint vs an admin
endpoint without creating two classes. It's the right tool when the
underlying data is the same and only the visibility differs. Separate
DTOs are correct when the queries, the data shape, or the service calls
differ โ not just the fields included in the response.