4669 字
23 分钟

Jackson 入门:从 ObjectMapper 到特殊类型处理

发布于 2026-04-09

一、为什么 Java 后端离不开 JSON 序列化#

在 Web 后端开发中,接口通常需要接收 JSON 请求体,并返回 JSON 响应结果。

例如前端传来这样一段 JSON:

{
"id": 1,
"username": "minjer",
"age": 23
}

后端一般不会直接手动解析字符串,而是希望把它自动转换成 Java 对象:

public class UserDTO {
private Long id;
private String username;
private Integer age;
}

同样,后端返回数据时,也希望可以自动把 Java 对象转换成 JSON 字符串。

这类“Java 对象和 JSON 之间互相转换”的过程,就叫做序列化和反序列化:

  • 序列化:Java 对象 -> JSON 字符串
  • 反序列化:JSON 字符串 -> Java 对象

Jackson 就是 Java 后端开发中非常常见的一套 JSON 处理库。

在 Spring Boot 项目中,我们写接口时通常不需要手动调用 Jackson:

@GetMapping("/user")
public UserDTO getUser() {
return new UserDTO(1L, "minjer", 23);
}

Spring MVC 会在返回响应时自动使用 Jackson 把 UserDTO 转成 JSON。

也就是说,很多时候我们虽然没有直接写 Jackson 代码,但它其实一直在接口数据转换过程中工作。

971802c7-10c1-4805-8db2-7468b16e2464


二、Jackson 的核心入口:ObjectMapper#

e47e5aa5-b631-4e4f-a04b-35aff7985500

如果脱离 Spring Boot,单独使用 Jackson,最核心的类就是 ObjectMapper

它负责完成 Java 对象、JSON 字符串、Map、List、JsonNode 等数据结构之间的转换。

1. Java 对象转 JSON#

ObjectMapper objectMapper = new ObjectMapper();
UserDTO user = new UserDTO();
user.setId(1L);
user.setUsername("minjer");
user.setAge(23);
String json = objectMapper.writeValueAsString(user);
System.out.println(json);

输出结果类似:

{ "id": 1, "username": "minjer", "age": 23 }

这里的 writeValueAsString() 就是把 Java 对象序列化成 JSON 字符串。


2. JSON 转 Java 对象#

String json = "{\"id\":1,\"username\":\"minjer\",\"age\":23}";
UserDTO user = objectMapper.readValue(json, UserDTO.class);
System.out.println(user.getUsername());

这里的 readValue() 就是把 JSON 字符串反序列化成 Java 对象。


3. JSON 转 Map#

有时候我们并不想提前定义 DTO,也可以直接转成 Map

Map<String, Object> map = objectMapper.readValue(json, Map.class);
System.out.println(map.get("username"));

这种方式适合处理结构不固定的 JSON,但缺点是类型不够明确,后续取值时需要自己做类型转换。


4. JSON 转 JsonNode#

Jackson 还提供了树模型 JsonNode,适合读取复杂 JSON 中的部分字段。

JsonNode root = objectMapper.readTree(json);
String username = root.get("username").asText();
Integer age = root.get("age").asInt();

JsonNode 的好处是不用完整定义 Java 类,也能比较方便地读取嵌套 JSON。


三、Jackson 常用注解#

在实际业务中,Java 字段名和 JSON 字段名不一定完全一致,有些字段也可能不希望返回给前端。

这时候可以使用 Jackson 提供的注解进行控制。


1. @JsonProperty:指定 JSON 字段名#

public class UserDTO {
@JsonProperty("user_name")
private String username;
}

序列化后,字段名会从 username 变成 user_name

{
"user_name": "minjer"
}

这个注解常用于前后端字段命名风格不一致的场景。

例如 Java 中常用驼峰命名:

userName

而 JSON 接口中可能希望使用下划线命名:

user_name

2. @JsonIgnore:忽略字段#

public class UserDTO {
private Long id;
@JsonIgnore
private String password;
}

