如何在 Jackson 中區分欄位缺失與空值
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)。在以下場景中,這種區別至關重要:
-
Partial Updates
- 在支援部分更新的 API(例如,PATCH 請求)中,缺少的欄位可能表示“不要變更此值”,而null
欄位可能表示“刪除此值”。 -
Default Values
-當欄位不存在時,應用程式可能會套用預設值。相反,明確將欄位設為null
表示清除其值的意圖。 -
Validation
-根據業務需求,缺失欄位和null
欄位的驗證規則通常有所不同。
在我們的範例中,我們將建立方法來修補現有對象,並考慮針對非缺失欄位的不同策略。因此,了解這些細微差別有助於確保可預測的應用程式行為並遵守 JSON 語義。此外,我們還將為原語添加自訂預設值和簡單的 JSON 驗證。
2.1.預設 Jackson 行為
考慮一個金額為零的無效場景。我們可以為Sample
類別中的amount
欄位設定一個預設值:
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 序列化
對於我們的修補方法,我們希望排除為null
、不存在或設定為 Java 預設值的欄位。在 Jackson 的語境中,「Absent」 指的是空的Optional
。我們可以使用Include.NON_DEFAULT
配置來實現所有這些。此設定透過省略不必要的欄位來減少有效負載大小。
我們將一個空的Sample
實例轉換為一個映射,以驗證是否由於我們的自訂預設值而只會出現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 如何處理缺失值和空值,並將其應用到實際環境中:部分更新。
簡而言之,有多種方法可以處理部分更新。我們來看兩個:
-
Update only non-nulls
,因為null
值表示“此值未改變” -
Update all non-absent
,因為null
和non-absent
值意味著“該值應設為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"));
}
使用此解決方案,明確包含一個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
和缺失值的方法。無論是忽略空值還是將其視為重要值,自訂 Jackson 的行為都可以讓我們在遵守 JSON 語義的同時實現所需的功能。
與往常一樣,原始碼可在 GitHub 上取得。