知識庫 / JSON RSS 訂閱

解決 Gson “多個 JSON 字段” 異常

JSON
HongKong
10
09:45 PM · Dec 05 ,2025

1. 概述

Google Gson 是一款用於 Java 中 JSON 數據綁定,功能強大且靈活的庫。 在大多數情況下,Gson 可以通過無需修改即可將數據綁定到現有的類。 然而,某些類結構可能會導致問題,這些問題難以調試。

一個有趣且可能令人困惑的異常是 IllegalArgumentException,它抱怨多個字段定義:

java.lang.IllegalArgumentException: Class <YourClass> declares multiple JSON fields named <yourField> ...

這可能特別晦澀,因為Java編譯器不允許同一類中包含多個具有相同名稱的字段。在本教程中,我們將討論導致此異常的原因,並學習如何規避它。

2. 異常原因

該異常的潛在原因與 Gson 解析器在序列化(或反序列化)類時,由於類結構或配置而產生的混淆有關。

2.1. @SerializedName 衝突

Gson 提供 @SerializedName 註解,允許在序列化對象中修改字段名稱。這是一個有用的功能,但也可能導致衝突。

例如,讓我們創建一個簡單的類,BasicStudent

public class BasicStudent {
    private String name;
    private String major;
    @SerializedName("major")
    private String concentration;
    // General getters, setters, etc.
}

在序列化過程中,Gson 會嘗試使用“major” 同時表示“major”和“concentration”,從而導致上述的 IllegalArgumentException

java.lang.IllegalArgumentException: Class BasicStudent declares multiple JSON fields named 'major';
conflict is caused by fields BasicStudent#major and BasicStudent#concentration

異常消息指向了問題字段,問題可以通過簡單地修改或刪除註釋或重命名字段來解決。

Gson 還有其他選項可以排除字段,我們稍後在本教程中將討論這些選項。

首先,讓我們看看導致此異常的其他原因。

2.2 類繼承層次結構

類繼承也可能成為序列化到 JSON 時產生問題

為了探索這個問題,我們需要更新我們的學生數據示例。

讓我們定義兩個類,StudentV1StudentV2,它們繼承 StudentV1 並添加一個額外的成員變量:

public class StudentV1 {
    private String firstName;
    private String lastName;
    // General getters, setters, etc.
}
public class StudentV2 extends StudentV1 {
    private String firstName;
    private String lastName;
    private String major;
    // General getters, setters, etc.
}

特別地,StudentV2不僅擴展了StudentV1,還定義了自己的變量集,其中一些變量與StudentV1中的變量重複。雖然這並非最佳實踐,但對於我們的示例至關重要,並且我們在使用第三方庫或遺留包時可能會遇到這種情況。

讓我們創建一個StudentV2的實例,並嘗試對其進行序列化。我們可以創建一個單元測試來確認IllegalArgumentException會被拋出。

@Test
public void givenLegacyClassWithMultipleFields_whenSerializingWithGson_thenIllegalArgumentExceptionIsThrown() {
    StudentV2 student = new StudentV2("Henry", "Winter", "Greek Studies");

    Gson gson = new Gson();
    assertThatThrownBy(() -> gson.toJson(student))
      .isInstanceOf(IllegalArgumentException.class)
      .hasMessageContaining("declares multiple JSON fields named 'firstName'");
}