这样返回 JSON 时,password 字段就不会被输出。

这个注解很适合用于密码、内部标识、临时字段等不应该暴露给前端的数据。


3. @JsonInclude:控制空值是否输出#

@JsonInclude(JsonInclude.Include.NON_NULL)
public class UserDTO {
private Long id;
private String username;
private String email;
}

emailnull 时,序列化结果中就不会出现这个字段。

如果不加控制,默认可能会输出:

{
"id": 1,
"username": "minjer",
"email": null
}

加上 @JsonInclude(JsonInclude.Include.NON_NULL) 后,会变成:

{
"id": 1,
"username": "minjer"
}

这可以让接口响应更加简洁。


4. @JsonFormat:格式化日期#

public class UserDTO {
@JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss", timezone = "GMT+8")
private LocalDateTime createTime;
}

这个注解常用于控制日期时间字段的输出格式。

例如把 LocalDateTime 输出成:

{
"createTime": "2026-06-09 20:30:00"
}

而不是时间戳或其他默认格式。


四、特殊类型处理:枚举、日期、泛型和嵌套对象#

基础类型字段通常可以被 Jackson 自动处理,但是在真实项目中,我们经常会遇到一些特殊类型。

例如:

public class UserDTO {
private Long id;
private String username;
private UserStatus status;
private LocalDateTime createTime;
private List<RoleDTO> roles;
private AddressDTO address;
private Map<String, Object> extra;
}

这些字段分别对应枚举、日期时间、泛型集合、嵌套对象和扩展字段。

其中,枚举类是非常常见的一类特殊类型,尤其适合表示用户状态、订单状态、支付方式、业务类型等固定范围的值。


五、枚举类的序列化与反序列化#

在实际业务开发中,接口字段并不总是简单的 StringIntegerLong

很多字段天然适合使用枚举类型表示,例如:

  • 用户状态:启用、禁用;
  • 订单状态:待支付、已支付、已取消;
  • 性别类型:男、女、未知;
  • 支付方式:微信、支付宝、银行卡;
  • 业务类型:新增、修改、删除。

相比直接使用数字或字符串,枚举类的好处是语义更清晰,也能减少魔法值。

例如:

public enum UserStatus {
ENABLED(1, "启用"),
DISABLED(0, "禁用");
private final Integer code;
private final String desc;
UserStatus(Integer code, String desc) {
this.code = code;
this.desc = desc;
}
public Integer getCode() {
return code;
}
public String getDesc() {
return desc;
}
}

然后在 DTO 中使用:

public class UserDTO {
private Long id;
private String username;
private UserStatus status;
}

1. Jackson 默认如何处理枚举#

默认情况下,Jackson 会把枚举按照枚举名称进行序列化。

例如:

UserDTO user = new UserDTO();
user.setId(1L);
user.setUsername("minjer");
user.setStatus(UserStatus.ENABLED);

序列化后的结果通常是:

{
"id": 1,
"username": "minjer",
"status": "ENABLED"
}

也就是说,默认输出的是枚举常量名:

ENABLED

如果前端传入:

{
"status": "ENABLED"
}

Jackson 也可以把它反序列化成:

UserStatus.ENABLED

这种方式最简单,但也有明显问题:

  1. 枚举名称通常偏后端代码风格,不适合直接暴露给前端;
  2. 前端更可能需要 codedesc
  3. 如果枚举名后续重构,可能影响接口兼容性;
  4. 数据库存储通常是 01 这类 code,而不是 ENABLEDDISABLED

所以在真实项目中,经常需要对枚举的序列化和反序列化进行定制。


2. 使用 @JsonValue 指定枚举输出值#

如果我们希望枚举序列化时只输出 code,可以在 getCode() 方法上添加 @JsonValue

public enum UserStatus {
ENABLED(1, "启用"),
DISABLED(0, "禁用");
private final Integer code;
private final String desc;
UserStatus(Integer code, String desc) {
this.code = code;
this.desc = desc;
}
@JsonValue
public Integer getCode() {
return code;
}
public String getDesc() {
return desc;
}
}

