Resolving Gson’s “Multiple JSON Fields” Exception – 解决 Gson’s “Multiple JSON Fields” 异常

最后修改: 2023年 12月 4日

中文/混合/英文(键盘快捷键:t)

1. Overview

1.概述

Google Gson is a useful and flexible library for JSON data binding in Java. In most cases, Gson can perform data binding to an existing class with no modification. However, certain class structures can cause issues that are difficult to debug.

Google Gson是 Java 中 JSON 数据绑定的实用而灵活的库。在大多数情况下,Gson 可以在不修改现有类的情况下执行数据绑定。但是,某些类结构可能会导致难以调试的问题。

One interesting and potentially confusing exception is an IllegalArgumentException that complains about multiple field definitions:

IllegalArgumentException 是一个有趣且可能引起混淆的异常,它会抱怨多个字段定义:

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

This can be particularly cryptic since the Java compiler doesn’t allow multiple fields in the same class to share a name. In this tutorial, we’ll discuss the causes of this exception and learn how to get around it.

由于 Java 编译器不允许同一类中的多个字段共享一个名称,因此这种情况可能特别令人费解。在本教程中,我们将讨论产生这种异常的原因,并学习如何解决它。

2. Exception Causes

2.异常原因

The potential causes for this exception relate to class structure or configuration that confuses the Gson parser when serializing (or de-serializing) a class.

造成这种异常的潜在原因与类的结构或配置有关,它们在序列化(或去序列化)类时混淆了 Gson 解析器。

2.1. @SerializedName Conflicts

2.1.@SerializedName 冲突

Gson provides the @SerializedName annotation to allow manipulation of the field name in the serialized object. This is a useful feature, but it can lead to conflicts.

Gson 提供了 @SerializedName 注解,允许对序列化对象中的字段名称进行操作。这是一个有用的功能,但也可能导致冲突。

For example, let’s create a simple class, BasicStudent:

例如,让我们创建一个简单的类,BasicStudent

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

During serialization, Gson will attempt to use “major” for both major and concentration, leading to the IllegalArgumentException from above:

在序列化过程中,Gson 会尝试对 majorconcentration 使用”major“,从而导致上述 IllegalArgumentException 异常:

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

The exception message points to the problem fields and the issue can be addressed by simply changing or removing the annotation or renaming the field.

异常信息指向问题字段,只需更改或删除注释或重新命名字段即可解决问题。

There are also other options for excluding fields in Gson, which we’ll discuss later in this tutorial.

本教程稍后还将讨论 在 Gson 中排除字段的其他选项。

First, let’s look at the other cause for this exception.

首先,我们来看看造成这种例外情况的另一个原因。

2.2. Class Inheritance Hierarchies

2.2.类继承层次结构

Class inheritance can also be a source of problems when serializing to JSON. To explore this issue, we’ll need to update our student data example.

当序列化为 JSON 时,类继承也可能成为问题的根源。要探讨这个问题,我们需要更新我们的学生数据示例。

Let’s define two classes, StudentV1 and StudentV2, which extends StudentV1 and adds an additional member variable:

让我们定义两个类: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.
}

Notably, StudentV2 not only extends StudentV1 but also defines its own set of variables, some of which duplicate those in StudentV1. While this isn’t best practice, it’s crucial to our example and something we may encounter in the real world when using a third-party library or legacy package.

值得注意的是,StudentV2 不仅扩展了 StudentV1,还定义了自己的变量集,其中一些变量与 StudentV1 中的变量重复。虽然这并不是最佳实践,但对我们的示例却至关重要,而且在现实世界中,当我们使用第三方库或传统软件包时,可能会遇到这种情况。

Let’s create an instance of StudentV2 and attempt to serialize it. We can create a unit test to confirm that IllegalArgumentException is thrown:

让我们创建 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'");
}

Similar to the @SerializedName conflicts above, Gson doesn’t know which field to use when encountering duplicate names in the class hierarchy.

与上述 @SerializedName 冲突类似,Gson 不知道在类层次结构中遇到重复名称时应使用哪个字段

3. Solutions

3.解决方案

There are a few solutions to this issue, each with its own pros and cons that provide different levels of control over serialization.

有几种解决方案可以解决这个问题,它们各有利弊,可提供不同程度的序列化控制。

3.1. Marking Fields as transient

3.1.将字段标记为暂存</em

The simplest way to control which fields are serialized is by using the transient field modifier. We can update BasicStudent from above:

控制哪些字段被序列化的最简单方法是使用 transient 字段修改器。我们可以更新上述 BasicStudent 字段:

public class BasicStudent {
    private String name;
    private transient String major;
    @SerializedName("major") 
    private String concentration; 

    // General getters, setters, etc. 
}

Let’s create a unit test to attempt serialization after this change:

让我们创建一个单元测试,尝试在更改后进行序列化:

@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();
}

Serialization succeeds, and the major field value isn’t included in the de-serialized instance.

序列化成功,但 major 字段值不包含在去序列化实例中。

