【问题标题】:How to work around Gson serialization giving different results when using generic wildcards?使用通用通配符时如何解决 Gson 序列化给出不同的结果?
【发布时间】:2019-03-18 13:20:15
【问题描述】:

考虑这个例子:

static class BaseBean { String baseField = "base"; }
static class ChildBean extends BaseBean { String childField = "child"; }

static class BaseBeanHolder {
    List <? extends BaseBean> beans;

    public BaseBeanHolder(List<? extends BaseBean> beans) { this.beans = beans; }
}

static class ChildBeanHolder {
    List <ChildBean> beans;

    public ChildBeanHolder(List<ChildBean> beans) { this.beans = beans; }
}

@Test
public void mcve() {
    BaseBeanHolder baseHolder = new BaseBeanHolder(singletonList(new ChildBean()));
    System.out.println(new Gson().toJson(baseHolder));

    ChildBeanHolder childHolder = new ChildBeanHolder(singletonList(new ChildBean()));
    System.out.println(new Gson().toJson(childHolder));
}

打印出来:

{"beans":[{"baseField":"base"}]}

{"beans":[{"childField":"child","baseField":"base"}]}

因此,虽然两个列表都包含子对象,但只有第二个持有者会导致子字段被序列化为 JSON。

我看到了其他问题,例如 here,但我想知道是否有合理的解决方法来实现我想要的。

换句话说:有没有办法让这样的一个“持有人”类接受BaseBeans或ChildBeans(&lt;? extends BaseBean&gt;这样做),并且 使用 Gson 将实例序列化为 JSON 字符串时给我正确的结果?

(注意:我不能使用特定类型的适配器,因为我无法控制实际 Gson 实例的来源以及它在我们的环境中的配置方式)

