12

I want to serialize my Example class below into JSON using GSON.

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

I expected to get the following output:

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

But calling the function toJSON() returns

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

It seems that GSON cannot serialize the Map Range inside the Map General. Is this a limitation of GSON or am I doing something wrong here?

StaxMan
  • 113,358
  • 34
  • 211
  • 239
asmaier
  • 11,132
  • 11
  • 76
  • 103
  • 2
    If other answers are correct and default Map serializer won't handle nested maps, I would file a bug report for GSON team. It seems like really bad defaults to me; at least it ought to throw an exception instead of quietly swallowing contents. – StaxMan Dec 28 '10 at 18:27
  • +1 @StaxMan it seems like that. I have seen this earlier, came up with some work-around. If there is some way to do this, it's worth documentation. – Nishant Dec 28 '10 at 18:38

3 Answers3

11

The reason why Nishant's answer works is because Gson's default constructor enables all kind of stuff per default that you would otherwise have to manually enably using the GsonBuilder.

From the JavaDocs:

Constructs a Gson object with default configuration. The default configuration has the following settings:

  • The JSON generated by toJson methods is in compact representation. This means that all the unneeded white-space is removed. You can change this behavior with GsonBuilder.setPrettyPrinting().
  • The generated JSON omits all the fields that are null. Note that nulls in arrays are kept as is since an array is an ordered list. Moreover, if a field is not null, but its generated JSON is empty, the field is kept. You can configure Gson to serialize null values by setting GsonBuilder.serializeNulls().
  • Gson provides default serialization and deserialization for Enums, Map, java.net.URL, java.net.URI, java.util.Locale, java.util.Date, java.math.BigDecimal, and java.math.BigInteger classes. If you would prefer to change the default representation, you can do so by registering a type adapter through GsonBuilder.registerTypeAdapter(Type, Object).
  • The default Date format is same as java.text.DateFormat.DEFAULT. This format ignores the millisecond portion of the date during serialization. You can change this by invoking GsonBuilder.setDateFormat(int) or GsonBuilder.setDateFormat(String).
  • By default, Gson ignores the com.google.gson.annotations.Expose annotation. You can enable Gson to serialize/deserialize only those fields marked with this annotation through GsonBuilder.excludeFieldsWithoutExposeAnnotation().
  • By default, Gson ignores the com.google.gson.annotations.Since annotation. You can enable Gson to use this annotation through GsonBuilder.setVersion(double).
  • The default field naming policy for the output Json is same as in Java. So, a Java class field versionNumber will be output as "versionNumber@quot; in Json. The same rules are applied for mapping incoming Json to the Java classes. You can change this policy through GsonBuilder.setFieldNamingPolicy(FieldNamingPolicy).
  • By default, Gson excludes transient or static fields from consideration for serialization and deserialization. You can change this behavior through GsonBuilder.excludeFieldsWithModifiers(int).

OK, now I see what the problem is. The default Map serializer, as you expected, does not support nested maps. As you can see in this source snippet from DefaultTypeAdapters (especially if you step through with a debugger) the variable childGenericType is set to the type java.lang.Object for some mysterious reason, so the runtime type of the value is never analysed.

Two solutions, I guess:

  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();
    }
    
Community
  • 1
  • 1
Sean Patrick Floyd
  • 292,901
  • 67
  • 465
  • 588
4

Try this:

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

Not sure if you're still looking for a solution, this works for me:

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

}

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


Obviously you could use ImmutableMap.copyOf(your_hashmap)here instead

Nishant
  • 54,584
  • 13
  • 112
  • 127
  • 1
    It works, but why? Also it is a bit inconvenient. If I want to add more maps to my class Example, I have to write in my function toJSON() something like `String out=gson.toJson(this.General); out+=gson.toJson(this.Map2); out+=gson.toJson(this.map3); ... return out;` – asmaier Dec 28 '10 at 17:11
  • not sure buddy. I had this issue, I went down this route. I assumed the cause to be this http://sites.google.com/site/gson/gson-user-guide#TOC-Nested-Classes-including-Inner-Clas – Nishant Dec 28 '10 at 17:22
  • Sorry, my first comment was wrong and I cannot edit it anymore. Using `gson.toJson(General)` returns `{"Version":"0.1","Range":{"Start_Time":"bla","End_Time":"blu"},"Checksum":"+1"}`. But I want to have `{"General": {"Version":"0.1",....` . so your solution doesn't work completely for me. – asmaier Dec 28 '10 at 17:25
  • weirdly, I have literally written down the code and executed. I do not see checksum. Is it a field coming from somewhere? The output is `{"Version":"0.1","Range":{"Start_Time":"now","End_Time":"never"}}` but that does not solve your problem obviously. – Nishant Dec 28 '10 at 17:47
  • well edited with some extra stuffs. But, couldn;t really get how immutable gets the work done – Nishant Dec 28 '10 at 18:23
  • Ignore the field `Checksum`. I added this field for my local test version and forgot to remove it from the result before posting the comment here. – asmaier Dec 28 '10 at 20:26
1

A simpler alternative would be to use Jackson instead of GSON, serialization of a nested map works out of the box:

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

Output:

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

jjoller
  • 661
  • 9
  • 17