这样序列化时,结果就会从:

{
"status": "ENABLED"
}

变成:

{
"status": 1
}

这适合前端只需要拿到状态编码的场景。

不过这种方式有一个缺点:前端只能看到 1,无法直接知道它代表“启用”。

如果接口需要同时返回 codedesc,可以使用另一种方式。


3. 使用 @JsonFormat 把枚举输出成对象#

如果希望枚举序列化后变成一个完整对象,可以在枚举类上添加:

@JsonFormat(shape = JsonFormat.Shape.OBJECT)
public enum UserStatus {
ENABLED(1, "启用"),
DISABLED(0, "禁用");
private final Integer code;
private final String desc;
UserStatus(Integer code, String desc) {
this.code = code;
this.desc = desc;
}
public Integer getCode() {
return code;
}
public String getDesc() {
return desc;
}
}

这时 UserDTO 序列化结果会变成:

{
"id": 1,
"username": "minjer",
"status": {
"code": 1,
"desc": "启用"
}
}

这种方式适合管理后台、字典展示、状态展示等场景。

它的优点是前端不需要再额外维护一份状态映射表。

但是也要注意:如果接口只需要传递状态值,这种结构会显得有点重。


4. 使用 @JsonCreator 支持根据 code 反序列化枚举#

如果前端传入的是:

{
"status": 1
}

而后端希望自动转换成:

UserStatus.ENABLED

可以在枚举类中添加一个静态方法,并使用 @JsonCreator 标记:

public enum UserStatus {
ENABLED(1, "启用"),
DISABLED(0, "禁用");
private final Integer code;
private final String desc;
UserStatus(Integer code, String desc) {
this.code = code;
this.desc = desc;
}
public Integer getCode() {
return code;
}
public String getDesc() {
return desc;
}
@JsonCreator
public static UserStatus fromCode(Integer code) {
if (code == null) {
return null;
}
for (UserStatus status : UserStatus.values()) {
if (status.getCode().equals(code)) {
return status;
}
}
throw new IllegalArgumentException("未知的用户状态 code: " + code);
}
}

这样当前端传入:

{
"status": 1
}

Jackson 就会调用:

UserStatus.fromCode(1)

最终得到:

UserStatus.ENABLED

这是一种非常常见的枚举反序列化写法。


5. 同时支持序列化和反序列化#

在实际项目中,经常会把 @JsonValue@JsonCreator 配合使用:

public enum UserStatus {
ENABLED(1, "启用"),
DISABLED(0, "禁用");
private final Integer code;
private final String desc;
UserStatus(Integer code, String desc) {
this.code = code;
this.desc = desc;
}
@JsonValue
public Integer getCode() {
return code;
}
public String getDesc() {
return desc;
}
@JsonCreator
public static UserStatus fromCode(Integer code) {
if (code == null) {
return null;
}
for (UserStatus status : UserStatus.values()) {
if (status.getCode().equals(code)) {
return status;
}
}
throw new IllegalArgumentException("未知的用户状态 code: " + code);
}
}

这样就可以实现:

Java 枚举序列化成 JSON:

UserStatus.ENABLED

输出为:

1

JSON 反序列化成 Java 枚举:

1

转换为:

UserStatus.ENABLED

这种方式比较适合前后端统一使用 code 作为枚举传输值的项目。


6. 如果前端传了未知枚举值怎么办#

还有一个常见问题:如果前端传入了后端不存在的枚举值怎么办?

例如:

{
"status": 999
}

如果直接抛异常,接口会返回错误。

这在严格业务场景下是合理的,因为非法状态本来就应该被拒绝。

但是在某些兼容性场景中,也可以返回一个兜底枚举:

public enum UserStatus {
ENABLED(1, "启用"),
DISABLED(0, "禁用"),
UNKNOWN(-1, "未知");
private final Integer code;
private final String desc;
UserStatus(Integer code, String desc) {
this.code = code;
this.desc = desc;
}
@JsonValue
public Integer getCode() {
return code;
}
public String getDesc() {
return desc;
}
@JsonCreator
public static UserStatus fromCode(Integer code) {
if (code == null) {
return null;
}
for (UserStatus status : UserStatus.values()) {
if (status.getCode().equals(code)) {
return status;
}
}
return UNKNOWN;
}
}

这样即使前端传入了未知值,也不会直接反序列化失败。

不过是否使用 UNKNOWN,要根据业务场景决定。

如果是订单状态、支付状态这类严肃字段,更推荐直接抛异常,而不是静默转成未知状态。


7. 什么时候使用枚举类#

枚举类适合用于值范围固定、业务含义明确的字段。

例如:

private UserStatus status;
private OrderStatus orderStatus;
private PayType payType;
private Gender gender;

不太适合频繁变化、需要后台动态维护的数据。

例如:

private String city;
private String productCategory;
private String departmentName;

这些字段通常更适合放在数据库字典表中维护,而不是写死在枚举类里。

可以按数据的稳定程度和使用方式选择:

  • 固定业务状态:适合枚举;
  • 变化频繁的数据字典:适合数据库表;
  • 前后端都需要展示含义:枚举需要提供 code 和 desc;
  • 接口只传递状态值:可以只输出 code;
  • 管理后台展示:可以输出完整对象。

8. 枚举处理小结#

Jackson 默认会按照枚举名称处理枚举类。

如果业务中希望使用 code 作为传输值,可以使用:

@JsonValue

控制枚举如何序列化。

如果希望前端传入 code 后自动转成枚举,可以使用:

@JsonCreator

提供一个静态工厂方法。

如果希望枚举输出成完整对象,可以使用:

@JsonFormat(shape = JsonFormat.Shape.OBJECT)

最终如何选择,取决于接口设计:

场景推荐方式
前后端直接传枚举名使用默认行为
只传状态编码@JsonValue + @JsonCreator
返回给前端展示 code 和 desc@JsonFormat(shape = JsonFormat.Shape.OBJECT)
未知枚举值需要兼容提供 UNKNOWN 兜底值
严格业务状态未知值直接抛异常

六、处理 LocalDateTime#

在 Java 8 之后,后端项目中经常使用 LocalDateTimeLocalDateLocalTime

如果直接使用 Jackson 处理 Java 8 时间类型,有时会遇到格式不符合预期的问题。

常见做法是引入并注册 JavaTimeModule

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

其中:

objectMapper.disable(SerializationFeature.WRITE_DATES_AS_TIMESTAMPS);

表示不要把日期时间输出成时间戳,而是输出成更可读的字符串格式。

在 Spring Boot 项目中,很多基础配置已经被自动处理,但如果你希望统一接口中的日期格式,仍然可以通过配置文件或自定义配置类进行调整。


七、处理泛型集合#

除了枚举和日期,泛型集合也是 Jackson 使用中的一个常见坑。

例如我们希望把 JSON 数组转换成 List<UserDTO>

[
{
"id": 1,
"username": "minjer"
},
{
"id": 2,
"username": "jack"
}
]

如果直接写成:

List<UserDTO> users = objectMapper.readValue(json, List.class);

虽然代码可以运行,但得到的通常不是 List<UserDTO>,而是类似 List<LinkedHashMap> 的结构。

原因是 Java 的泛型在运行时存在类型擦除,Jackson 只知道目标类型是 List,不知道 List 里面具体是什么对象。

这时候应该使用 TypeReference

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

如果是更复杂的结构,例如:

Map<String, List<UserDTO>>

也可以这样处理:

Map<String, List<UserDTO>> userMap = objectMapper.readValue(
json,
new TypeReference<Map<String, List<UserDTO>>>() {}
);

目标类型包含泛型时,优先使用 TypeReference 保留完整类型信息。


