问题 如何使用GSON序列化地图的地图?


我想使用GSON将下面的Example类序列化为JSON。

import com.google.gson.Gson;
import com.google.gson.GsonBuilder;
import java.util.LinkedHashMap;

public class Example
{
    private LinkedHashMap<String,Object> General;

    private static final String VERSION="Version";
    private static final String RANGE="Range";
    private static final String START_TIME="Start_Time";
    private static final String END_TIME="End_Time";

    public Example() {
        General = new LinkedHashMap<String,Object>();
        General.put(VERSION, "0.1");

        LinkedHashMap<String,String> Range = new LinkedHashMap<String, String>();
        Range.put(START_TIME, "now");
        Range.put(END_TIME, "never");

        General.put(RANGE, Range);
    }

    public String toJSON() {
        Gson gson = new GsonBuilder().serializeNulls().create();
        return gson.toJson(this);
    }
}

我希望得到以下输出:

{"General":{"Version":"0.1","Range":{"Start_Time":"now","End_Time":"never"}}}

但是调用函数 toJSON() 回报

{"General":{"Version":"0.1","Range":{}}}

似乎GSON无法序列化Map Range 在地图内 General。这是GSON的限制还是我在这里做错了什么?


1170
2017-12-28 16:56


起源

如果其他答案是正确的并且默认的Map serializer不会处理嵌套映射,我会为GSON团队提交错误报告。这对我来说似乎是非常糟糕的默认;至少它应该抛出异常,而不是静静地吞咽内容。 - StaxMan
+1 @StaxMan似乎就是这样。我之前看过这个,想出了一些解决方法。如果有办法做到这一点,那值得记录。 - Nishant


答案:


之所以 Nishant的回答 工作是因为Gson的默认构造函数默认启用了所有类型的东西,否则你必须手动使用GsonBuilder。

来自 的JavaDoc

使用默认配置构造Gson对象。默认配置具有以下设置:

  • toJson方法生成的JSON是紧凑的表示形式。这意味着删除了所有不需要的空白区域。您可以使用GsonBuilder.setPrettyPrinting()更改此行为。
  • 生成的JSON省略了所有null的字段。请注意,由于数组是有序列表,因此数组中的空值保持不变。此外,如果字段不为null,但其生成的JSON为空,则保留该字段。您可以通过设置GsonBuilder.serializeNulls()将Gson配置为序列化空值。
  • Gson为Enums,Map,java.net.URL,java.net.URI,java.util.Locale,java.util.Date,java.math.BigDecimal和java.math.BigInteger类提供默认序列化和反序列化。如果您希望更改默认表示,可以通过GsonBuilder.registerTypeAdapter(Type,Object)注册类型适配器来实现。
  • 默认日期格式与java.text.DateFormat.DEFAULT相同。此格式忽略序列化期间日期的毫秒部分。您可以通过调用GsonBuilder.setDateFormat(int)或GsonBuilder.setDateFormat(String)来更改此设置。
  • 默认情况下,Gson会忽略com.google.gson.annotations.Expose注释。您可以通过GsonBuilder.excludeFieldsWithoutExposeAnnotation()使Gson仅对通过此批注标记的字段进行序列化/反序列化。
  • 默认情况下,Gson会忽略com.google.gson.annotations.Since注释。您可以通过GsonBuilder.setVersion(double)启用Gson以使用此批注。
  • 输出Json的默认字段命名策略与Java中的相同。因此,Java类字段versionNumber将在Json中作为“versionNumber @”输出。应用相同的规则将传入的Json映射到Java类。您可以通过GsonBuilder.setFieldNamingPolicy(FieldNamingPolicy)更改此策略。
  • 默认情况下,Gson从序列化和反序列化的考虑中排除瞬态或静态字段。您可以通过GsonBuilder.excludeFieldsWithModifiers(int)更改此行为。

好的,现在我明白了问题所在。默认的Map序列化程序,如您所料,不支持嵌套映射。正如你所看到的那样 来自DefaultTypeAdapters的此源代码段 (特别是如果你单步使用调试器)变量 childGenericType设置为类型 java.lang.Object 出于某种神秘的原因,因此永远不会分析该值的运行时类型。