類似於上面遇到的 @SerializedName 衝突,public class BasicStudent { private String name; private transient String major; @SerializedName("major") private String concentration; // General getters, setters, etc. }

讓我們創建一個單元測試,以嘗試在這一更改後進行序列化:

@Test
public void givenBasicStudent_whenSerializingWithGson_thenTransientFieldNotSet() {
    BasicStudent student = new BasicStudent("Henry Winter", "Greek Studies", "Classical Greek Studies");

    Gson gson = new Gson();
    String json = gson.toJson(student);

    BasicStudent deserialized = gson.fromJson(json, BasicStudent.class);
    assertThat(deserialized.getMajor()).isNull();
}

序列化成功,且 major 字段的值未包含在反序列化的實例中。

儘管這是一種簡單的解決方案,但這種方法存在兩個缺點。 添加 transient 關鍵字會導致字段在所有序列化過程中被排除,包括基本的 Java 序列化。 此外,這種方法還假設 BasicStudent 類可以被修改,但這並非總是能夠實現的。

3.2. 使用 Gson 的 @Expose 註解進行序列化

如果問題類可以被修改,並且我們希望採用僅限於 Gson 序列化的方法,我們可以利用 @Expose 註解。該註解告知 Gson 哪些字段應該在序列化、反序列化或兩者中暴露。

我們可以更新我們的 StudentV2 實例,顯式地告知 Gson 只暴露其字段:

public class StudentV2 extends StudentV1 {
    @Expose
    private String firstName;
    @Expose 
    private String lastName; 
    @Expose
    private String major;

    // General getters, setters, etc. 
}

如果再次運行代碼,沒有任何變化,我們仍然會看到異常。默認情況下,Gson 在遇到 @Expose 時不會改變其行為——我們需要告訴解析器應該做什麼。

讓我們更新我們的單元測試,使用 GsonBuilder 創建一個排除沒有 @Expose 字段的解析器實例:

@Test
public void givenStudentV2_whenSerializingWithGsonExposeAnnotation_thenSerializes() {
    StudentV2 student = new StudentV2("Henry", "Winter", "Greek Studies");

    Gson gson = new GsonBuilder().excludeFieldsWithoutExposeAnnotation().create();

    String json = gson.toJson(student);
    assertThat(gson.fromJson(json, StudentV2.class)).isEqualTo(student);
}

序列化和反序列化現在成功完成。@Expose 仍然提供了一種簡單易用的解決方案,但僅影響 Gson 的序列化(並且僅在我們將解析器配置為識別它時才生效)。

這種方法仍然假設我們可以編輯源代碼。它也沒有提供多少靈活性——所有我們關心的字段都需要進行標註,而其餘字段則從序列化和反序列化中排除

3.3. 使用 Gson 的 ExclusionStrategy 進行序列化

幸運的是,Gson 在我們無法修改源類或需要更多靈活性時,提供瞭解決方案:即 ExclusionStrategy

這個接口告知 Gson 在序列化或反序列化過程中如何排除字段,並允許更復雜的業務邏輯。我們可以聲明一個簡單的 ExclusionStrategy 實現:

public class StudentExclusionStrategy implements ExclusionStrategy {
    @Override
    public boolean shouldSkipField(FieldAttributes field) {
        return field.getDeclaringClass() == StudentV1.class;
    }

    @Override
    public boolean shouldSkipClass(Class<?> aClass) {
        return false;
    }
}

ExclusionStrategy接口有兩個方法:shouldSkipField方法提供對單個字段級別的精細控制,而shouldSkipClass方法則控制特定類型的所有字段是否應被跳過。 在我們上面的示例中,我們從簡單開始,跳過所有來自StudentV1的字段。

類似於@Expose,我們需要告訴Gson如何使用該策略。 讓我們在我們的測試中進行配置:

@Test
public void givenStudentV2_whenSerializingWithGsonExclusionStrategy_thenSerializes() {
    StudentV2 student = new StudentV2("Henry", "Winter", "Greek Studies");

    Gson gson = new GsonBuilder().setExclusionStrategies(new StudentExclusionStrategy()).create();

    assertThat(gson.fromJson(gson.toJson(student), StudentV2.class)).isEqualTo(student);
}

值得注意的是,我們正在使用 setExclusionStrategies() 配置解析器——這意味着我們的策略用於序列化和反序列化過程。

如果我們想要在更靈活地控制 ExclusionStrategy 的應用時機,可以以不同的方式配置解析器:

// Only exclude during serialization
Gson gson = new GsonBuilder().addSerializationExclusionStrategy(new StudentExclusionStrategy()).create();

// Only exclude during de-serialization
Gson gson = new GsonBuilder().addDeserializationExclusionStrategy(new StudentExclusionStrategy()).create();

這種方法比我們之前的兩個解決方案略複雜一些:我們需要聲明一個新的類,並且更深入地思考哪些字段應該包含在內。我們為了這個示例,將業務邏輯保留在 ExclusionStrategy 中保持相對簡單,但這種方法的優勢在於更豐富和更健壯的字段排除。最後,我們不需要修改 StudentV2StudentV1 中的代碼

4. 結論

在本文中,我們討論了在使用 Gson 時可能遇到的一個棘手但最終可修復的 <em >IllegalArgumentException</em> 的原因。

我們發現,根據我們的需求,我們可以實現各種解決方案,這些解決方案在簡潔性、粒度和靈活性方面各有側重。

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

發佈 評論

Some HTML is okay.