1. 簡介
本教程將探討如何配置 Jackson 的 ObjectMapper 以處理 null 值和缺失值的序列化和反序列化。最後,我們將演示一個實際場景,其中包含一個方法,該方法會以不同的方式處理 null 值和缺失值。
2. JSON 中缺失字段和空字段的區別
在處理 JSON 數據時,區分缺失字段和明確設置為 null 值的字段至關重要。雖然它們可能看起來相似,但對數據處理和 API 設計具有不同的影響。以下是一個包含原始類型、List 和 Object 值的簡單 POJO 示例:
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 的上下文中,Absent 指的是一個空的 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. 修補方法
現在,讓我們運用對 Jackson 如何處理缺失和 null 值以及 null 值的理解,將其應用於實際場景:部分更新。
簡而言之,處理部分更新有多種方法。我們來看兩種:
- 僅更新非 null 值,因為 null 值表示“此值未更改”
- 更新所有非缺失值,因為 null 值和非缺失值表示“此值應設置為 null”
讓我們來看一些實現這些方法的事實代碼,偏離了常規的“複製所有屬性”方法,同時利用我們的 Jackson 配置。
5.1. 只更新非空值
我們的第一種方法是忽略序列化後所有的 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. 測試非空字段更新策略
讓我們添加一些設置來測試這一點,首先在我們的 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"));
}通過此解決方案,明確地包含一個 字段,意味着我們在更新現有對象時想要清除該字段。
5.4. 測試非空字段更新策略
為了測試此策略,我們將明確地將 <em >keys</em > 字段設置為 <em >null</em >,並修改 <em >values</em > 字段。 我們預期只有這些字段會受到影響,因此我們還檢查是否存在未設置的字段是否保持不變:
@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 和缺失值的各種方法,具體取決於應用程序的需求。 通過自定義 Jackson 的行為,無論是在忽略 null 值還是將其視為有意義值的情況下,我們都可以實現所需的功能,同時遵守 JSON 語義。