八、处理嵌套对象#

Jackson 对嵌套对象的支持比较自然。

例如:

public class UserDTO {
private Long id;
private String username;
private AddressDTO address;
}
public class AddressDTO {
private String province;
private String city;
}

对应 JSON 可以是:

{
"id": 1,
"username": "minjer",
"address": {
"province": "广东",
"city": "深圳"
}
}

只要 Java 类结构和 JSON 结构能够对应上,Jackson 就可以自动完成转换。

需要注意的是,嵌套对象也需要有可访问的构造方式和字段访问方式。

常见做法是:

  • 提供无参构造方法;
  • 提供 getter / setter;
  • 或者使用 Lombok 的 @Data@NoArgsConstructor 等注解。

九、自定义序列化:JsonSerializer#

Jackson 的注解可以解决很多常见问题,但如果业务逻辑更加复杂,就需要自定义序列化器。

例如下面这些场景:

  • 手机号脱敏
  • 身份证号脱敏
  • 金额格式化
  • 枚举值转中文描述
  • 根据用户权限决定字段是否展示明文

自定义序列化器需要继承 JsonSerializer<T>,并重写 serialize() 方法。

例如我们希望把手机号脱敏:

public class PhoneJsonSerializer extends JsonSerializer<String> {
@Override
public void serialize(
String value,
JsonGenerator gen,
SerializerProvider serializers
) throws IOException {
if (value == null) {
gen.writeNull();
return;
}
String masked = value.replaceAll("(\\d{3})\\d{4}(\\d{4})", "$1****$2");
gen.writeString(masked);
}
}

然后在字段上使用:

public class UserDTO {
@JsonSerialize(using = PhoneJsonSerializer.class)
private String phone;
}

当接口返回时,手机号就会被自动脱敏。

例如:

{
"phone": "138****5678"
}

这个过程的本质是:

  1. Jackson 序列化 phone 字段;
  2. 发现字段上有 @JsonSerialize
  3. 使用指定的 PhoneJsonSerializer
  4. 调用 serialize() 方法;
  5. 写出脱敏后的 JSON 字段值。

5a2dd692-8fe3-4b7b-9ea2-e33fb3ad4702


十、自定义反序列化:JsonDeserializer#

和序列化相反,反序列化是把 JSON 转成 Java 对象。

如果前端传来的数据格式比较特殊,也可以自定义反序列化器。

public class GenderDeserializer extends JsonDeserializer<Integer> {
@Override
public Integer deserialize(
JsonParser p,
DeserializationContext ctxt
) throws IOException {
String value = p.getText();
if ("男".equals(value)) {
return 1;
}
if ("女".equals(value)) {
return 2;
}
return 0;
}
}

使用方式:

public class UserDTO {
@JsonDeserialize(using = GenderDeserializer.class)
private Integer gender;
}

这样当前端传入:

{
"gender": "男"
}

后端接收到的 Java 对象中,gender 就会被转换成 1


十一、Spring Boot 中如何配置 Jackson#

在 Spring Boot 项目中,Jackson 通常已经被自动集成。

如果我们只是写普通接口:

@RestController
@RequestMapping("/users")
public class UserController {
@GetMapping("/{id}")
public UserDTO getUser(@PathVariable Long id) {
UserDTO user = new UserDTO();
user.setId(id);
user.setUsername("minjer");
return user;
}
}

Spring Boot 会自动把返回对象序列化成 JSON。

如果想进行全局配置,可以使用配置文件:

spring:
jackson:
date-format: yyyy-MM-dd HH:mm:ss
time-zone: GMT+8
default-property-inclusion: non_null

也可以通过配置类进行更灵活的控制:

@Configuration
public class JacksonConfig {
@Bean
public Jackson2ObjectMapperBuilderCustomizer jacksonCustomizer() {
return builder -> {
builder.simpleDateFormat("yyyy-MM-dd HH:mm:ss");
builder.serializationInclusion(JsonInclude.Include.NON_NULL);
};
}
}

