知識庫 / JSON / Jackson RSS 訂閱

如何區分 Jackson 中的 Field Absent 與 Null

Jackson
HongKong
6
09:41 PM · Dec 05 ,2025

1. 簡介

本教程將探討如何配置 Jackson 的 ObjectMapper 以處理 null 值和缺失值的序列化和反序列化。最後,我們將演示一個實際場景,其中包含一個方法,該方法會以不同的方式處理 null 值和缺失值。

2. JSON 中缺失字段和空字段的區別

在處理 JSON 數據時,區分缺失字段和明確設置為 null 值的字段至關重要。雖然它們可能看起來相似,但對數據處理和 API 設計具有不同的影響。以下是一個包含原始類型、ListObject 值的簡單 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)。 這種區分在以下場景中至關重要:

  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 的上下文中,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 語義。

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

發佈 評論

Some HTML is okay.