概述
Jackson 是 Java 生态中最流行的 JSON 处理库,由 FasterXML 团队维护。它提供了高性能的 JSON 序列化与反序列化能力,广泛应用于 Spring Boot、Dropwizard 等主流框架中。Jackson 的核心优势在于其流式 API 的高性能、注解驱动的灵活配置,以及对复杂 Java 类型(如泛型、多态、嵌套对象)的完整支持。
Jackson 由三个核心模块组成,分别是 jackson-core、jackson-annotations 和 jackson-databind。jackson-core 提供底层的流式读写 API(JsonParser 与 JsonGenerator),jackson-annotations 定义所有注解,jackson-databind 则在两者之上构建出对象绑定能力。日常开发中只需引入 jackson-databind,它会自动传递依赖另外两个模块。
本指南基于 Jackson 2.13 版本,按照实际调用顺序组织内容:先讲序列化再讲反序列化,先讲简单类型再讲复杂类型,先讲字段筛选再讲字段转换。所有示例代码均可直接运行,读者可按章节顺序逐步实践。
引入依赖
使用 Maven 时,在 pom.xml 中添加 jackson-databind 依赖即可,它会自动引入 jackson-core 与 jackson-annotations。
xml
<dependency>
<groupId>com.fasterxml.jackson.core</groupId>
<artifactId>jackson-databind</artifactId>
<version>2.13.4.2</version>
</dependency>
如果使用 Gradle,对应的配置如下。版本号可根据实际需要调整,本指南示例均基于 2.13.x 系列。
groovy
implementation 'com.fasterxml.jackson.core:jackson-databind:2.13.4.2'
ObjectMapper 核心入口
ObjectMapper 是 Jackson 的核心门面类,所有序列化与反序列化操作都通过它完成。一个 ObjectMapper 实例内部维护了类型工厂、序列化器缓存、反序列化器缓存等重型组件,因此它的创建成本较高。官方推荐将 ObjectMapper 作为单例复用,而不是每次请求都新建一个实例。ObjectMapper 本身是线程安全的,配置完成后可被多线程并发调用。
下面是最基础的 ObjectMapper 创建与使用示例。我们先定义一个简单的 POJO,然后将其序列化为 JSON 字符串。
java
import com.fasterxml.jackson.databind.ObjectMapper;
public class User {
private Long id;
private String name;
private Integer age;
public User() {}
public User(Long id, String name, Integer age) {
this.id = id;
this.name = name;
this.age = age;
}
public Long getId() { return id; }
public void setId(Long id) { this.id = id; }
public String getName() { return name; }
public void setName(String name) { this.name = name; }
public Integer getAge() { return age; }
public void setAge(Integer age) { this.age = age; }
}
ObjectMapper 提供了一系列 writeValue 与 readValue 方法,分别用于序列化与反序列化。下面演示如何将对象转为 JSON 字符串,以及如何将 JSON 字符串转回对象。
java
ObjectMapper mapper = new ObjectMapper();
// 序列化:对象 -> JSON 字符串
User user = new User(1L, "张三", 28);
String json = mapper.writeValueAsString(user);
// 输出:{"id":1,"name":"张三","age":28}
// 反序列化:JSON 字符串 -> 对象
User parsed = mapper.readValue(json, User.class);
序列化基础
序列化是将 Java 对象转换为 JSON 的过程。Jackson 默认会调用对象的所有 public getter 方法,将返回值作为 JSON 字段。字段名默认与属性名一致,字段顺序默认与声明顺序一致(实际依赖反射返回顺序,建议通过注解显式控制)。
对于简单类型,Jackson 内置了完整的序列化器支持。String、Integer、Long、Double、Boolean 等基本包装类型,以及 BigDecimal、BigInteger、UUID、URL 等 java 标准类型,都无需任何配置即可正确序列化。
java
ObjectMapper mapper = new ObjectMapper();
Map<String, Object> data = new HashMap<>();
data.put("name", "李四");
data.put("age", 30);
data.put("active", true);
data.put("score", 95.5);
data.put("createdAt", new Date());
String json = mapper.writeValueAsString(data);
// 输出:{"name":"李四","age":30,"active":true,"score":95.5,"createdAt":1658908800000}
注意上面示例中 Date 类型默认被序列化为时间戳(毫秒数)。如果希望输出格式化日期字符串,需要使用 @JsonFormat 注解或全局配置,这部分内容会在字段转换章节详细介绍。
如果希望输出的 JSON 带有缩进以便阅读,可以开启 SerializationFeature.INDENT_OUTPUT。
java
ObjectMapper mapper = new ObjectMapper();
mapper.enable(SerializationFeature.INDENT_OUTPUT);
String json = mapper.writeValueAsString(user);
// 输出:
// {
// "id" : 1,
// "name" : "张三",
// "age" : 28
// }
序列化字段筛选
在实际业务中,并非所有字段都应当输出到 JSON。例如密码、敏感信息、内部状态字段等。Jackson 提供了多种注解用于控制字段的序列化行为,本节按使用频率依次介绍。
@JsonIgnore 忽略单个字段
@JsonIgnore 作用于字段、getter 方法或 setter 方法上,表示该字段在序列化与反序列化时都被忽略。这是最常用的字段过滤注解,适合明确不需要参与 JSON 转换的字段。
java
public class Account {
private Long id;
private String username;
@JsonIgnore
private String password;
@JsonIgnore
private String salt;
// getter / setter 省略
}
ObjectMapper mapper = new ObjectMapper();
Account account = new Account();
account.setId(1L);
account.setUsername("admin");
account.setPassword("123456");
account.setSalt("abc");
String json = mapper.writeValueAsString(account);
// 输出:{"id":1,"username":"admin"}
@JsonIgnoreProperties 批量忽略字段
@JsonIgnoreProperties 作用于类级别,可以一次性声明多个需要忽略的字段名。它还提供了 ignoreUnknown 属性,用于在反序列化时忽略 JSON 中存在但 Java 类中不存在的字段,这是避免 UnrecognizedPropertyException 的常用手段。
java
@JsonIgnoreProperties({"internalCode", "auditLog"})
public class Product {
private Long id;
private String name;
private String internalCode;
private String auditLog;
// getter / setter 省略
}
@JsonProperty 重命名与访问控制
@JsonProperty 是 Jackson 中功能最丰富的注解之一。它的核心作用是为字段指定 JSON 中的名称,同时还可以通过 access 属性控制字段在序列化与反序列化时的可见性。access 取值为 JsonProperty.Access 枚举,包含 READ_ONLY、WRITE_ONLY、READ_WRITE、AUTO 四个值。
READ_ONLY 表示字段只在序列化时输出,反序列化时忽略(即只读);WRITE_ONLY 表示字段只在反序列化时接收输入,序列化时不输出(即只写,常用于密码字段)。
java
public class LoginRequest {
@JsonProperty("user_name")
private String username;
@JsonProperty(access = JsonProperty.Access.WRITE_ONLY)
private String password;
@JsonProperty(access = JsonProperty.Access.READ_ONLY)
private String token;
// getter / setter 省略
}
ObjectMapper mapper = new ObjectMapper();
LoginRequest req = new LoginRequest();
req.setUsername("admin");
req.setPassword("secret");
req.setToken("tmp-token");
String json = mapper.writeValueAsString(req);
// 输出:{"user_name":"admin","token":"tmp-token"}
// password 因 WRITE_ONLY 不会输出
LoginRequest parsed = mapper.readValue(
"{\"user_name\":\"admin\",\"password\":\"secret\",\"token\":\"x\"}",
LoginRequest.class
);
// parsed.username = "admin"
// parsed.password = "secret"
// parsed.token = null(READ_ONLY 字段反序列化时被忽略)
@JsonInclude 控制空值输出
默认情况下,Jackson 会序列化所有字段,包括值为 null、空集合、空字符串的字段。@JsonInclude 用于控制何时将字段写入 JSON。它可以作用于类级别或字段级别。
java
@JsonInclude(JsonInclude.Include.NON_NULL)
public class Article {
private Long id;
private String title;
private String content;
private String summary;
// getter / setter 省略
}
Article article = new Article();
article.setId(1L);
article.setTitle("Hello Jackson");
// content 与 summary 为 null,不会出现在 JSON 中
String json = new ObjectMapper().writeValueAsString(article);
// 输出:{"id":1,"title":"Hello Jackson"}
JsonInclude.Include 提供了多种策略:ALWAYS(默认,总是包含)、NON_NULL(非 null 才包含)、NON_EMPTY(非 null 且非空字符串/集合)、NON_DEFAULT(非默认值才包含)、CUSTOM(自定义过滤器)。
如果希望全局生效,可以在 ObjectMapper 上配置,避免在每个类上重复声明。
java
ObjectMapper mapper = new ObjectMapper();
mapper.setSerializationInclusion(JsonInclude.Include.NON_NULL);
反序列化基础
反序列化是将 JSON 转换为 Java 对象的过程。Jackson 默认调用对象的无参构造函数创建实例,然后通过 setter 方法或字段直接赋值填充属性。因此,可反序列化的 POJO 必须提供无参构造函数(或通过 @JsonCreator 指定其他构造方式)。
对于简单类型,readValue 方法可以直接将 JSON 字符串映射到目标类。Jackson 会根据字段名自动匹配 JSON 中的 key,名称不匹配的字段会被忽略(前提是开启了 FAIL_ON_UNKNOWN_PROPERTIES = false,否则会抛异常)。
java
ObjectMapper mapper = new ObjectMapper();
// 默认开启 FAIL_ON_UNKNOWN_PROPERTIES,建议关闭
mapper.configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false);
String json = "{\"id\":1,\"name\":\"张三\",\"age\":28,\"extra\":\"ignored\"}";
User user = mapper.readValue(json, User.class);
// user.getId() = 1
// user.getName() = "张三"
// user.getAge() = 28
Jackson 还提供了 readTree 方法,将 JSON 解析为 JsonNode 树结构,适合在不定义 POJO 的情况下灵活访问 JSON 数据。
java
JsonNode root = mapper.readTree(json);
long id = root.get("id").asLong();
String name = root.get("name").asText();
// 也可以用 path 方法,避免字段不存在时返回 null
int age = root.path("age").asInt();
@JsonCreator 自定义反序列化入口
当 POJO 没有无参构造函数,或希望用全参构造函数、静态工厂方法创建实例时,可以使用 @JsonCreator 注解。@JsonCreator 可标注在构造函数或静态方法上,配合 @JsonProperty 指定参数与 JSON 字段的映射关系。
java
public class Money {
private final long cents;
private final String currency;
@JsonCreator
public Money(
@JsonProperty("cents") long cents,
@JsonProperty("currency") String currency
) {
this.cents = cents;
this.currency = currency;
}
public long getCents() { return cents; }
public String getCurrency() { return currency; }
}
String json = "{\"cents\":1999,\"currency\":\"CNY\"}";
Money money = new ObjectMapper().readValue(json, Money.class);
// money.getCents() = 1999
// money.getCurrency() = "CNY"
@JsonCreator 还支持 mode 属性,取值为 DEFAULT、PROPERTIES、DELEGATING。PROPERTIES 模式按属性名匹配参数(最常用),DELEGATING 模式将整个 JSON 值作为单个参数传入(适合包装类型的反序列化)。
@JsonAlias 字段别名
@JsonAlias 为字段提供一个或多个备选名称,在反序列化时如果 JSON 中出现任意一个别名,都会被映射到该字段。这在对接多个外部系统、字段命名风格不一致时非常有用。注意 @JsonAlias 只在反序列化时生效,序列化时仍使用 @JsonProperty 指定的主名称。
java
public class Order {
@JsonProperty("order_id")
@JsonAlias({"orderId", "orderNo", "id"})
private String orderId;
@JsonProperty("total_amount")
@JsonAlias({"totalAmount", "amount"})
private BigDecimal totalAmount;
// getter / setter 省略
}
ObjectMapper mapper = new ObjectMapper();
// 以下三种 JSON 都能正确反序列化
Order o1 = mapper.readValue("{\"order_id\":\"A001\",\"total_amount\":100}", Order.class);
Order o2 = mapper.readValue("{\"orderId\":\"A002\",\"totalAmount\":200}", Order.class);
Order o3 = mapper.readValue("{\"orderNo\":\"A003\",\"amount\":300}", Order.class);
复杂类型处理
简单 POJO 的序列化与反序列化在前面章节已经覆盖。本节讨论更复杂的场景:嵌套对象、集合类型、泛型擦除、枚举处理。这些场景在实际业务中极为常见,掌握它们是熟练使用 Jackson 的关键。
嵌套对象
Jackson 天然支持嵌套对象的序列化与反序列化,无需额外配置。只要嵌套的内部类本身是合法的 POJO,Jackson 就会递归处理。
java
public class Address {
private String province;
private String city;
private String detail;
// getter / setter 省略
}
public class Customer {
private Long id;
private String name;
private Address address;
// getter / setter 省略
}
ObjectMapper mapper = new ObjectMapper();
Customer customer = new Customer();
customer.setId(1L);
customer.setName("王五");
Address addr = new Address();
addr.setProvince("广东");
addr.setCity("深圳");
addr.setDetail("南山区科技园");
customer.setAddress(addr);
String json = mapper.writeValueAsString(customer);
// 输出:
// {"id":1,"name":"王五","address":{"province":"广东","city":"深圳","detail":"南山区科技园"}}
Customer parsed = mapper.readValue(json, Customer.class);
集合与映射类型
处理 List、Set、Map 等集合类型时,需要告诉 Jackson 集合元素的类型。对于 readValue 方法,可以通过 TypeReference 或 JavaType 来传递泛型信息。
java
ObjectMapper mapper = new ObjectMapper();
// 序列化集合
List<User> users = Arrays.asList(
new User(1L, "张三", 28),
new User(2L, "李四", 30)
);
String json = mapper.writeValueAsString(users);
// 输出:[{"id":1,"name":"张三","age":28},{"id":2,"name":"李四","age":30}]
// 反序列化集合:使用 TypeReference 保留泛型信息
List<User> parsed = mapper.readValue(
json,
new TypeReference<List<User>>() {}
);
// 反序列化 Map
Map<String, User> userMap = mapper.readValue(
"{\"u1\":{\"id\":1,\"name\":\"张三\",\"age\":28}}",
new TypeReference<Map<String, User>>() {}
);
TypeReference 是 Jackson 提供的泛型类型保留工具。它利用匿名内部类的方式在运行时保留泛型参数信息,从而绕过 Java 的类型擦除机制。这是处理嵌套泛型集合的标准做法。
如果不想用 TypeReference,也可以通过 TypeFactory 构造 JavaType。
java
ObjectMapper mapper = new ObjectMapper();
JavaType listType = mapper.getTypeFactory()
.constructCollectionType(List.class, User.class);
List<User> parsed = mapper.readValue(json, listType);
JavaType mapType = mapper.getTypeFactory()
.constructMapType(Map.class, String.class, User.class);
Map<String, User> userMap = mapper.readValue(json, mapType);
枚举处理
枚举默认按名称序列化与反序列化。例如 DayOfWeek.MONDAY 会被序列化为字符串 "MONDAY"。如果希望按序号处理,可以使用 @JsonFormat 注解。
java
public class Task {
public enum Status {
PENDING,
RUNNING,
DONE,
FAILED
}
private String name;
private Status status;
// getter / setter 省略
}
ObjectMapper mapper = new ObjectMapper();
Task task = new Task();
task.setName("backup");
task.setStatus(Task.Status.RUNNING);
String json = mapper.writeValueAsString(task);
// 输出:{"name":"backup","status":"RUNNING"}
Task parsed = mapper.readValue(
"{\"name\":\"backup\",\"status\":\"DONE\"}",
Task.class
);
// parsed.getStatus() == Task.Status.DONE
当枚举值与 JSON 中的字符串不一致时,可以使用 @JsonProperty 或 @JsonValue 重命名。@JsonValue 标注在枚举字段或方法上,表示该值作为枚举的序列化结果;@JsonCreator 标注在静态工厂方法上,用于根据 JSON 值反序列化为枚举实例。
java
public enum Gender {
MALE("M"),
FEMALE("F"),
UNKNOWN("U");
private final String code;
Gender(String code) {
this.code = code;
}
@JsonValue
public String getCode() {
return code;
}
@JsonCreator
public static Gender fromCode(String code) {
for (Gender g : values()) {
if (g.code.equalsIgnoreCase(code)) {
return g;
}
}
return UNKNOWN;
}
}
ObjectMapper mapper = new ObjectMapper();
String json = mapper.writeValueAsString(Gender.MALE);
// 输出:"M"
Gender g = mapper.readValue("\"F\"", Gender.class);
// g == Gender.FEMALE
多态类型处理
当 JSON 需要反序列化为某个抽象类或接口的具体子类时,Jackson 需要额外的类型信息才能决定实例化哪个子类。这就是多态类型处理(Polymorphic Type Handling)。Jackson 通过 @JsonTypeInfo 与 @JsonSubTypes 两个注解配合实现。
@JsonTypeInfo 与 @JsonSubTypes
@JsonTypeInfo 标注在父类上,声明类型信息如何存储。@JsonSubTypes 列出所有可能的子类及其类型标识。下面以一个支付方式场景为例。
java
@JsonTypeInfo(
use = JsonTypeInfo.Id.NAME,
include = JsonTypeInfo.As.PROPERTY,
property = "type"
)
@JsonSubTypes({
@JsonSubTypes.Type(value = AlipayPayment.class, name = "alipay"),
@JsonSubTypes.Type(value = WechatPayment.class, name = "wechat"),
@JsonSubTypes.Type(value = BankCardPayment.class, name = "bank_card")
})
public abstract class Payment {
protected BigDecimal amount;
public BigDecimal getAmount() { return amount; }
public void setAmount(BigDecimal amount) { this.amount = amount; }
}
public class AlipayPayment extends Payment {
private String alipayUserId;
// getter / setter 省略
}
public class WechatPayment extends Payment {
private String openId;
// getter / setter 省略
}
public class BankCardPayment extends Payment {
private String cardNumber;
// getter / setter 省略
}
序列化时,Jackson 会在 JSON 中添加一个 type 字段标识具体类型;反序列化时,根据 type 字段的值选择对应的子类。
java
ObjectMapper mapper = new ObjectMapper();
Payment payment = new AlipayPayment();
payment.setAmount(new BigDecimal("99.50"));
((AlipayPayment) payment).setAlipayUserId("user-001");
String json = mapper.writeValueAsString(payment);
// 输出:{"type":"alipay","amount":99.50,"alipayUserId":"user-001"}
Payment parsed = mapper.readValue(json, Payment.class);
// parsed instanceof AlipayPayment == true
@JsonTypeName 自定义子类标识
除了在 @JsonSubTypes.Type 中通过 name 属性指定标识,也可以在子类上使用 @JsonTypeName 注解声明自身的类型名。这种方式让类型标识与子类定义放在一起,更易于维护。
java
@JsonTypeInfo(use = JsonTypeInfo.Id.NAME, property = "type")
@JsonSubTypes({
@JsonSubTypes.Type(Circle.class),
@JsonSubTypes.Type(Rectangle.class)
})
public abstract class Shape {
protected String name;
// getter / setter 省略
}
@JsonTypeName("circle")
public class Circle extends Shape {
private double radius;
// getter / setter 省略
}
@JsonTypeName("rectangle")
public class Rectangle extends Shape {
private double width;
private double height;
// getter / setter 省略
}
类型信息的存储方式
@JsonTypeInfo 的 include 属性控制类型信息在 JSON 中的存储位置。常用取值包括 PROPERTY(作为普通字段,默认值)、WRAPPER_OBJECT(用包装对象包裹)、EXISTING_PROPERTY(使用已存在的字段)。
java
// WRAPPER_OBJECT 模式:序列化结果会被一个以类型名为 key 的对象包裹
@JsonTypeInfo(
use = JsonTypeInfo.Id.NAME,
include = JsonTypeInfo.As.WRAPPER_OBJECT
)
// 序列化 Circle 实例时输出:{"circle":{"name":"c1","radius":5.0}}
use 属性控制类型标识的取值类型。Id.NAME 使用逻辑名称(最常用),Id.CLASS 使用完整 Java 类名(不推荐,泄露内部实现且耦合类路径),Id.MINIMAL_CLASS 使用相对类名。
字段转换
前面章节主要讨论字段是否参与序列化、字段如何命名。本节讨论字段值的转换:日期格式化、命名策略、对象展开、自定义序列化器与反序列化器。这些是 Jackson 进阶使用的核心内容。
@JsonFormat 格式化日期与数字
@JsonFormat 最常见的用途是格式化日期。默认情况下,java.util.Date 与 java.util.Calendar 被序列化为时间戳。通过 @JsonFormat 可以指定日期格式字符串、时区,以及是否按形状(shape)输出。
java
public class Event {
private String name;
@JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss", timezone = "GMT+8")
private Date startTime;
@JsonFormat(pattern = "yyyy-MM-dd")
private Date eventDate;
// getter / setter 省略
}
ObjectMapper mapper = new ObjectMapper();
Event event = new Event();
event.setName("发布会");
SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
event.setStartTime(sdf.parse("2024-06-01 14:30:00"));
event.setEventDate(sdf.parse("2024-06-01"));
String json = mapper.writeValueAsString(event);
// 输出:{"name":"发布会","startTime":"2024-06-01 14:30:00","eventDate":"2024-06-01"}
@JsonFormat 也可以作用于 java.time 包下的 LocalDate、LocalDateTime、LocalTime 等类型(需要引入 jackson-datatype-jsr310 模块并注册)。shape 属性可以改变输出形状,例如 Shape.STRING 将数字输出为字符串。
@JsonNaming 命名策略
当 JSON 字段采用 snake_case(下划线小写)而 Java 字段采用 camelCase(驼峰)时,逐个字段使用 @JsonProperty 重命名非常繁琐。@JsonNaming 注解可以在类级别统一应用命名策略,自动转换所有字段名。
java
@JsonNaming(PropertyNamingStrategies.SnakeCaseStrategy.class)
public class SystemConfig {
private String configName;
private String configValue;
private Integer retryCount;
private Boolean isEnabled;
// getter / setter 省略
}
ObjectMapper mapper = new ObjectMapper();
SystemConfig config = new SystemConfig();
config.setConfigName("timeout");
config.setConfigValue("30");
config.setRetryCount(3);
config.setIsEnabled(true);
String json = mapper.writeValueAsString(config);
// 输出:{"config_name":"timeout","config_value":"30","retry_count":3,"is_enabled":true}
Jackson 2.13 内置了多种命名策略:SnakeCaseStrategy(snake_case)、UpperCamelCaseStrategy(PascalCase)、LowerCaseStrategy(全小写)、KebabCaseStrategy(kebab-case)、LowerDotCaseStrategy(lower.dot.case)。也可以全局配置 ObjectMapper 使用某种策略。
java
ObjectMapper mapper = new ObjectMapper();
mapper.setPropertyNamingStrategy(PropertyNamingStrategies.SNAKE_CASE);
@JsonUnwrapped 展开嵌套对象
默认情况下,嵌套对象会作为子对象输出。某些场景下,我们希望将嵌套对象的字段直接平铺到父对象中,避免层级过深。@JsonUnwrapped 实现了这一功能。
java
public class Location {
private String latitude;
private String longitude;
// getter / setter 省略
}
public class Place {
private String name;
@JsonUnwrapped
private Location location;
// getter / setter 省略
}
ObjectMapper mapper = new ObjectMapper();
Place place = new Place();
place.setName("深圳北站");
Location loc = new Location();
loc.setLatitude("22.6107");
loc.setLongitude("114.0293");
place.setLocation(loc);
String json = mapper.writeValueAsString(place);
// 输出:{"name":"深圳北站","latitude":"22.6107","longitude":"114.0293"}
// location 的字段被平铺到顶层,没有 location 这一层
Place parsed = mapper.readValue(json, Place.class);
// 反序列化时同样会从顶层提取 latitude/longitude 填充到 location
@JsonUnwrapped 还支持 prefix 与 suffix 属性,为展开的字段添加前缀或后缀,避免字段名冲突。
java
@JsonUnwrapped(prefix = "home_")
private Location homeAddress;
@JsonUnwrapped(prefix = "work_")
private Location workAddress;
// 输出:{"home_latitude":"...","home_longitude":"...","work_latitude":"...","work_longitude":"..."}
@JsonRootName 根节点包装
默认情况下,序列化对象直接输出其字段。如果希望整个对象被一个根节点包裹,可以使用 @JsonRootName 注解,并开启 SerializationFeature.WRAP_ROOT_VALUE。
java
@JsonRootName("user")
public class UserDto {
private Long id;
private String name;
// getter / setter 省略
}
ObjectMapper mapper = new ObjectMapper();
mapper.enable(SerializationFeature.WRAP_ROOT_VALUE);
String json = mapper.writeValueAsString(new UserDto(1L, "张三"));
// 输出:{"user":{"id":1,"name":"张三"}}
// 反序列化时需要开启 UNWRAP_ROOT_VALUE
mapper.enable(DeserializationFeature.UNWRAP_ROOT_VALUE);
UserDto parsed = mapper.readValue(json, UserDto.class);
自定义序列化器 JsonSerializer
当内置序列化器无法满足需求时,可以实现自定义的 JsonSerializer。常见场景包括:将 BigDecimal 输出为分单位的整数、将敏感字段脱敏、将枚举按业务规则映射等。
自定义序列化器需要继承 JsonSerializer,重写 serialize 方法。然后通过 @JsonSerialize(using = ...) 注解将其绑定到字段或类上。
java
public class MaskingSerializer extends JsonSerializer<String> {
@Override
public void serialize(String value, JsonGenerator gen, SerializerProvider provider)
throws IOException {
if (value == null || value.length() <= 4) {
gen.writeString("***");
return;
}
String masked = value.substring(0, 2)
+ "****"
+ value.substring(value.length() - 2);
gen.writeString(masked);
}
}
public class Contact {
private Long id;
@JsonSerialize(using = MaskingSerializer.class)
private String phone;
@JsonSerialize(using = MaskingSerializer.class)
private String idCard;
// getter / setter 省略
}
ObjectMapper mapper = new ObjectMapper();
Contact contact = new Contact();
contact.setId(1L);
contact.setPhone("13800138000");
contact.setIdCard("440106199001011234");
String json = mapper.writeValueAsString(contact);
// 输出:{"id":1,"phone":"13****00","idCard":"44****34"}
自定义反序列化器 JsonDeserializer
自定义反序列化器继承 JsonDeserializer,重写 deserialize 方法。常见场景包括:将多种日期格式统一解析、将字符串数字转为 BigDecimal、根据业务规则校验并转换字段。
下面示例展示一个能够兼容多种日期格式的反序列化器。
java
public class MultiFormatDateDeserializer extends JsonDeserializer<Date> {
private static final String[] PATTERNS = {
"yyyy-MM-dd HH:mm:ss",
"yyyy/MM/dd HH:mm:ss",
"yyyy-MM-dd",
"yyyy/MM/dd",
"MM/dd/yyyy"
};
@Override
public Date deserialize(JsonParser p, DeserializationContext ctxt)
throws IOException {
String text = p.getText();
if (text == null || text.isEmpty()) {
return null;
}
// 纯数字视为时间戳
if (text.matches("\\d+")) {
return new Date(Long.parseLong(text));
}
for (String pattern : PATTERNS) {
try {
return new SimpleDateFormat(pattern).parse(text);
} catch (ParseException ignored) {
// 尝试下一种格式
}
}
throw ctxt.weirdStringException(text, Date.class, "不支持的日期格式");
}
}
public class LogEntry {
private String level;
@JsonDeserialize(using = MultiFormatDateDeserializer.class)
private Date timestamp;
private String message;
// getter / setter 省略
}
ObjectMapper mapper = new ObjectMapper();
LogEntry entry = mapper.readValue(
"{\"level\":\"INFO\",\"timestamp\":\"2024/06/01 14:30:00\",\"message\":\"started\"}",
LogEntry.class
);
// entry.getTimestamp() 被正确解析为 Date 对象
@JsonSerialize 与 @JsonDeserialize 高级用法
除了 using 属性指定序列化器类,这两个注解还支持 as 属性(指定序列化时使用的视图类型)、contentAs / keyAs(用于集合与 Map 的元素/键类型转换)、converter 属性(指定 Converter 做轻量级转换)。
Converter 介于注解与完整序列化器之间,适合简单的双向转换。它需要实现 Converter<I, O> 接口,同时定义正向与反向转换逻辑。
java
public class CentsToYuanConverter implements Converter<Long, BigDecimal> {
@Override
public BigDecimal convert(Long value) {
return value == null ? null
: BigDecimal.valueOf(value).movePointLeft(2);
}
@Override
public JavaType getInputType(TypeFactory typeFactory) {
return typeFactory.constructType(Long.class);
}
@Override
public JavaType getOutputType(TypeFactory typeFactory) {
return typeFactory.constructType(BigDecimal.class);
}
}
public class Wallet {
private Long id;
@JsonSerialize(converter = CentsToYuanConverter.class)
private Long balanceInCents;
// getter / setter 省略
}
Wallet wallet = new Wallet();
wallet.setId(1L);
wallet.setBalanceInCents(19999L);
String json = new ObjectMapper().writeValueAsString(wallet);
// 输出:{"id":1,"balanceInCents":199.99}
视图与高级特性
本节介绍 Jackson 的视图机制、动态过滤、动态字段处理等高级特性。这些功能在需要根据调用方权限或场景输出不同字段时非常有用。
@JsonView 视图过滤
@JsonView 允许同一个对象在不同场景下输出不同的字段集合。其原理是定义若干视图类(通常是空接口),在字段上标注所属视图,序列化时通过 writerWithView 指定激活的视图。
视图支持继承:子视图包含父视图的所有字段。下面示例定义了 Public 与 Internal 两个视图,Internal 继承自 Public。
java
public class Views {
public static class Public {}
public static class Internal extends Public {}
}
public class Article {
@JsonView(Views.Public.class)
private Long id;
@JsonView(Views.Public.class)
private String title;
@JsonView(Views.Public.class)
private String summary;
@JsonView(Views.Internal.class)
private String content;
@JsonView(Views.Internal.class)
private String auditNote;
// getter / setter 省略
}
ObjectMapper mapper = new ObjectMapper();
Article article = new Article();
article.setId(1L);
article.setTitle("Jackson 教程");
article.setSummary("入门到进阶");
article.setContent("完整内容...");
article.setAuditNote("已审核");
// 公开视图:只输出 Public 字段
String publicJson = mapper
.writerWithView(Views.Public.class)
.writeValueAsString(article);
// 输出:{"id":1,"title":"Jackson 教程","summary":"入门到进阶"}
// 内部视图:输出 Public + Internal 字段
String internalJson = mapper
.writerWithView(Views.Internal.class)
.writeValueAsString(article);
// 输出:{"id":1,"title":"Jackson 教程","summary":"入门到进阶","content":"完整内容...","auditNote":"已审核"}
@JsonFilter 动态过滤
@JsonView 需要预先定义视图类,灵活性有限。@JsonFilter 提供了运行时动态决定输出字段的能力。使用步骤是:在类上标注 @JsonFilter("名称"),然后通过 FilterProvider 注册一个 SimpleBeanPropertyFilter。
java
@JsonFilter("userFilter")
public class Employee {
private Long id;
private String name;
private String salary;
private String idCard;
private String phone;
// getter / setter 省略
}
ObjectMapper mapper = new ObjectMapper();
// 只输出 id 与 name
FilterProvider filter = new SimpleFilterProvider()
.addFilter("userFilter",
SimpleBeanPropertyFilter.filterOutAllExcept("id", "name"));
String json = mapper.setFilterProvider(filter)
.writeValueAsString(employee);
// 输出:{"id":1,"name":"张三"}
// 排除敏感字段
FilterProvider sensitiveFilter = new SimpleFilterProvider()
.addFilter("userFilter",
SimpleBeanPropertyFilter.serializeAllExcept("idCard", "salary"));
String safeJson = mapper.setFilterProvider(sensitiveFilter)
.writeValueAsString(employee);
// 输出:{"id":1,"name":"张三","phone":"138****0000"}
SimpleBeanPropertyFilter 提供了多种内置策略:filterOutAllExcept(只保留指定字段)、serializeAllExcept(排除指定字段)、serializeAll(全部序列化)。
@JsonAnyGetter 与 @JsonAnySetter 动态字段
当 JSON 中包含未知字段,或 Java 对象需要保留任意键值对时,可以使用 @JsonAnyGetter 与 @JsonAnySetter。前者将 Map 的内容平铺到 JSON 顶层,后者将 JSON 中未匹配的字段收入 Map。
java
public class DynamicConfig {
private String name;
private Map<String, Object> extras = new HashMap<>();
public String getName() { return name; }
public void setName(String name) { this.name = name; }
@JsonAnyGetter
public Map<String, Object> getExtras() {
return extras;
}
@JsonAnySetter
public void addExtra(String key, Object value) {
extras.put(key, value);
}
}
ObjectMapper mapper = new ObjectMapper();
// 反序列化:未知字段进入 extras
DynamicConfig config = mapper.readValue(
"{\"name\":\"app\",\"timeout\":30,\"retries\":3,\"debug\":true}",
DynamicConfig.class
);
// config.getExtras() = {timeout=30, retries=3, debug=true}
// 序列化:extras 中的键值被平铺到顶层
String json = mapper.writeValueAsString(config);
// 输出:{"name":"app","timeout":30,"retries":3,"debug":true}
@JsonRawValue 原始 JSON 输出
某些场景下,字段值本身就是一段 JSON 字符串,希望序列化时直接作为 JSON 结构输出,而不是被当作普通字符串加引号转义。@JsonRawValue 实现了这一功能。
java
public class Widget {
private String name;
@JsonRawValue
private String config;
// getter / setter 省略
}
Widget widget = new Widget();
widget.setName("chart");
widget.setConfig("{\"type\":\"bar\",\"width\":600}");
String json = new ObjectMapper().writeValueAsString(widget);
// 输出:{"name":"chart","config":{"type":"bar","width":600}}
// config 的值被当作原始 JSON 嵌入,而不是字符串
@JsonMerge 合并反序列化
@JsonMerge 标注在字段上,表示反序列化时如果目标对象已有值,则将 JSON 内容合并到现有对象中,而不是覆盖。这对于嵌套对象的局部更新非常有用。
java
public class Profile {
private String nickname;
private String avatar;
private Map<String, String> settings = new HashMap<>();
@JsonMerge
public void setSettings(Map<String, String> settings) {
this.settings.putAll(settings);
}
// 其他 getter / setter 省略
}
ObjectMapper mapper = new ObjectMapper();
Profile profile = new Profile();
profile.setNickname("old-name");
profile.setAvatar("old.png");
profile.getSettings().put("theme", "light");
profile.getSettings().put("lang", "zh");
// 只更新部分字段,settings 会被合并而不是替换
mapper.readerForUpdating(profile)
.readValue("{\"nickname\":\"new-name\",\"settings\":{\"theme\":\"dark\"}}");
// profile.nickname = "new-name"
// profile.avatar = "old.png"(未更新)
// profile.settings = {theme=dark, lang=zh}(theme 被更新,lang 保留)
配置与最佳实践
ObjectMapper 提供了大量的配置项,分布在 SerializationFeature、DeserializationFeature、MapperFeature、JsonParser.Feature、JsonGenerator.Feature 等枚举中。本节介绍生产环境常用的配置项与最佳实践。
序列化特性 SerializationFeature
java
ObjectMapper mapper = new ObjectMapper();
// 美化输出(生产环境通常关闭,仅调试时开启)
mapper.enable(SerializationFeature.INDENT_OUTPUT);
// null 值不输出
mapper.setSerializationInclusion(JsonInclude.Include.NON_NULL);
// Date 输出为时间戳(默认 true,设为 false 配合 @JsonFormat 输出字符串)
mapper.disable(SerializationFeature.WRITE_DATES_AS_TIMESTAMPS);
// 枚举按 toString() 输出(默认按 name())
mapper.enable(SerializationFeature.WRITE_ENUMS_USING_TO_STRING);
// 空对象不报错
mapper.disable(SerializationFeature.FAIL_ON_EMPTY_BEANS);
反序列化特性 DeserializationFeature
java
ObjectMapper mapper = new ObjectMapper();
// 未知字段不报错(强烈建议开启)
mapper.disable(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES);
// 数值溢出不报错(如 long 装入 int)
mapper.disable(DeserializationFeature.FAIL_ON_NUMBERS_FOR_ENUMS);
// 接受单个值作为数组(如 "tag" 与 ["tag"] 都能映射到 List)
mapper.enable(DeserializationFeature.ACCEPT_SINGLE_VALUE_AS_ARRAY);
// 接受空字符串作为 null
mapper.enable(DeserializationFeature.ACCEPT_EMPTY_STRING_AS_NULL_OBJECT);
// 枚举大小写不敏感
mapper.enable(MapperFeature.ACCEPT_CASE_INSENSITIVE_ENUMS);
注册模块与 JSR-310 时间支持
Jackson 2.13 默认不注册 Java 8 时间模块。如果使用 LocalDate、LocalDateTime 等类型,需要手动注册 JavaTimeModule。
java
ObjectMapper mapper = new ObjectMapper();
mapper.registerModule(new JavaTimeModule());
mapper.disable(SerializationFeature.WRITE_DATES_AS_TIMESTAMPS);
对应的依赖为 jackson-datatype-jsr310。其他常用模块包括 jackson-datatype-jdk8(Optional 支持)、jackson-datatype-guava、jackson-module-parameter-names(构造函数参数名推断)。
线程安全与复用
ObjectMapper 是线程安全的,配置完成后可被多线程并发使用。最佳实践是将 ObjectMapper 作为静态字段或 Spring Bean 单例注入,避免每次请求都创建新实例。重复创建不仅浪费资源,还会丢失已注册的模块与序列化器缓存。
java
public class JsonUtils {
private static final ObjectMapper MAPPER = new ObjectMapper();
static {
MAPPER.disable(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES);
MAPPER.setSerializationInclusion(JsonInclude.Include.NON_NULL);
MAPPER.registerModule(new JavaTimeModule());
MAPPER.disable(SerializationFeature.WRITE_DATES_AS_TIMESTAMPS);
}
public static String toJson(Object obj) {
try {
return MAPPER.writeValueAsString(obj);
} catch (JsonProcessingException e) {
throw new RuntimeException("序列化失败", e);
}
}
public static <T> T fromJson(String json, Class<T> clazz) {
try {
return MAPPER.readValue(json, clazz);
} catch (IOException e) {
throw new RuntimeException("反序列化失败", e);
}
}
public static <T> T fromJson(String json, TypeReference<T> typeRef) {
try {
return MAPPER.readValue(json, typeRef);
} catch (IOException e) {
throw new RuntimeException("反序列化失败", e);
}
}
}
注意 ObjectMapper 一旦开始使用就不应再修改配置(如调用 configure、registerModule 等方法)。如果需要不同配置的 ObjectMapper,应当创建新实例,或使用 copy() 方法基于现有实例创建副本后再修改。
java
ObjectMapper base = new ObjectMapper();
base.disable(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES);
// 创建副本用于特殊场景
ObjectMapper strict = base.copy();
strict.enable(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES);
树模型 JsonNode
除了 POJO 绑定,Jackson 还提供了树模型 JsonNode,适合处理结构不固定或只需要访问部分字段的 JSON。JsonNode 是不可变的树结构,ObjectNode 与 ArrayNode 是其可变子类。
java
ObjectMapper mapper = new ObjectMapper();
// 解析为树
JsonNode root = mapper.readTree(json);
// 访问字段
String name = root.path("name").asText();
int age = root.path("age").asInt();
boolean hasAddress = root.has("address");
// 遍历数组
JsonNode items = root.path("items");
for (JsonNode item : items) {
String sku = item.path("sku").asText();
BigDecimal price = item.path("price").decimalValue();
}
// 构造 JSON 树
ObjectNode node = mapper.createObjectNode();
node.put("name", "张三");
node.put("age", 28);
ArrayNode tags = node.putArray("tags");
tags.add("vip").add("active");
String json = mapper.writeValueAsString(node);
// 输出:{"name":"张三","age":28,"tags":["vip","active"]}
树模型与 POJO 模型可以混合使用。例如先用 readTree 解析 JSON,再通过 treeToValue 将某个子节点转为 POJO;或用 valueToTree 将 POJO 转为 JsonNode 后再做局部修改。
java
// 树转 POJO
JsonNode userNode = root.path("user");
User user = mapper.treeToValue(userNode, User.class);
// POJO 转树
JsonNode userTree = mapper.valueToTree(user);
((ObjectNode) userTree).put("extraField", "extra");
常见问题与排查
本节汇总使用 Jackson 时常见的问题与排查思路。
UnrecognizedPropertyException
反序列化时,如果 JSON 包含 Java 类中不存在的字段,默认会抛出此异常。解决方案有两种:在类上标注 @JsonIgnoreProperties(ignoreUnknown = true),或全局配置 mapper.disable(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES)。生产环境推荐全局关闭,避免每个类都加注解。
InvalidDefinitionException
序列化时如果对象没有可访问的属性(如所有字段都是 private 且没有 getter),会抛出 No serializer found 错误。解决方案是提供 getter 方法、将字段改为 public、或使用 @JsonAutoDetect 调整可见性规则。Lombok 用户通常不会遇到此问题,因为 @Data 注解会自动生成 getter。
MismatchedInputException
反序列化时类型不匹配会抛出此异常。常见原因包括:JSON 是数组但目标类型是对象、JSON 字段是字符串但目标类型是数字、JSON 缺少必填字段等。排查时检查 JSON 结构与 POJO 字段类型是否一致,必要时使用 @JsonCreator 自定义反序列化逻辑。
日期解析失败
Date 与 LocalDateTime 类型的反序列化对格式要求严格。如果 JSON 中的日期格式与默认格式不匹配,会抛出异常。解决方案是为字段添加 @JsonFormat 注解指定格式,或注册 JavaTimeModule 并配置自定义反序列化器。对于多种日期格式并存的场景,建议使用前文示例中的 MultiFormatDateDeserializer。
性能优化建议
ObjectMapper 复用是性能优化的首要原则。除此之外,可以关注以下几点:避免在序列化路径中使用反射过重的特性(如 @JsonTypeInfo.Id.CLASS)、对热点接口预编译 TypeReference、关闭调试用的 INDENT_OUTPUT、避免在循环中频繁调用 writeValueAsString(可考虑流式 API JsonGenerator)。对于极高吞吐场景,可以考虑使用 Jackson Afterburner 模块或 Blackbird 模块加速序列化。