1. 簡介
在本教程中,我們將探索如何為 ObjectMapper 創建和配置自定義的 Jersey 應用程序,使用 Jackson。 ObjectMapper 負責將 Java 對象轉換為 JSON,反之亦然,通過自定義它,我們可以控制格式、日期處理、字段命名約定和序列化規則等方面。
2. 理解 ObjectMapper
ObjectMapper 是 Jackson 的核心功能,負責將 Java 對象轉換為 JSON 字符串,以及將 JSON 轉換回 Java 對象。 Jersey 自動集成 Jackson,因此我們可以輕鬆地從 REST 端點返回對象並獲得 JSON 響應。
雖然這種默認設置很方便,但它可能無法滿足所有實際應用的需求。 例如,Jackson 默認將日期寫入時間戳,這不太易於人類閲讀,並且它不進行 JSON 美化,這可能會使調試更困難。
3. JacksonJaxbJsonProvider 方式 (Jersey 2.x)
在 Jersey 2.x 中,自定義 Jackson 的常見方法是擴展 JacksonJaxbJsonProvider。該提供程序充當 Jersey 和 Jackson 之間的橋樑,允許我們注入自己的 ObjectMapper。
使用這種方法,我們可以全局配置 JSON 的序列化和反序列化方式,應用於應用程序的所有 REST 端點。
這是一個簡單的自定義提供程序的示例:
@Provider
public class CustomObjectMapperProvider extends JacksonJaxbJsonProvider {
public CustomObjectMapperProvider() {
ObjectMapper mapper = new ObjectMapper();
mapper.enable(SerializationFeature.INDENT_OUTPUT);
mapper.disable(SerializationFeature.WRITE_DATES_AS_TIMESTAMPS);
mapper.setSerializationInclusion(JsonInclude.Include.NON_NULL);
mapper.setPropertyNamingStrategy(PropertyNamingStrategies.SNAKE_CASE);
setMapper(mapper);
}
}
在這個提供程序中,ObjectMapper 被配置為產生更易讀的 JSON,通過啓用縮進。日期以 ISO-8601 字符串格式寫入,而不是時間戳,通過禁用 WRITE_DATES_AS_TIMESTAMPS 實現。
此外,帶有空值字段會自動被跳過,通過設置 setSerializationInclusion(JsonInclude.Include.NON_NULL) 實現。 我們還使用 PropertyNamingStrategies.SNAKE_CASE 將 Java 的 camelCase 屬性名稱轉換為 JSON 響應中的 snake_case 。
通過使用 @Provider 標註此類並擴展 JacksonJaxbJsonProvider ,Jersey 會自動檢測和註冊它。 一旦此提供程序到位,應用程序中的每個 REST 端點都會使用這些 JSON 規則,而無需在單個資源類中進行額外的配置。
這種方法對於 Jersey 2.x 應用程序來説非常有效,但它是全局配置。
4. ContextResolverObjectMapper Approach (Jersey 3.x)
With Jersey 3.x, the preferred way to customize Jackson is through implementing a ContextResolver<ObjectMapper>. This approach allows applications to provide multiple ObjectMapper configurations and select them conditionally based on the model class or annotation.
For example, we might want stricter rules for public API responses, while internal models include more debugging information. Using a ContextResolver<ObjectMapper> makes this conditional serialization straightforward, flexible, and fully compatible with Jersey 3 and modern Jakarta EE applications.
4.1. Setup Project
To get started with Jackson in Jersey 3.x, we need to include the Jackson Jakarta RS provider in our pom.xml. This library enables JSON serialization and deserialization for Jersey endpoints:
<dependency>
<groupId>com.fasterxml.jackson.jakarta.rs</groupId>
<artifactId>jackson-jakarta-rs-json-provider</artifactId>
<version>2.19.1</version>
</dependency>
With this dependency, Jersey can use Jackson to automatically convert Java objects to JSON and parse JSON back into Java objects.
4.2. Basic ContextResolver
When our app needs different ObjectMapper settings, we use a ContextResolver<ObjectMapper>to switch between the right setup for each case.
Let’s create a simple ContextResolver example:
@Provider
public class ObjectMapperContextResolver implements ContextResolver<ObjectMapper> {
private final ObjectMapper mapper;
public ObjectMapperContextResolver() {
mapper = JsonMapper.builder()
.findAndAddModules()
.build();
mapper.enable(SerializationFeature.INDENT_OUTPUT);
mapper.disable(SerializationFeature.WRITE_DATES_AS_TIMESTAMPS);
mapper.setSerializationInclusion(JsonInclude.Include.NON_NULL);
mapper.setPropertyNamingStrategy(PropertyNamingStrategies.SNAKE_CASE);
}
@Override
public ObjectMapper getContext(Class<?> type) {
return mapper;
}
}
This ContextResolver provides a pre-configured ObjectMapper with:
- Pretty-printed JSON (INDENT_OUTPUT)
- ISO-8601 date format (WRITE_DATES_AS_TIMESTAMPS disabled)
- Skipping null fields (Include.NON_NULL)
- Snake case property names (PropertyNamingStrategies.SNAKE_CASE)
We use findAndAddModules() to automatically scan the classpath for available modules and register them with the ObjectMapper. This ensures that optional features, like Java 8 date/time support from jackson-datatype-jsr310, are automatically enabled without manual registration, making the ObjectMapper fully aware of all modules in the project.
Using a ContextResolver is flexible because we can later provide different ObjectMapper instances depending on the class type. This makes it ideal for applications with multiple API models or varying serialization rules.
4.3. Conditional ObjectMapper
In real-world applications, we often need different serialization rules for different types of objects or API contexts. For example, we might want stricter rules for public API responses while internal models include more debugging information.
A ContextResolver<ObjectMapper> can be extended to support conditional ObjectMapper configurations based on class type or annotations.
Let’s create a more complex ContextResolver that provides different ObjectMapper configurations based on the class type:
@Provider
public class ConditionalObjectMapperResolver implements ContextResolver<ObjectMapper> {
private final ObjectMapper publicApiMapper;
private final ObjectMapper internalApiMapper;
private final ObjectMapper defaultMapper;
public ConditionalObjectMapperResolver() {
publicApiMapper = JsonMapper.builder()
.findAndAddModules()
.build();
publicApiMapper.enable(SerializationFeature.INDENT_OUTPUT);
mapper.disable(SerializationFeature.WRITE_DATES_AS_TIMESTAMPS);
mapper.setSerializationInclusion(JsonInclude.Include.NON_NULL);
mapper.setPropertyNamingStrategy(PropertyNamingStrategies.SNAKE_CASE);
publicApiMapper.disable(SerializationFeature.WRITE_EMPTY_JSON_ARRAYS);
internalApiMapper = JsonMapper.builder()
.findAndAddModules()
.build();
internalApiMapper.enable(SerializationFeature.INDENT_OUTPUT);
internalApiMapper.disable(SerializationFeature.WRITE_DATES_AS_TIMESTAMPS);
internalApiMapper.setSerializationInclusion(JsonInclude.Include.NON_EMPTY);
internalApiMapper.enable(SerializationFeature.WRITE_DATES_WITH_ZONE_ID);
defaultMapper = JsonMapper.builder()
.findAndAddModules()
.build();
defaultMapper.enable(SerializationFeature.INDENT_OUTPUT);
defaultMapper.disable(SerializationFeature.WRITE_DATES_AS_TIMESTAMPS);
}
@Override
public ObjectMapper getContext(Class<?> type) {
if (isPublicApiModel(type)) {
return publicApiMapper;
} else if (isInternalApiModel(type)) {
return internalApiMapper;
}
return defaultMapper;
}
private boolean isPublicApiModel(Class<?> type) {
return type.getPackage().getName().contains("public.api") ||
type.isAnnotationPresent(PublicApi.class);
}
private boolean isInternalApiModel(Class<?> type) {
return type.getPackage().getName().contains("internal.api") ||
type.isAnnotationPresent(InternalApi.class);
}
}
4.4. Marker Annotations
We can create simple annotations to mark which API type a model belongs to:
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.TYPE)
public @interface PublicApi {
}
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.TYPE)
public @interface InternalApi {
}
Now, when we create model classes, those annotated with @PublicApi will use stricter rules, for example, skipping sensitive fields and null values, and JSON should be formatted in snake_case:
@Test
void givenPublicApiMessage_whenSerialized_thenOmitsSensitiveFieldAndNulls() throws Exception {
PublicApiMessage message = new PublicApiMessage("Public Hello!", LocalDate.of(2025, 8, 23), null);
String json = publicApiMapper.writeValueAsString(message);
assertTrue(json.contains("text"));
assertTrue(json.contains("date"));
assertFalse(json.contains("sensitiveField"));
assertFalse(json.contains("null"));
}
This test confirms that the public API mapper enforces stricter rules for public-facing data.
4.5. Default ObjectMapper
Finally, the default ObjectMapper handles models without special annotations.
@Test
void givenDefaultMessage_whenSerialized_thenIncludesOptionalFieldAndMetadata() throws Exception {
Message message = new Message("Default Hello!", LocalDate.of(2025, 9, 2), "optional");
message.metadata = new ArrayList<>();
String json = defaultMapper.writeValueAsString(message);
assertTrue(json.contains("metadata"));
assertTrue(json.contains("optionalField") || json.contains("optional"));
}
We also check that dates are serialized in ISO-8601 format:
@Test
void givenMessageWithDate_whenSerialized_thenDateIsInIso8601Format() throws Exception {
Message message = new Message("Date Test", LocalDate.of(2025, 9, 2), "optional");
String json = defaultMapper.writeValueAsString(message);
assertTrue(json.contains("2025-09-02"));
}
6. 結論
在本教程中,我們探討了為 Jersey 應用程序自定義 Jackson 的 ObjectMapper 的方法。對於 Jersey 2.x,通過擴展 JacksonJaxbJsonProvider,我們可以全局配置 JSON 序列化,從而確保所有端點都採用一致的格式。這種方法簡單但適用於所有模型。
對於 Jersey 3.x,實現 ContextResolver<ObjectMapper> 提供了更大的靈活性。我們可以定義多個 ObjectMapper 配置並根據註解或模型類型進行條件選擇。這種方法確保了現代 Jersey 應用程序中靈活、可維護且一致的 JSON 處理。