JSON 输出属性丢失?@JsonProperty 与 @JsonSetter 配置讲解
大家好,今天我们来探讨一个在 Java JSON 处理中常见的困境:JSON 序列化(输出)时,某些属性意外丢失。我们将深入研究导致这种现象的原因,并重点讲解 Jackson 库中的 @JsonProperty 和 @JsonSetter 注解,以及它们如何帮助我们解决这个问题。
问题复现:属性丢失的场景
在 Java 应用中,我们经常使用 JSON 作为数据交换的格式。Jackson 是一个流行的 JSON 处理库,它提供了强大的序列化和反序列化功能。然而,在使用 Jackson 的过程中,我们可能会遇到这样的情况:Java 对象中的某些属性在转换为 JSON 字符串时,并没有出现在输出结果中。
例如,考虑以下 User 类:
public class User {
private String firstName;
private String lastName;
private int age;
public String getFirstName() {
return firstName;
}
public void setFirstName(String firstName) {
this.firstName = firstName;
}
public String getLastName() {
return lastName;
}
public void setLastName(String lastName) {
this.lastName = lastName;
}
public int getAge() {
return age;
}
public void setAge(int age) {
this.age = age;
}
@Override
public String toString() {
return "User{" +
"firstName='" + firstName + ''' +
", lastName='" + lastName + ''' +
", age=" + age +
'}';
}
}
如果我们使用 Jackson 将这个 User 对象序列化为 JSON,一切正常:
import com.fasterxml.jackson.databind.ObjectMapper;
public class Main {
public static void main(String[] args) throws Exception {
User user = new User();
user.setFirstName("John");
user.setLastName("Doe");
user.setAge(30);
ObjectMapper objectMapper = new ObjectMapper();
String json = objectMapper.writeValueAsString(user);
System.out.println(json); // 输出: {"firstName":"John","lastName":"Doe","age":30}
}
}
但假设我们稍微修改一下 User 类,将 firstName 属性的 getter 方法命名为 getFname (注意 firstName 与 fname 的区别):
public class User {
private String firstName;
private String lastName;
private int age;
public String getFname() { // 修改了 getter 方法名
return firstName;
}
public void setFirstName(String firstName) {
this.firstName = firstName;
}
public String getLastName() {
return lastName;
}
public void setLastName(String lastName) {
this.lastName = lastName;
}
public int getAge() {
return age;
}
public void setAge(int age) {
this.age = age;
}
@Override
public String toString() {
return "User{" +
"firstName='" + firstName + ''' +
", lastName='" + lastName + ''' +
", age=" + age +
'}';
}
}
再次运行相同的序列化代码:
import com.fasterxml.jackson.databind.ObjectMapper;
public class Main {
public static void main(String[] args) throws Exception {
User user = new User();
user.setFirstName("John");
user.setLastName("Doe");
user.setAge(30);
ObjectMapper objectMapper = new ObjectMapper();
String json = objectMapper.writeValueAsString(user);
System.out.println(json); // 输出: {"lastName":"Doe","age":30} firstName 丢失了!
}
}
可以看到,JSON 输出中 firstName 属性消失了。这就是我们今天要解决的问题。
属性丢失的原因:Jackson 的默认行为
Jackson 默认使用基于 JavaBeans 规范的属性发现机制。这意味着它通过查找符合 getXXX() 和 isXXX() 模式的方法来确定哪些属性应该被序列化。
-
Getter 方法:
Jackson会查找以get开头,后跟大写字母的方法(如getFirstName),或者对于boolean类型,以is开头,后跟大写字母的方法(如isActive)。Jackson会将get或is后面的大写字母转换为小写,作为json的属性名。例如getFirstName对应firstName。 -
Setter 方法:
Jackson会查找以set开头,后跟大写字母的方法(如setFirstName)。
如果一个属性只有 setter 方法而没有 getter 方法,或者 getter 方法不符合命名规范,Jackson 默认不会将其序列化到 JSON 中。在上面的例子中,由于我们将 firstName 的 getter 方法命名为 getFname,Jackson 无法识别它是一个标准的 getter 方法,因此忽略了 firstName 属性。
使用 @JsonProperty 解决属性丢失问题
@JsonProperty 注解允许我们显式地指定 JSON 属性的名称,并将其与 Java 类的字段或方法关联起来。 它可以作用在字段上,也可以作用在getter和setter方法上。
1. 在字段上使用 @JsonProperty
我们可以直接在 firstName 字段上使用 @JsonProperty 注解,指定它对应的 JSON 属性名称为 "firstName":
import com.fasterxml.jackson.annotation.JsonProperty;
public class User {
@JsonProperty("firstName")
private String firstName;
private String lastName;
private int age;
public String getFname() { // 修改了 getter 方法名
return firstName;
}
public void setFirstName(String firstName) {
this.firstName = firstName;
}
public String getLastName() {
return lastName;
}
public void setLastName(String lastName) {
this.lastName = lastName;
}
public int getAge() {
return age;
}
public void setAge(int age) {
this.age = age;
}
@Override
public String toString() {
return "User{" +
"firstName='" + firstName + ''' +
", lastName='" + lastName + ''' +
", age=" + age +
'}';
}
}
现在,即使 getter 方法的名称不符合规范,Jackson 也会根据 @JsonProperty 注解将 firstName 属性序列化到 JSON 中。
2. 在 Getter 方法上使用 @JsonProperty
另一种方法是在 getter 方法上使用 @JsonProperty 注解:
import com.fasterxml.jackson.annotation.JsonProperty;
public class User {
private String firstName;
private String lastName;
private int age;
@JsonProperty("firstName")
public String getFname() { // 修改了 getter 方法名
return firstName;
}
public void setFirstName(String firstName) {
this.firstName = firstName;
}
public String getLastName() {
return lastName;
}
public void setLastName(String lastName) {
this.lastName = lastName;
}
public int getAge() {
return age;
}
public void setAge(int age) {
this.age = age;
}
@Override
public String toString() {
return "User{" +
"firstName='" + firstName + ''' +
", lastName='" + lastName + ''' +
", age=" + age +
'}';
}
}
这两种方法的效果是相同的。 推荐在getter方法上添加,这样可以更清晰地表达属性的序列化行为。
@JsonProperty 的作用
| 作用域 | 描述 |
|---|---|
| 字段 | 指定该字段对应的 JSON 属性名称。 即使没有标准的 getter 方法,Jackson 也会根据 @JsonProperty 注解将该字段序列化到 JSON 中. |
| Getter | 指定该 getter 方法返回的值对应的 JSON 属性名称。 Jackson 会调用该 getter 方法获取属性值,并将其序列化到 JSON 中. 即使getter方法名称不符合规范,也会生效。 |
| Setter | 指定该 setter 方法用于设置哪个 JSON 属性的值。 Jackson 会使用该 setter 方法将 JSON 中的属性值设置到 Java 对象的相应字段中。 即使setter方法名称不符合规范,也会生效。 如果只添加了setter的JsonProperty,而没有getter的JsonProperty,那么序列化的时候,这个属性依然不会出现在json中。反序列化时会生效。 |
使用 @JsonSetter 进行反序列化配置
@JsonSetter 注解主要用于反序列化,它允许我们指定在反序列化 JSON 数据时,哪个 setter 方法应该用于设置特定的 JSON 属性值。 当setter方法名称不符合规范时,这个注解非常有用。
例如,假设我们接收到的 JSON 数据中,firstName 属性的名称是 "fname",而不是 "firstName"。 我们的 User 类的 setFirstName 方法仍然使用 "firstName" 作为参数名。 我们可以使用 @JsonSetter 注解来解决这个问题:
import com.fasterxml.jackson.annotation.JsonSetter;
public class User {
private String firstName;
private String lastName;
private int age;
public String getFname() {
return firstName;
}
@JsonSetter("fname")
public void setFirstName(String firstName) {
this.firstName = firstName;
}
public String getLastName() {
return lastName;
}
public void setLastName(String lastName) {
this.lastName = lastName;
}
public int getAge() {
return age;
}
public void setAge(int age) {
this.age = age;
}
@Override
public String toString() {
return "User{" +
"firstName='" + firstName + ''' +
", lastName='" + lastName + ''' +
", age=" + age +
'}';
}
}
现在,当我们从 JSON 字符串反序列化 User 对象时,Jackson 会将 "fname" 属性的值传递给 setFirstName 方法。
@JsonSetter 的作用
| 作用域 | 描述 |
|---|---|
| Setter | 指定该 setter 方法用于设置哪个 JSON 属性的值。 Jackson 会使用该 setter 方法将 JSON 中的属性值设置到 Java 对象的相应字段中。 即使setter方法名称不符合规范,也会生效。 |
@JsonProperty 与 @JsonSetter 的联合使用
在一些复杂的场景中,我们可能需要同时使用 @JsonProperty 和 @JsonSetter 注解。例如,我们可能需要将 Java 类的属性名与 JSON 属性名完全解耦,并且 getter 和 setter 方法的命名都不符合 JavaBeans 规范。
考虑以下情况:
- Java 类中的属性名为
internalFirstName - JSON 中的属性名为
first_name - getter 方法名为
getFirstNameValue - setter 方法名为
setTheFirstName
我们可以这样配置:
import com.fasterxml.jackson.annotation.JsonProperty;
import com.fasterxml.jackson.annotation.JsonSetter;
public class User {
private String internalFirstName;
private String lastName;
private int age;
@JsonProperty("first_name")
public String getFirstNameValue() {
return internalFirstName;
}
@JsonSetter("first_name")
public void setTheFirstName(String firstName) {
this.internalFirstName = firstName;
}
public String getLastName() {
return lastName;
}
public void setLastName(String lastName) {
this.lastName = lastName;
}
public int getAge() {
return age;
}
public void setAge(int age) {
this.age = age;
}
@Override
public String toString() {
return "User{" +
"firstName='" + internalFirstName + ''' +
", lastName='" + lastName + ''' +
", age=" + age +
'}';
}
}
在这个例子中,@JsonProperty("first_name") 注解确保 getFirstNameValue 方法返回的值在序列化为 JSON 时,对应的属性名为 "first_name"。@JsonSetter("first_name") 注解确保在反序列化 JSON 数据时,"first_name" 属性的值会被传递给 setTheFirstName 方法。
总结:理解 Jackson 属性映射机制并灵活使用注解
总而言之,Jackson 默认使用 JavaBeans 规范进行属性发现。如果属性的 getter 或 setter 方法不符合规范,或者我们需要自定义 JSON 属性名,可以使用 @JsonProperty 和 @JsonSetter 注解来显式地指定属性映射关系。 理解 Jackson 的属性映射机制,并灵活使用这两个注解,可以帮助我们有效地解决 JSON 序列化和反序列化过程中遇到的属性丢失问题,并实现更灵活的数据绑定。