如何在 Jackson 中區分 Field Absent 和 Null

Jackson
Remote
0
06:32 PM · Nov 30 ,2025

1. 簡介

在本教程中,我們將探討如何配置 Jackson 的 ObjectMapper 以處理序列化和反序列化 null 值和缺失值。最後,我們將演示一個實際場景,其中一個方法用於更新記錄,並以不同的方式處理 null 值和缺失值。

2. 差異:JSON 中的缺失字段與空字段

在處理 JSON 數據時,區分缺失字段和顯式設置為 null 的字段至關重要。雖然它們可能看起來相似,但對數據處理和 API 設計具有不同的影響。讓我們從以下簡單的 POJO 開始,它包含原始類型、ListObject 類型混合:

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)。 這種區分在以下場景中至關重要:

  1. 部分更新 — 在支持部分更新的 API(例如 PATCH 請求)中,缺失字段可能表示“不要更改此值”,而 null 字段可能意味着“刪除此值”。
  2. 默認值 — 應用程序可能會在字段缺失時應用默認值。相反,顯式將字段設置為 null 信號着要清除其值。
  3. 驗證 — 驗證規則通常因缺失字段和 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 語義。

user avatar
0 位用戶收藏了這個故事!
收藏

發佈 評論

Some HTML is okay.