如果需要注册自定义序列化器,可以使用 SimpleModule

@Configuration
public class JacksonConfig {
@Bean
public Module customModule() {
SimpleModule module = new SimpleModule();
module.addSerializer(String.class, new PhoneJsonSerializer());
return module;
}
}

不过这里要注意:如果直接给 String.class 注册全局序列化器,会影响所有字符串字段。

所以在真实项目中,更推荐结合注解,只对特定字段生效。


十二、ObjectMapper 不建议频繁 new#

在示例代码里,我们经常这样写:

ObjectMapper objectMapper = new ObjectMapper();

这是为了演示方便。

但在真实项目中,不建议每次使用时都重新创建 ObjectMapper

原因是 ObjectMapper 内部会维护序列化器、反序列化器、类型信息等缓存。频繁创建不仅没有必要,也会影响性能和配置一致性。

在 Spring Boot 项目中,通常直接使用框架自动配置好的 ObjectMapper

@Service
public class JsonService {
private final ObjectMapper objectMapper;
public JsonService(ObjectMapper objectMapper) {
this.objectMapper = objectMapper;
}
}

这样可以保证项目中的 JSON 行为保持一致。


十三、常见问题总结#

1. 字段不想返回给前端怎么办?#

使用 @JsonIgnore

@JsonIgnore
private String password;

2. 字段名想改成下划线怎么办?#

可以使用 @JsonProperty

@JsonProperty("user_name")
private String username;

也可以使用全局命名策略,让所有字段统一从驼峰转下划线。


3. 空字段不想返回怎么办?#

使用 @JsonInclude

@JsonInclude(JsonInclude.Include.NON_NULL)

4. 日期格式不符合预期怎么办?#

可以使用 @JsonFormat 控制单个字段,也可以通过 Spring Boot 全局配置统一处理。


5. JSON 数组转 List 结果不对怎么办?#

如果目标类型是 List<UserDTO>Map<String, UserDTO> 这类泛型结构,建议使用 TypeReference

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

6. 枚举字段应该怎么设计?#

如果只是内部系统,直接使用枚举名也可以。

如果是前后端接口,更推荐为枚举设计稳定的 code

@JsonValue
public Integer getCode() {
return code;
}

同时使用 @JsonCreator 支持根据 code 反序列化:

@JsonCreator
public static UserStatus fromCode(Integer code) {
// 根据 code 查找枚举
}

这样接口传输值更加稳定,不容易受到后端枚举命名变化的影响。


7. 复杂字段规则怎么办?#

如果普通注解无法满足需求,就可以考虑自定义 JsonSerializerJsonDeserializer


十四、总结#

Jackson 是 Java 后端开发中非常重要的 JSON 处理工具。

对于普通业务开发来说,掌握下面几个点基本就够用了:

  1. 使用 ObjectMapper 完成对象和 JSON 的转换;
  2. 使用常见注解控制字段名、空值、日期格式和忽略字段;
  3. 使用 JsonNode 处理结构不固定的 JSON;
  4. 使用 TypeReference 处理泛型集合;
  5. 使用 @JsonValue@JsonCreator@JsonFormat 处理枚举类;
  6. 使用 JsonSerializer 实现自定义序列化;
  7. 在 Spring Boot 中尽量使用框架统一管理的 ObjectMapper

对于普通接口开发,Jackson 的默认配置通常已经够用。

但一旦遇到字段脱敏、权限控制、枚举转换、日期格式统一、复杂 JSON 解析等场景,就需要真正理解 Jackson 的序列化和反序列化机制。

理解 Jackson,不只是为了会写 JSON 转换代码,更是为了更好地掌控后端接口的数据输入和输出。

Jackson 入门:从 ObjectMapper 到特殊类型处理
https://mj3622.github.io/posts/编程实践/jackson经验分享/
作者
Minjer
发布于
2026-04-09
许可协议
CC BY-NC-SA 4.0