Though this is a simple solution, there are two downsides to this approach. Adding transient means the field will be excluded from all serialization, including basic Java serialization. This approach also assumes that BasicStudent can be modified, which may not always be the case.

虽然这是一个简单的解决方案,但这种方法有两个缺点。添加 transient 意味着字段将被排除在所有序列化之外,包括 基本 Java 序列化。这种方法还假设 BasicStudent 可以被修改,但实际情况并非总是如此。

3.2. Serialization With Gson’s @Expose Annotation

3.2.使用 Gson 的 @Expose 注解进行序列化

If the problem class can be modified and we want an approach scoped to only Gson serialization, we can make use of the @Expose annotation. This annotation informs Gson which fields should be exposed during serialization, de-serialization, or both.

如果问题类可以修改,而我们又希望只使用 Gson 序列化方法,那么我们可以使用 @Expose 注解。此注解可告知 Gson 在序列化、去序列化或两者过程中应暴露哪些字段。

We can update our StudentV2 instance to explicitly expose only its fields to Gson:

我们可以更新StudentV2实例,显式地只向 Gson 公开其字段:

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

    // General getters, setters, etc. 
}

If we run the code again, nothing will change, and we’ll still see the exception. By default, Gson doesn’t change its behavior when encountering @Expose – we need to tell the parser what it should do.

如果我们再次运行代码,一切都不会改变,我们仍然会看到异常。默认情况下,Gson 在遇到 @Expose 时不会改变其行为,我们需要告诉解析器它应该做什么。

Let’s update our unit test to use the GsonBuilder to create an instance of the parser that excludes fields without @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);
}

Serialization and de-serialization now succeed. @Expose has the benefit of still being a simple solution while only affecting Gson serialization (and only if we configure the parser to recognize it).

现在序列化和去序列化都成功了。@Expose的好处是,它仍然是一个简单的解决方案,同时只影响 Gson 序列化(而且只有当我们配置解析器来识别它时)。

This approach still assumes we can edit the source code, however. It also doesn’t provide much flexibility – all fields that we care about need to be annotated, and the rest are excluded from both serialization and de-serialization.

不过,这种方法仍然假定我们可以编辑源代码。它也没有提供太多灵活性–所有我们关心的字段都需要注释,其余的字段则被排除在序列化和去序列化之外

3.3. Serialization With Gson’s ExclusionStrategy

3.3.使用 Gson 的 ExclusionStrategy 序列化

Fortunately, Gson provides a solution when we can’t change the source class or we need more flexibility: the ExclusionStrategy.

幸运的是,当我们无法更改源类或需要更大的灵活性时,Gson 提供了一种解决方案:ExclusionStrategy.

This interface informs Gson of how to exclude fields during serialization or de-serialization and allows for more complex business logic. We can declare a simple ExclusionStrategy implementation:

该接口通知 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;
    }
}

The ExclusionStrategy interface has two methods: shouldSkipField() provides granular control at the individual field level, and shouldSkipClass() controls if all fields of a certain type should be skipped. In our example above, we’re starting simple and skipping all fields from StudentV1.

ExclusionStrategy 接口有两个方法:shouldSkipField() 提供单个字段级别的细粒度控制,而 shouldSkipClass() 则控制是否应跳过某一类型的所有字段。在上面的示例中,我们从简单开始,跳过 StudentV1 中的所有字段。

Just as with @Expose, we need to tell Gson how to use this strategy. Let’s configure it in our test:

@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);
}

It’s worth noting that we’re configuring the parser with setExclusionStrategies() – this means our strategy is used for both serialization and de-serialization.

值得注意的是,我们使用 setExclusionStrategies() 配置解析器–这意味着我们的策略将同时用于序列化和去序列化。

If we wanted more flexibility of when the ExclusionStrategy is applied, we could configure the parser differently:

如果我们希望在应用 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();

This approach is slightly more complex than our other two solutions: we needed to declare a new class and think more about what makes a field important to include. We kept the business logic in our ExclusionStrategy fairly simple for this example, but the upside of this approach is richer and more robust field exclusion. Finally, we didn’t need to change the code inside StudentV2 or StudentV1.

这种方法比其他两种解决方案稍微复杂一些:我们需要声明一个新类,并更多地考虑是什么使一个字段具有重要的包含意义。在本示例中,我们将 ExclusionStrategy 中的业务逻辑保持得相当简单,但这种方法的优点是字段排除功能更丰富、更强大。最后,我们无需更改 StudentV2StudentV1 中的代码。

4. Conclusion

4.结论

In this article, we discussed the causes for a tricky yet ultimately fixable IllegalArgumentException we can encounter when using Gson.

在本文中,我们讨论了在使用 Gson 时可能会遇到的棘手但最终可以修复的 IllegalArgumentException 的原因。

We found that there are a variety of solutions we can implement based on our needs for simplicity, granularity, and flexibility.

我们发现,根据我们对简单性、粒度和灵活性的需求,我们可以实施多种解决方案。

As always, all of the code can be found over on GitHub.

与往常一样,所有代码都可以在 GitHub 上找到