JSON 字段丢失?Jackson @JsonIgnoreProperties 与 @JsonInclude 用法对比
大家好,今天我们来深入探讨一个在 Java JSON 序列化与反序列化过程中常见的问题:JSON 字段丢失。我们将聚焦于 Jackson 库,并通过对比 @JsonIgnoreProperties 和 @JsonInclude 这两个注解的使用,来分析问题的原因以及如何有效地解决它。
JSON 序列化与反序列化基础
在深入研究具体的注解之前,我们先简单回顾一下 JSON 序列化与反序列化的概念。
- 序列化 (Serialization): 将 Java 对象转换成 JSON 字符串的过程。
- 反序列化 (Deserialization): 将 JSON 字符串转换成 Java 对象的过程。
Jackson 是一个流行的 Java JSON 处理库,它提供了强大且灵活的功能,可以方便地进行序列化和反序列化操作。 然而,在实际应用中,我们经常会遇到一些问题,例如:
- 某些字段不需要序列化/反序列化: Java 对象中可能包含一些敏感信息或临时数据,我们不希望将它们包含在 JSON 字符串中。
- 某些字段的值为空: 为了减少 JSON 字符串的大小,我们希望忽略那些值为
null或空值的字段。 - JSON 字符串中包含 Java 对象中不存在的字段: 反序列化时,如果 JSON 字符串中包含 Java 对象中不存在的字段,可能会导致异常。
为了解决这些问题,Jackson 提供了多种注解,其中 @JsonIgnoreProperties 和 @JsonInclude 是两个非常重要的注解。
@JsonIgnoreProperties:忽略未知字段和指定字段
@JsonIgnoreProperties 注解用于在反序列化时忽略 JSON 字符串中 Java 对象中不存在的字段,或者在序列化和反序列化时忽略指定的字段。
1. 忽略未知字段
当 JSON 字符串中包含 Java 对象中不存在的字段时,默认情况下 Jackson 会抛出一个异常。为了避免这种情况,可以使用 @JsonIgnoreProperties(ignoreUnknown = true)。
import com.fasterxml.jackson.annotation.JsonIgnoreProperties;
@JsonIgnoreProperties(ignoreUnknown = true)
public class User {
private String name;
private int age;
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public int getAge() {
return age;
}
public void setAge(int age) {
this.age = age;
}
@Override
public String toString() {
return "User{" +
"name='" + name + ''' +
", age=" + age +
'}';
}
}
import com.fasterxml.jackson.databind.ObjectMapper;
public class Main {
public static void main(String[] args) throws Exception {
String json = "{"name":"Alice", "age":30, "address":"Beijing"}";
ObjectMapper mapper = new ObjectMapper();
User user = mapper.readValue(json, User.class);
System.out.println(user); // 输出: User{name='Alice', age=30}
}
}
在这个例子中,User 类没有 address 字段,但是 JSON 字符串中包含了这个字段。由于使用了 @JsonIgnoreProperties(ignoreUnknown = true),所以 Jackson 会忽略 address 字段,而不会抛出异常。
2. 忽略指定字段
@JsonIgnoreProperties 还可以用于忽略指定的字段,通过 value 属性来指定要忽略的字段名称。
import com.fasterxml.jackson.annotation.JsonIgnoreProperties;
@JsonIgnoreProperties({"age"})
public class User {
private String name;
private int age;
private String password; //添加password字段
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public int getAge() {
return age;
}
public void setAge(int age) {
this.age = age;
}
public String getPassword() {
return password;
}
public void setPassword(String password) {
this.password = password;
}
@Override
public String toString() {
return "User{" +
"name='" + name + ''' +
", age=" + age +
", password='" + password + ''' +
'}';
}
}
import com.fasterxml.jackson.databind.ObjectMapper;
public class Main {
public static void main(String[] args) throws Exception {
ObjectMapper mapper = new ObjectMapper();
// 反序列化
String json = "{"name":"Bob", "age":25, "password":"123456"}";
User user = mapper.readValue(json, User.class);
System.out.println("反序列化: " + user); // 输出: User{name='Bob', age=0, password='123456'} age被忽略
// 序列化
User user2 = new User();
user2.setName("Charlie");
user2.setAge(35);
user2.setPassword("abcdef");
String json2 = mapper.writeValueAsString(user2);
System.out.println("序列化: " + json2); // 输出: {"name":"Charlie","password":"abcdef"} age被忽略
}
}
在这个例子中,@JsonIgnoreProperties({"age"}) 指定了忽略 age 字段。在反序列化时,JSON 字符串中的 age 字段会被忽略,User 对象的 age 字段的值为默认值 (0)。在序列化时,age 字段也不会被包含在 JSON 字符串中。
总结:
@JsonIgnoreProperties 注解的主要作用是:
ignoreUnknown = true:忽略 JSON 字符串中 Java 对象中不存在的字段,防止反序列化时抛出异常。value = {"field1", "field2", ...}:忽略指定的字段,这些字段不会被序列化或反序列化。
@JsonInclude:控制哪些字段被序列化
@JsonInclude 注解用于控制哪些字段被序列化到 JSON 字符串中。它可以根据字段的值来决定是否包含该字段。
@JsonInclude 注解接受一个 JsonInclude.Include 枚举值,该枚举值定义了不同的包含策略:
| 枚举值 | 描述 |
|---|---|
JsonInclude.Include.ALWAYS |
总是包含该字段,即使它的值为 null 或空值。这是默认值。 |
JsonInclude.Include.NON_NULL |
只有当字段的值不为 null 时才包含该字段。 |
JsonInclude.Include.NON_ABSENT |
对于 Optional 类型,只有当 Optional 对象存在值时才包含该字段。 对于其他类型,行为与 NON_NULL 相同。 |
JsonInclude.Include.NON_EMPTY |
只有当字段的值不为 null 且不为空 (例如,空字符串、空集合、空数组) 时才包含该字段。 |
JsonInclude.Include.NON_DEFAULT |
只有当字段的值不为默认值时才包含该字段。例如,int 类型的默认值为 0,boolean 类型的默认值为 false。 |
JsonInclude.Include.CUSTOM |
使用自定义的包含策略。需要指定一个 valueFilter 或 contentFilter 属性来定义自定义的过滤逻辑。 |
JsonInclude.Include.USE_DEFAULTS |
使用 Jackson 的全局默认配置。 |
1. 忽略 Null 值
import com.fasterxml.jackson.annotation.JsonInclude;
@JsonInclude(JsonInclude.Include.NON_NULL)
public class User {
private String name;
private Integer age; // 使用 Integer 而不是 int
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;
}
@Override
public String toString() {
return "User{" +
"name='" + name + ''' +
", age=" + age +
'}';
}
}
import com.fasterxml.jackson.databind.ObjectMapper;
public class Main {
public static void main(String[] args) throws Exception {
ObjectMapper mapper = new ObjectMapper();
User user = new User();
user.setName("David");
// user.setAge(null); age为null
String json = mapper.writeValueAsString(user);
System.out.println(json); // 输出: {"name":"David"} age被忽略,因为它为null
}
}
在这个例子中,@JsonInclude(JsonInclude.Include.NON_NULL) 指定了只有当字段的值不为 null 时才包含该字段。由于 age 字段的值为 null,所以在序列化时,该字段被忽略。 注意这里 age 的类型是 Integer 而不是 int,因为 int 类型不能为 null。
2. 忽略空字符串
import com.fasterxml.jackson.annotation.JsonInclude;
@JsonInclude(JsonInclude.Include.NON_EMPTY)
public class User {
private String name;
private String address;
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public String getAddress() {
return address;
}
public void setAddress(String address) {
this.address = address;
}
@Override
public String toString() {
return "User{" +
"name='" + name + ''' +
", address='" + address + ''' +
'}';
}
}
import com.fasterxml.jackson.databind.ObjectMapper;
public class Main {
public static void main(String[] args) throws Exception {
ObjectMapper mapper = new ObjectMapper();
User user = new User();
user.setName("Eve");
user.setAddress(""); //address为 空字符串
String json = mapper.writeValueAsString(user);
System.out.println(json); // 输出: {"name":"Eve"} address被忽略,因为它为空字符串
}
}
在这个例子中,@JsonInclude(JsonInclude.Include.NON_EMPTY) 指定了只有当字段的值不为 null 且不为空时才包含该字段。由于 address 字段的值为空字符串,所以在序列化时,该字段被忽略。
3. 忽略默认值
import com.fasterxml.jackson.annotation.JsonInclude;
@JsonInclude(JsonInclude.Include.NON_DEFAULT)
public class User {
private String name;
private int age; // int 类型的默认值为 0
private boolean active; // boolean类型的默认值为 false
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public int getAge() {
return age;
}
public void setAge(int age) {
this.age = age;
}
public boolean isActive() {
return active;
}
public void setActive(boolean active) {
this.active = active;
}
@Override
public String toString() {
return "User{" +
"name='" + name + ''' +
", age=" + age +
", active=" + active +
'}';
}
}
import com.fasterxml.jackson.databind.ObjectMapper;
public class Main {
public static void main(String[] args) throws Exception {
ObjectMapper mapper = new ObjectMapper();
User user = new User();
user.setName("Frank");
// user.setAge(0); age为默认值
// user.setActive(false); active为默认值
String json = mapper.writeValueAsString(user);
System.out.println(json); // 输出: {"name":"Frank"} age和active被忽略,因为它们为默认值
}
}
在这个例子中,@JsonInclude(JsonInclude.Include.NON_DEFAULT) 指定了只有当字段的值不为默认值时才包含该字段。由于 age 字段的值为 0 (int 类型的默认值),active 字段的值为 false (boolean 类型的默认值),所以在序列化时,这两个字段都被忽略。
4. 应用于类级别和字段级别
@JsonInclude 注解可以应用于类级别和字段级别。
- 类级别: 应用于整个类,指定该类的所有字段的默认包含策略。
- 字段级别: 应用于单个字段,覆盖类级别的包含策略。
import com.fasterxml.jackson.annotation.JsonInclude;
@JsonInclude(JsonInclude.Include.NON_NULL) // 类级别
public class User {
private String name;
private Integer age;
@JsonInclude(JsonInclude.Include.ALWAYS) // 字段级别,覆盖类级别的配置
private String address;
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;
}
public String getAddress() {
return address;
}
public void setAddress(String address) {
this.address = address;
}
@Override
public String toString() {
return "User{" +
"name='" + name + ''' +
", age=" + age +
", address='" + address + ''' +
'}';
}
}
import com.fasterxml.jackson.databind.ObjectMapper;
public class Main {
public static void main(String[] args) throws Exception {
ObjectMapper mapper = new ObjectMapper();
User user = new User();
user.setName("Grace");
user.setAge(null);
user.setAddress(null);
String json = mapper.writeValueAsString(user);
System.out.println(json); // 输出: {"name":"Grace","address":null} age被忽略,address没有被忽略
}
}
在这个例子中,@JsonInclude(JsonInclude.Include.NON_NULL) 应用于类级别,指定了该类的所有字段的默认包含策略为 NON_NULL。但是,address 字段使用了 @JsonInclude(JsonInclude.Include.ALWAYS),覆盖了类级别的配置,所以 address 字段总是会被包含在 JSON 字符串中,即使它的值为 null。
总结:
@JsonInclude 注解的主要作用是:
- 控制哪些字段被序列化到 JSON 字符串中,可以根据字段的值来决定是否包含该字段。
- 可以应用于类级别和字段级别,字段级别的配置会覆盖类级别的配置。
- 常用的
JsonInclude.Include枚举值包括NON_NULL、NON_EMPTY和NON_DEFAULT。
@JsonIgnoreProperties 与 @JsonInclude 的对比
| 特性 | @JsonIgnoreProperties | @JsonInclude |
|---|---|---|
| 主要作用 | 忽略指定的字段或忽略 JSON 字符串中 Java 对象中不存在的字段。 | 控制哪些字段被序列化到 JSON 字符串中,可以根据字段的值来决定是否包含该字段。 |
| 应用场景 | 反序列化时,忽略 JSON 字符串中 Java 对象中不存在的字段,防止抛出异常。 序列化和反序列化时,忽略指定的字段,例如敏感信息或临时数据。 | 序列化时,忽略值为 null 的字段,减少 JSON 字符串的大小。 序列化时,忽略值为默认值的字段,减少 JSON 字符串的大小。 * 根据自定义的逻辑来决定是否包含某个字段。 |
| 影响 | 既影响序列化,又影响反序列化。被忽略的字段不会被序列化到 JSON 字符串中,也不会从 JSON 字符串反序列化到 Java 对象中。 | 主要影响序列化,对反序列化的影响较小。它只控制哪些字段被序列化到 JSON 字符串中,不会阻止从 JSON 字符串反序列化到 Java 对象中。 |
| 配置方式 | ignoreUnknown = true:忽略 JSON 字符串中 Java 对象中不存在的字段。 value = {"field1", "field2", ...}:忽略指定的字段。 |
JsonInclude.Include 枚举值:指定包含策略,例如 NON_NULL、NON_EMPTY 和 NON_DEFAULT。 valueFilter 或 contentFilter 属性:指定自定义的过滤逻辑。 |
| 作用范围 | 可以应用于类级别和字段级别。 | 可以应用于类级别和字段级别,字段级别的配置会覆盖类级别的配置。 |
| 侧重点 | 忽略不需要的字段。 | 控制哪些字段被包含。 |
选择建议:
- 如果你的目标是忽略某些字段,使它们完全不参与序列化和反序列化,那么应该使用
@JsonIgnoreProperties。 - 如果你的目标是根据字段的值来决定是否包含该字段,并且主要关注序列化过程,那么应该使用
@JsonInclude。 - 如果需要同时忽略一些字段,并且根据字段的值来控制包含哪些字段,可以同时使用这两个注解。
实际应用场景
1. API 接口数据传输
在 API 接口的数据传输过程中,我们通常需要将 Java 对象转换成 JSON 字符串,并发送给客户端。为了减少数据传输量,可以 使用 @JsonInclude 注解来忽略值为 null 或空值的字段。
@JsonInclude(JsonInclude.Include.NON_NULL)
public class ApiResponse<T> {
private int code;
private String message;
private T data;
// 省略 getter 和 setter 方法
}
2. 数据库实体类
在数据库实体类中,我们可能需要忽略一些字段,例如密码或敏感信息。可以使用 @JsonIgnoreProperties 注解来忽略这些字段,防止它们被序列化到 JSON 字符串中。
@JsonIgnoreProperties({"password"})
public class User {
private String username;
private String password;
// 省略 getter 和 setter 方法
}
3. 前后端数据交互
在前后端数据交互过程中,前端可能需要向后端传递一些数据,后端需要将这些数据转换成 Java 对象。如果前端传递的 JSON 字符串中包含后端 Java 对象中不存在的字段,可以使用 @JsonIgnoreProperties(ignoreUnknown = true) 来忽略这些字段,防止反序列化时抛出异常。
@JsonIgnoreProperties(ignoreUnknown = true)
public class User {
private String name;
private int age;
// 省略 getter 和 setter 方法
}
容易忽略的点
intvsInteger: 在使用@JsonInclude(JsonInclude.Include.NON_NULL)时,需要注意基本数据类型 (例如int,boolean,double) 不能为null。因此,如果需要忽略值为null的字段,应该使用对应的包装类型 (例如Integer,Boolean,Double)。- 集合和数组:
@JsonInclude(JsonInclude.Include.NON_EMPTY)不仅可以忽略空字符串,还可以忽略空集合和空数组。 - 自定义序列化器和反序列化器: 如果使用了自定义的序列化器和反序列化器,
@JsonIgnoreProperties和@JsonInclude注解可能会失效。需要确保自定义的序列化器和反序列化器也能够正确地处理这些字段。 - Jackson 版本: 某些
@JsonInclude.Include枚举值是在较新的 Jackson 版本中才引入的。如果使用的是较旧的 Jackson 版本,可能会出现编译错误或运行时异常。
几个关键点总结
@JsonIgnoreProperties用于忽略字段,控制哪些字段不参与序列化和反序列化。@JsonInclude用于控制哪些字段被序列化,基于字段的值决定是否包含。- 合理选择和使用这两个注解,可以有效地解决 JSON 字段丢失的问题,并提高数据传输的效率和安全性。