1. 引言
在Java开发中,我们经常需要处理JSON数据。Jackson作为主流的JSON处理库,如何配置其ObjectMapper
来区分字段缺失(absent)和显式设置为null的字段,是一个常见需求。本文将探讨Jackson的序列化和反序列化配置,并通过一个实际案例(更新记录时区分null和缺失字段)来演示。
2. JSON中缺失字段与Null字段的区别
在JSON处理中,区分字段缺失和显式null至关重要。虽然看起来相似,但它们在数据处理和API设计中含义不同。我们以一个包含基本类型、列表和对象类型的POJO为例:
public class Sample {
private Long id;
private String name;
private int amount;
private List<String> keys;
private List<Integer> values;
// 标准getter和setter
}
字段缺失:指JSON载荷中完全没有该字段。例如,以下JSON中,除了name
字段外,其他字段都缺失:
{
"name": null
}
反序列化时,缺失字段会采用其类型的默认值(例如,对象为null,基本类型为0)。这种区别在以下场景中很关键:
- 部分更新:在支持部分更新的API(如PATCH请求)中,缺失字段可能表示“不修改该值”,而null字段可能表示“清除该值”。
- 默认值:当字段缺失时,应用可能使用默认值;而显式设置为null则表示要清除该值。
- 验证:根据业务需求,缺失字段和null字段的验证规则可能不同。
在示例中,我们将创建方法来更新现有对象,针对非缺失字段采用不同策略。理解这些细微差别有助于确保应用行为可预测,并符合JSON语义。此外,我们还会为基本类型设置自定义默认值,并添加简单的JSON验证。
2.1. Jackson的默认行为
假设amount
为0是无效的,我们可以在Sample
类中为amount
字段设置默认值:
private int amount = 1;
当序列化一个未调用任何setter的新Sample
实例时,结果JSON中amount
为1,其他字段为null:
@Test
void whenSerializingWithDefaults_thenNullValuesIncluded() {
Sample zeroArg = new Sample();
Map<String, Object> map = new ObjectMapper()
.convertValue(zeroArg, Map.class);
assertEquals(1, map.get("amount"));
assertTrue(map.containsKey("id"));
assertNull(map.get("id"));
// 其他字段...
}
如果JSON载荷显式将amount
设为null,Jackson会使用基本类型的默认值(0)而非我们的自定义默认值:
@Test
void whenDeserializingToMapWithDefaults_thenNullPrimitiveIsDefaulted() {
String json = """
{
"amount": null
}
""";
Sample sample = new ObjectMapper().readValue(json, Sample.class);
assertEquals(0, sample.getAmount());
}
3. 自定义Jackson反序列化
为确保null值不会被静默转换为默认值,我们可以启用FAIL_ON_NULL_FOR_PRIMITIVES
反序列化特性。启用后,为基本类型设置null会抛出MismatchedInputException
:
@Test
void whenValidatingNullPrimitives_thenFailOnNullAmount() {
ObjectMapper mapper = new ObjectMapper();
mapper.enable(DeserializationFeature.FAIL_ON_NULL_FOR_PRIMITIVES);
String json = """
{
"amount": null
}
""";
assertThrows(MismatchedInputException.class,
() -> mapper.readValue(json, Sample.class));
}
4. 自定义Jackson序列化
对于我们的补丁方法,我们希望排除null、缺失或设置为Java默认值的字段。在Jackson中,“缺失”指空的Optional
。 使用Include.NON_DEFAULT
配置即可实现。此设置通过省略不必要字段来减小载荷大小。
让我们将一个空的Sample
实例转换为Map,验证只有amount
字段会出现(因为设置了自定义默认值):
@Test
void whenSerializingNonDefault_thenOnlyNonJavaDefaultsIncluded() {
ObjectMapper mapper = new ObjectMapper();
mapper.setSerializationInclusion(Include.NON_DEFAULT);
Sample zeroArg = new Sample();
Map<String, Serializable> map = mapper.convertValue(
zeroArg, Map.class);
assertEquals(zeroArg.getAmount(), map.get("amount"));
assertEquals(1, map.keySet().size());
}
精简的序列化使得在补丁对象时更容易决定更新哪些字段。
5. 补丁方法
现在,我们将Jackson对缺失和null值的处理应用到实际场景:部分更新。
简而言之,处理部分更新有多种方式。我们看两种:
- 仅更新非null值:因为null值表示“该值未改变”
- 更新所有非缺失字段:因为null和非缺失值表示“该值应设为null”
让我们看实现这两种方式的具体代码,摒弃常见的“复制所有属性”方法,利用我们的Jackson配置。
5.1. 仅更新非null值
第一种方法在反序列化后忽略所有null值。 这样,发送补丁时只需关注要修改的值:
void updateIgnoringNulls(String json, Sample current)
throws JsonProcessingException {
Sample update = MAPPER.readValue(json, Sample.class);
if (update.getId() != null)
current.setId(update.getId());
if (update.getName() != null)
current.setName(update.getName());
current.setAmount(update.getAmount());
if (update.getKeys() != null)
current.setKeys(update.getKeys());
if (update.getValues() != null)
current.setValues(update.getValues());
}
如果不需要删除现有值,这种方案很有效。
5.2. 测试非null字段更新策略
添加一些测试设置,首先在Sample
类中设置一些默认值:
public static Sample basic() {
Sample defaults = new Sample();
List keys = List.of("foo", "bar");
List values = List.of(1, 2);
defaults.setId(1l);
defaults.setKeys(keys);
defaults.setValues(values);
return defaults;
}
然后测试:JSON输入中只包含values
字段,检查该字段是否更新,以及一个缺失字段是否保留原值:
@Test
void whenPatchingNonNulls_thenNullsIgnored() {
List<Integer> values = List.of(3);
Sample defaults = Sample.basic();
String json = """
{
"values": %s
}
""".formatted(values);
updateIgnoringNulls(json, defaults);
assertEquals(values, defaults.getValues());
assertNotNull(defaults.getKeys());
}
5.3. 更新所有非缺失字段
下一种方案更新JSON输入中包含的所有字段,即使是null:
void updateNonAbsent(String json, Sample current)
throws JsonProcessingException {
Map<String, Serializable> update = MAPPER.readValue(json, Map.class);
if (update.containsKey("id"))
current.setId((Long) update.get("id"));
if (update.containsKey("name"))
current.setName((String) update.get("name"));
if (update.containsKey("amount"))
current.setAmount((int) update.get("amount"));
if (update.containsKey("keys"))
current.setKeys((List<String>) update.get("keys"));
if (update.containsKey("values"))
current.setValues((List<Integer>) update.get("values"));
}
使用此方案时,显式包含null字段表示更新现有对象时要清除该字段。
5.4. 测试非缺失字段更新策略
测试时,显式将keys
字段设为null,并修改values
字段。我们预期只有这两个字段受影响,同时检查一个缺失字段是否保持不变:
@Test
void whenPatchingNonAbsent_thenNullsConsidered() {
List<Integer> values = List.of(3);
Sample defaults = Sample.basic();
String json = """
{
"values": %s,
"keys": null
}
""".formatted(values);
updateNonAbsent(json, defaults);
assertEquals(values, defaults.getValues());
assertNull(defaults.getKeys());
assertNotNull(defaults.getId());
}
6. 结论
本文探讨了根据应用需求灵活处理null和缺失值的方法。无论是忽略null还是将其视为有效值,自定义Jackson行为都能让我们在遵循JSON语义的同时实现所需功能。关键点总结:
✅ 区分缺失和null:在部分更新中,缺失表示“不修改”,null表示“清除”。
⚠️ 基本类型默认值:Jackson会将null转为0,踩坑时启用FAIL_ON_NULL_FOR_PRIMITIVES
。
❌ 避免静默转换:确保null值不会意外被默认值覆盖。
通过合理配置Jackson,我们可以更精确地控制JSON数据处理,避免常见陷阱。