
本文旨在解决使用 jackson 反序列化 json 数据时,当多个字段可能表示同一信息,且其中部分字段可能为 `null` 或空字符串时,如何优先选择非空值的挑战。我们将探讨两种核心策略:通过定义多个智能 `setter` 方法并结合 `@jsonsetter` 注解,以及利用自定义 `converter` 结合辅助 pojo 和 `@jsondeserialize` 注解,实现灵活且健壮的数据映射,确保数据完整性。
在 Spring Boot 应用中,使用 Jackson 处理来自第三方服务或遗留系统的 JSON 数据时,我们经常会遇到 JSON 结构冗余的问题。例如,一个逻辑上的“名称”信息可能在 JSON 中以 name、full_name 或 fullName 等多个字段表示。更复杂的是,这些字段可能并非总是同时存在或非空,有时只有一个字段包含有效数据,而其他字段为 null 或空字符串。Jackson 的 @JsonAlias 注解虽然可以处理多个别名,但它通常不会智能地忽略 null 或空值,而是按照其内部优先级选择一个值,这可能导致最终 POJO 字段被 null 或空值覆盖。
本教程将详细介绍两种有效策略,以确保 Jackson 在这种场景下能够优先选择非 null 或非空字符串的字段值。
1. 使用多个 @JsonSetter 方法
第一种方法是为每个可能包含目标信息的 JSON 字段定义一个独立的 setter 方法,并使用 @JsonSetter 注解将其映射到对应的 JSON 属性名。核心思想是在这些 setter 方法中引入逻辑,仅当目标 POJO 字段当前为 null 或空时才更新其值。
实现步骤
- 定义判空/判空字符串的 Predicate: 为了避免重复逻辑,可以定义一个静态的 Predicate 来检查字符串是否为 null 或空。
- 主 setter 方法: 为 POJO 字段定义一个标准 setter,并在其中包含更新逻辑。
- 辅助 setter 方法: 为每个别名 JSON 字段定义一个额外的 setter 方法,使用 @JsonSetter 注解将其映射到对应的 JSON 属性,并委托给主 setter 方法。
示例代码
import com.fasterxml.jackson.annotation.JsonSetter;
import com.fasterxml.jackson.databind.ObjectMapper;
import java.util.function.Predicate;
public class MyPojo {
// 定义一个谓词,用于检查字符串是否为null或空
public static final Predicate NULL_OR_EMPTY =
s -> s == null || s.isEmpty();
private String name;
// 主setter方法,包含更新逻辑
@JsonSetter("name")
public void setName(String name) {
// 只有当当前name字段为null或空时才更新
if (NULL_OR_EMPTY.test(this.name)) {
this.name = name;
}
}
// 辅助setter方法,映射到"full_name",并委托给主setName方法
@JsonSetter("full_name")
public void setFullNameAlias(String name) {
setName(name);
}
// 辅助setter方法,映射到"fullName",并委托给主setName方法
@JsonSetter("fullName")
public void setCamelCaseFullNameAlias(String name) {
setName(name);
}
// Getter和toString方法,便于测试
public String getName() {
return name;
}
@Override
public String toString() {
return "MyPojo{name='" + name + "'}";
}
// 示例用法
public static void main(String[] args) throws Exception {
String json1 = "{ \"name\" : null, \"full_name\" : \"\", \"fullName\" : \"John Doe\"}";
String json2 = "{ \"name\" : \"Jane Doe\", \"full_name\" : \"\", \"fullName\" : null}";
String json3 = "{ \"name\" : null, \"full_name\" : \"Another Name\", \"fullName\" : null}";
ObjectMapper mapper = new ObjectMapper();
MyPojo myPojo1 = mapper.readValue(json1, MyPojo.class);
System.out.println("JSON 1 Output: " + myPojo1); // Expected: MyPojo{name='John Doe'}
MyPojo myPojo2 = mapper.readValue(json2, MyPojo.class);
System.out.println("JSON 2 Output: " + myPojo2); // Expected: MyPojo{name='Jane Doe'}
MyPojo myPojo3 = mapper.readValue(json3, MyPojo.class);
System.out.println("JSON 3 Output: " + myPojo3); // Expected: MyPojo{name='Another Name'}
}
} 注意事项与优缺点
- 优点: 实现相对直接,不需要额外创建辅助类。
- 缺点: 这种方法在领域模型中引入了“智能 setter”,这可能违反单一职责原则(setter 通常不应包含复杂的验证或条件逻辑)。此外,如果有很多冗余属性,POJO 类会因为额外的 setter 方法而变得臃肿。
2. 使用自定义 Converter
第二种更优雅的解决方案是利用 Jackson 的 Converter 机制。Converter 允许我们在反序列化过程中将一种 POJO 类型转换为另一种 POJO 类型。这种方法将复杂的选择逻辑从领域模型中分离出来,提高了代码的清晰度和可维护性。
实现步骤
- 定义辅助 POJO: 创建一个辅助 POJO,其字段直接对应 JSON 中所有可能的冗余属性。这个 POJO 仅仅用于临时捕获原始 JSON 数据。
- 创建自定义 Converter: 实现 com.fasterxml.jackson.databind.util.StdConverter 抽象类,指定从辅助 POJO 到目标 POJO 的转换逻辑。在这个 Converter 中,实现选择第一个非 null 非空值的逻辑。
- 在目标 POJO 上应用 Converter: 使用 @JsonDeserialize(converter = YourConverter.class) 注解将自定义 Converter 应用到目标 POJO 类上。
Converter 与 Deserializer 的区别
- Deserializer: 负责根据 JsonParser 中的原始 JSON 令牌构建 POJO 实例。它处理底层 JSON 结构到 Java 对象的映射。
- Converter: 在一个 POJO 已经反序列化完成后,将其转换为另一个 POJO。它用于数据转换,而不是直接构建对象。
示例代码
import com.fasterxml.jackson.annotation.JsonProperty;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.databind.annotation.JsonDeserialize;
import com.fasterxml.jackson.databind.util.StdConverter;
import lombok.Builder;
import lombok.Getter;
import java.util.Arrays;
import java.util.function.Predicate;
// 1. 定义辅助POJO,用于捕获所有可能的JSON字段
// 使用record简化数据类定义,需要Java 16+
record AuxiliaryPojo(
@JsonProperty("name") String name,
@JsonProperty("full_name") String name1,
@JsonProperty("fullName") String name2
) {}
// 2. 创建自定义Converter
public class AuxiliaryPojoToMyPojoConverter extends StdConverter {
public static final Predicate NOT_NULL_OR_EMPTY =
s -> s != null && !s.isEmpty();
@Override
public MyPojo convert(AuxiliaryPojo v) {
// 在转换逻辑中,查找第一个非null且非空的字符串
String finalName = findMatching(v.name(), v.name1(), v.name2());
// 使用Builder模式创建MyPojo实例
return MyPojo.builder().name(finalName).build();
}
// 辅助方法,用于从多个字符串中查找第一个非null非空值
private String findMatching(String... args) {
return Arrays.stream(args)
.filter(NOT_NULL_OR_EMPTY)
.findFirst()
.orElse(null); // 如果所有都为null或空,则返回null
}
}
// 3. 目标POJO,应用Converter
@Getter
@Builder
@JsonDeserialize(converter = AuxiliaryPojoToMyPojoConverter.class)
public static class MyPojo {
private String name;
@Override
public String toString() {
return "MyPojo{name='" + name + "'}";
}
// 示例用法
public static void main(String[] args) throws Exception {
String json1 = "{ \"name\" : null, \"full_name\" : \"\", \"fullName\" : \"John Doe\"}";
String json2 = "{ \"name\" : \"Jane Doe\", \"full_name\" : \"\", \"fullName\" : null}";
String json3 = "{ \"name\" : null, \"full_name\" : \"Another Name\", \"fullName\" : null}";
ObjectMapper mapper = new ObjectMapper();
MyPojo myPojo1 = mapper.readValue(json1, MyPojo.class);
System.out.println("JSON 1 Output: " + myPojo1); // Expected: MyPojo{name='John Doe'}
MyPojo myPojo2 = mapper.readValue(json2, MyPojo.class);
System.out.println("JSON 2 Output: " + myPojo2); // Expected: MyPojo{name='Jane Doe'}
MyPojo myPojo3 = mapper.readValue(json3, MyPojo.class);
System.out.println("JSON 3 Output: " + myPojo3); // Expected: MyPojo{name='Another Name'}
}
} 注意事项与优缺点
- 优点: 领域模型 MyPojo 保持干净,不包含任何与反序列化逻辑相关的代码。逻辑封装在 Converter 中,职责分离清晰,可维护性高。
- 缺点: 需要额外定义一个辅助 POJO 和一个 Converter 类,增加了代码量。对于非常简单的场景,可能会显得有些“过度设计”。
总结
当 Jackson 反序列化遇到多别名 JSON 字段且需要优先选择非 null 或非空值时,我们有两种主要策略:
- 多智能 setter 方法: 适用于字段数量不多,且不介意在领域模型中引入少量逻辑的场景。优点是实现简单直接,无需额外类。
- 自定义 Converter: 适用于字段冗余复杂、需要保持领域模型纯净、追求更高可维护性和解耦的场景。优点是职责分离清晰,代码结构更专业。
通常情况下,推荐使用自定义 Converter 的方法,因为它提供了更强大的灵活性和更好的代码组织结构,尤其是在处理复杂的反序列化逻辑时。根据项目的具体需求和复杂性,选择最适合的策略。










