1. 簡介
在本教程中,我們將探討如何配置 Jackson 的 ObjectMapper 以處理序列化和反序列化 null 值和缺失值。最後,我們將演示一個實際場景,其中一個方法用於更新記錄,並以不同的方式處理 null 值和缺失值。
2. 差異:JSON 中的缺失字段與空字段
在處理 JSON 數據時,區分缺失字段和顯式設置為 null 的字段至關重要。雖然它們可能看起來相似,但對數據處理和 API 設計具有不同的影響。讓我們從以下簡單的 POJO 開始,它包含原始類型、List 和 Object 類型混合:
public class Sample {
private Long id;
private String name;
private int amount;
private List<String> keys;
private List<Integer> values;
// standard getters and setters
}
字段在 JSON 負載中完全缺失時不存在。 例如,在以下 JSON 中,除了 name 字段之外,所有字段都是缺失的:
{
"name": null
}
當反序列化時,缺失字段會採用其類型的默認值(例如,對象為 null 或原始類型為 0)。 這種區分在以下場景中至關重要:
- 部分更新 — 在支持部分更新的 API(例如 PATCH 請求)中,缺失字段可能表示“不要更改此值”,而 null 字段可能意味着“刪除此值”。
- 默認值 — 應用程序可能會在字段缺失時應用默認值。相反,顯式將字段設置為 null 信號着要清除其值。
- 驗證 — 驗證規則通常因缺失字段和 null 字段而異,具體取決於業務需求。
在我們的示例中,我們將創建方法來修補現有對象,同時考慮不同策略對於非空字段。因此,理解這些細微之處有助於確保可預測的應用程序行為和對 JSON 語義的遵守。此外,我們還將包括自定義默認值和簡單的 JSON 驗證,用於原始類型。
2.1. 默認 Jackson 行為
考慮一下,如果金額為零無效的情況。我們可以將 amount 字段的默認值設置為 Sample 類中:
private int amount = 1;
當序列化一個新的 Sample 實例,而沒有調用任何 setter 方法時,生成的 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"));
// other fields ...
}
如果 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 序列化
對於我們的 patch 方法,我們希望排除值為 null、缺失或設置為 Java 默認值的字段。 在 Jackson 中,缺失 指的是一個空的 Optional。 我們可以使用 Include.NON_DEFAULT 配置來實現這一切。 此設置通過省略不必要的字段來減少 payload 大小。
讓我們將一個空 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. Patching Methods
現在,讓我們來理解 Jackson 如何處理缺失和 null 值,並將其應用於實際場景:部分更新。
簡而言之,有多種處理部分更新的方法。讓我們看看兩種:
- 更新非空值,因為 null 值表示“此值未更改”
- 更新所有非空值,因為 null 和 非空 值表示“此值應設置為 null”
讓我們看一下實現這些方法的具體代碼,偏離了常規的“複製所有屬性”方法,同時利用了我們的 Jackson 配置。
5.1. Update Only Non-Nulls
我們的第一種方法是忽略所有 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. Test Non-Null Fields Update Strategy
為了測試這一點,我們先設置 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;
}
然後,我們通過僅包含 values 字段在 JSON 輸入中進行測試,檢查該字段是否被更新,以及如果某個不存在的字段保持不變:
@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. Update All Non-Absent
我們的下一項解決方案是更新 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. Test Non-Absent Fields Update Strategy
為了測試這一點,我們將 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 語義。