我猜两种解决方案:

  1. 实现自己的Map serializer / deserializer 
  2. 使用更复杂的方法版本,如下所示:

    public String toJSON(){
        final Gson gson = new Gson();
        final JsonElement jsonTree = gson.toJsonTree(General, Map.class);
        final JsonObject jsonObject = new JsonObject();
        jsonObject.add("General", jsonTree);
        return jsonObject.toString();
    }
    

8
2017-12-28 17:23



对不起,我对Nishant的回答的第一个评论是错误的。他的建议对我来说并不完全有效。 - asmaier
@asmaier好的,在我的回答中添加了更多内容 - Sean Patrick Floyd
奇怪的是,如果用ImmutableMap替换LinkedHashMap可以正常工作。 - Nishant


尝试这个:

Gson gson = new Gson();
System.out.println(gson.toJson(General));

不确定你是否还在寻找解决方案,这对我有用:

import java.util.LinkedHashMap;

import com.google.common.collect.ImmutableMap;
import com.google.gson.Gson;
import com.google.gson.GsonBuilder;

public class Example {
//  private static LinkedHashMap<String,Object> General;
    private ImmutableMap General;

    private static final String VERSION="Version";
    private static final String RANGE="Range";
    private static final String START_TIME="Start_Time";
    private static final String END_TIME="End_Time";

    public Example() {

        LinkedHashMap<String,String> Range = new LinkedHashMap<String, String>();
        Range.put(START_TIME, "now");
        Range.put(END_TIME, "never");

//        General.put(RANGE, Range);
        General = ImmutableMap.of(VERSION, "0.1", RANGE, Range);
    }

    public String toJSON() {
//        Gson gson = new GsonBuilder().serializeNulls().create();
          Gson gson = new Gson();
          return gson.toJson(this);
    }

}

返回:{“常规”:{“版本”:“0.1”,“范围”:{“Start_Time”:“now”,“End_Time”:“never”}}}


显然你可以使用 ImmutableMap.copyOf(your_hashmap)这里 代替


4
2017-12-28 17:05



它有效,但为什么呢?它也有点不方便。如果我想在我的类Example中添加更多地图,我必须在我的函数中写入JSON()之类的东西 String out=gson.toJson(this.General); out+=gson.toJson(this.Map2); out+=gson.toJson(this.map3); ... return out; - asmaier
不确定伙伴。我有这个问题,我走了这条路。我认为原因是这样的 sites.google.com/site/gson/... - Nishant
对不起,我的第一条评论错了,我再也无法编辑了。运用 gson.toJson(General) 回报 {"Version":"0.1","Range":{"Start_Time":"bla","End_Time":"blu"},"Checksum":"+1"}。但我想拥有 {"General": {"Version":"0.1",.... 。所以你的解决方案对我来说不起作用。 - asmaier
奇怪的是,我已经写下了代码并执行了。我没有看到校验和。它来自某个地方吗?输出是 {"Version":"0.1","Range":{"Start_Time":"now","End_Time":"never"}} 但这显然无法解决您的问题。 - Nishant
用一些额外的东西编辑得很好。但是,无法真正获得完成工作的不可变性 - Nishant


一个更简单的替代方案是使用Jackson而不是GSON,嵌套地图的序列化开箱即用:

    LinkedHashMap<String, Object> general;
    general = new LinkedHashMap<String, Object>();
    general.put("Version", "0.1");

    LinkedHashMap<String, String> Range = new LinkedHashMap<String, String>();
    Range.put("Start_Time", "now");
    Range.put("End_Time", "never");

    general.put("Range", Range);

    // Serialize the map to json using Jackson
    ByteArrayOutputStream os = new ByteArrayOutputStream();
    new org.codehaus.jackson.map.ObjectMapper().writer().writeValue(os,
            general);       
    String json = os.toString();
    os.close();

    System.out.println(json);

输出:

{ “版本”: “0.1”, “范围”:{ “START_TIME”: “现在”, “END_TIME”: “从来没有”}}


1
2017-08-18 10:50