如何使用GSON序列化Map of a Map?

12

我想使用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 对象进行序列化,这是 GSON 的限制还是我的操作有误?


2
如果其他答案是正确的,并且默认的Map序列化程序无法处理嵌套映射,那么我会为GSON团队提交一个错误报告。对我来说,这似乎是非常糟糕的默认设置;至少它应该抛出异常而不是悄悄地吞噬内容。 - StaxMan
+1 @StaxMan 看起来是这样。我之前见过这种情况,想出了一些解决方法。如果有办法做到这一点,值得记录下来。 - Nishant
3个回答

11

Nishant的回答之所以有效,是因为Gson的默认构造函数默认启用了所有种类的东西,否则您将不得不使用GsonBuilder手动启用。

JavaDocs中:

使用默认配置构造Gson对象。默认配置有以下设置:
  • toJson方法生成的JSON处于紧凑表示形式。这意味着所有不必要的空格都被删除了。您可以使用GsonBuilder.setPrettyPrinting()更改此行为。
  • 生成的JSON省略了所有为空的字段。请注意,数组中的null保持不变,因为数组是一个有序列表。此外,如果一个字段不为空,但其生成的JSON为空,则该字段将被保留。您可以通过设置GsonBuilder.serializeNulls()来配置Gson以序列化null值。
  • Gson为枚举、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@quot;。将相同的规则应用于将传入的Json映射到Java类。您可以通过GsonBuilder.setFieldNamingPolicy(FieldNamingPolicy)更改此策略。
  • 默认情况下,Gson排除瞬态或静态字段不考虑进行序列化和反序列化。您可以通过GsonBuilder.excludeFieldsWithModifiers(int)更改此行为。

好的,现在我知道问题出在哪里了。默认的Map序列化器不支持嵌套映射,正如你所预期的那样。你可以在从DefaultTypeAdapters的源代码片段中看到(特别是如果你通过调试器逐步执行),变量childGenericType由于某种神秘的原因被设置为类型java.lang.Object,因此值的运行时类型永远不会被分析。

我想有两种解决方案:

  1. Implement your own Map serializer / deserializer
  2. Use a more complicated version of your method, something like this:

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

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

4

试试这个:

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

}

返回值: {"General":{"版本":"0.1","范围":{"开始时间":"现在","结束时间":"永远"}}}


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


1
它能工作,但是为什么呢?而且有点不方便。如果我想要将更多的地图添加到我的Example类中,我必须在我的函数toJSON()中写入类似于String out=gson.toJson(this.General); out+=gson.toJson(this.Map2); out+=gson.toJson(this.map3); ... return out;这样的代码。 - asmaier
不确定,伙计。我遇到了这个问题,然后我走了这条路线。我认为原因是这个:http://sites.google.com/site/gson/gson-user-guide#TOC-Nested-Classes-including-Inner-Clas - 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
忽略“校验和”字段。我在本地测试版本中添加了此字段,但在发布评论之前忘记将其从结果中删除。 - asmaier

1

一个更简单的选择是使用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);

输出:

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


网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接