【问题讨论】:

    标签: java json serialization gson


    【解决方案1】:

    通常集合实现从集合字段声明中“获取”类型 - 而不是来自List/Set/等上的给定项目。我们需要编写自定义序列化器,为每个项目找到序列化器并使用它。简单实现:

    class TypeAwareListJsonSeserializer implements JsonSerializer<List<?>> {
        @Override
        public JsonElement serialize(List<?> src, Type typeOfSrc, JsonSerializationContext context) {
            if (src == null) {
                return JsonNull.INSTANCE;
            }
            JsonArray array = new JsonArray();
            for (Object item : src) {
                JsonElement jsonElement = context.serialize(item, item.getClass());
                array.add(jsonElement);
            }
            return array;
        }
    }
    

    我们可以这样使用它:

    import com.google.gson.Gson;
    import com.google.gson.GsonBuilder;
    import com.google.gson.JsonArray;
    import com.google.gson.JsonElement;
    import com.google.gson.JsonNull;
    import com.google.gson.JsonSerializationContext;
    import com.google.gson.JsonSerializer;
    import com.google.gson.annotations.JsonAdapter;
    
    import java.lang.reflect.Type;
    import java.util.Arrays;
    import java.util.List;
    
    public class GsonApp {
    
        public static void main(String[] args) throws Exception {
            List<BaseBean> children = Arrays.asList(new BaseBean(), new ChildBean(), new ChildBean2());
            BaseBeanHolder baseHolder = new BaseBeanHolder(children);
            Gson gson = new GsonBuilder()
                    .setPrettyPrinting()
                    .create();
            System.out.println(gson.toJson(baseHolder));
        }
    }
    
    class BaseBean {
        String baseField = "base";
    }
    
    class ChildBean extends BaseBean {
        String childField = "child";
    }
    
    class ChildBean2 extends BaseBean {
        int bean2Int = 356;
    }
    
    class BaseBeanHolder {
    
        @JsonAdapter(TypeAwareListJsonSeserializer.class)
        private List<? extends BaseBean> beans;
    
        // getters, setters, toString
    }
    

    上面的代码打印:

    {
      "beans": [
        {
          "baseField": "base"
        },
        {
          "childField": "child",
          "baseField": "base"
        },
        {
          "bean2Int": 356,
          "baseField": "base"
        }
      ]
    }
    

    编辑

    在序列化过程中,我们会丢失反序列化过程中需要的类型信息。我开发了简单的类型信息,这些信息将在序列化过程中存储并用于反序列化。它可能如下所示:

    class TypeAwareListJsonAdapter implements JsonSerializer<List<?>>, JsonDeserializer<List<?>> {
    
        private final String typeProperty = "@type";
    
        @Override
        public JsonElement serialize(List<?> src, Type typeOfSrc, JsonSerializationContext context) {
            if (src == null) {
                return JsonNull.INSTANCE;
            }
            JsonArray array = new JsonArray();
            for (Object item : src) {
                JsonObject jsonElement = (JsonObject) context.serialize(item, item.getClass());
                jsonElement.addProperty(typeProperty, item.getClass().getSimpleName());
    
                array.add(jsonElement);
            }
            return array;
        }
    
        @Override
        public List<?> deserialize(JsonElement json, Type typeOfT, JsonDeserializationContext context) throws JsonParseException {
            final Type elementType = $Gson$Types.getCollectionElementType(typeOfT, List.class);
    
            if (json instanceof JsonArray) {
                final JsonArray array = (JsonArray) json;
                final int size = array.size();
                if (size == 0) {
                    return Collections.emptyList();
                }
    
                final List<?> suites = new ArrayList<>(size);
                for (int i = 0; i < size; i++) {
                    JsonObject jsonElement = (JsonObject) array.get(i);
                    String simpleName = jsonElement.get(typeProperty).getAsString();
                    suites.add(context.deserialize(jsonElement, getClass(simpleName, elementType)));
                }
    
                return suites;
            }
    
            return Collections.emptyList();
        }
    
        private Type getClass(String simpleName, Type defaultType) {
            try {
                // you can use mapping or something else...
                return Class.forName("com.model." + simpleName);
            } catch (ClassNotFoundException e) {
                return defaultType;
            }
        }
    }
    

    最大的问题是如何将类映射到JSON 值。我们可以使用类简单名称或提供Map&lt;String, Class&gt; 并使用它。现在,我们可以像上面一样使用它。现在打印示例应用程序:

    {
      "beans": [
        {
          "baseField": "base",
          "@type": "BaseBean"
        },
        {
          "childField": "child",
          "baseField": "base",
          "@type": "ChildBean"
        },
        {
          "bean2Int": 356,
          "baseField": "base",
          "@type": "ChildBean2"
        }
      ]
    }
    BaseBean{baseField='base'}
    ChildBean{baseField='base', childField='child'}
    ChildBean2{baseField='base', bean2Int=356}
    

    【讨论】:

    • 非常好的答案。现在我想知道:是否有类似的优雅方式以某种“对称”方式进行反序列化?还是需要在 gson 实例上使用不同的类型适配器?
    • @GhostCat,我认为我的answerMapping Json Array to Java Models 的问题可能很有用。它需要一些重构,但大多数情况下它包含要反序列化的所有内容,给定JSON 对象类型。我将尝试“反转”实施并更新答案。
    • @GhostCat,在序列化过程中我们丢失了类型相关信息。我们可以在反序列化器中尝试反序列化到许多子类型并检查给定对象的fit rate,但是对于许多子类型会出现问题。最简单的方法是在序列化期间包含额外的字段 - 例如类简单名称并在反序列化期间使用此信息。当然,如果我们想要一个类型的适配器类用于序列化和反序列化,它应该扩展两个接口:JsonSerializer&lt;List&lt;?&gt;&gt;JsonDeserializer&lt;List&lt;?&gt;&gt;
    • 是的,我也有类似的想法。但现在一切都好。我现在最大的问题是决定是否要使用您的解决方案,或者我的解决方法(见下文,使用数组)或退后一步并简单地使用两个不同的持有者类(这意味着更多的代码,但更具表现力的类型用于我需要的不同用例)。
    • @GhostCat,数组技巧很棒!我不知道数组序列化过程与collections 不同。我添加了简单的type-aware 序列化/反序列化示例。希望它能以某种方式帮助你。
    【解决方案2】:

    Gson 的构建考虑了“我将用于序列化”“我将用于反序列化”。

    无法从原始 JSON 中确定 BaseBean 后代的确切运行时类型是什么。

    您可以按照here 的描述使用RuntimeTypeAdapterFactory - 不幸的是,它没有与基本 Gson 模块一起发布,也没有像 here 描述的那样在 Maven Central 中发布。这将使用 JSON 发布足够的信息,从而允许 Gson 对其进行反序列化。

    【讨论】:

    • 好点,但不幸的是,在我们的代码库中,Gson 实例进入了一些“通用”路径,我无法为这种特定情况添加自定义类型适配器。
    • 我认为那是不可能的。 Gson 无法在反序列化路径上知道所讨论的类型是什么,因此它将默认为最安全的路线:( 在运行时,您能否 100% 确定您要序列化的唯一类型是 ChildBean(而不是其他后代,例如 AnotherChildBeanType、OneMoreChildBeanType 等)?如果是这样,我之前已经部署了一个 hacky 解决方法。
    【解决方案3】:

    更多附录:我只是认为至少序列化与 arrays 可以正常工作,所以一个简单的解决方法是重新处理持有者:

    static class BaseBeanHolder {
        BaseBean[] beans;
        public BaseBeanHolder(BaseBean... beans) { this.beans = beans; }
    }
    

    【讨论】:

      猜你喜欢
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 2011-09-18
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 2013-08-21
      相关资源
      最近更新 更多