IllegalStateException – Serializing a map with a String array using Gson

Here is the full error I am receiving:

com.google.gson.JsonSyntaxException: java.lang.IllegalStateException: Expected BEGIN_ARRAY but was STRING at line 1 column 3 path $.

The map I am trying to encode is a

HashMap<String[],Boolean>.

I use this to serialize the map:

Gson gson = new Gson();
gson.toJson(object);

and this to deserialize the object:

json.fromJson(encodedObject, new TypeToken<HashMap<String[], Boolean>>(){}.getType())

This is the raw JSON output:

{
     "[Ljava.lang.String;@433fe238":false,
     "[Ljava.lang.String;@433feb38":false,
     "[Ljava.lang.String;@433fe018":false,
     "[Ljava.lang.String;@433fd618":false
}

I have other maps that I encode and decode in a similar way, but this is the only one that is giving me issues. Is there a way to make this work?

Answer

It’s unfortunate you can’t change the type of elements in the data-structure. You should likely never store arrays as keys as they don’t override equals, hashcode and toString.

That said let’s talk about your error. It’s due to the fact that Gson serialize the key as a String, because the JSON grammar requires that in a key-value pair, the key must be a string. But you expect an array as a key so the parser throws the error.

Since you cannot change the structure, you can write a custom serializer and deserializer to handle this:

class MapSerializer implements JsonSerializer<Map<String[], Boolean>> {
    @Override
    public JsonElement serialize(Map<String[], Boolean> src, Type typeOfSrc, JsonSerializationContext context) {
        JsonObject obj = new JsonObject();
        for(Map.Entry<String[], Boolean> entry : src.entrySet()) {
            obj.addProperty(Arrays.toString(entry.getKey()), entry.getValue());
        }
        return obj;
    }
}

class MapDeserializer implements JsonDeserializer<Map<String[], Boolean>> {
    @Override
    public Map<String[], Boolean> deserialize(JsonElement json, Type typeOfT, JsonDeserializationContext context) throws JsonParseException {
        Map<String[],Boolean> map = new HashMap<>();
        JsonObject obj = json.getAsJsonObject();
        for(Map.Entry<String, JsonElement> entry : obj.entrySet()) {
            String s = entry.getKey();
            //See how you're dependent of the String representation given by Arrays.toString? 
            //Not very convenient.
            map.put(s.substring(1, s.length()-1).split(", "), entry.getValue().getAsBoolean());
        }
        return map;
    }
}

Basically the key is now given by Arrays.toString, which is a readable representation of the elements in the array, instead of its toString method.

But as you see the deserialization process has to assume that this representation won’t change, this is quite risky as any modification in the result of Arrays.toString would break the process, but you have to deal with it… (although it’s unlikely to happen but this is a fact that you have to be aware of)

To make this work, you need to register the adapters in the parser.

Map<String[],Boolean> map = new HashMap<>();
map.put(new String[]{"Hello"},false);
map.put(new String[]{"Stack", "Overflow"},true);

Type t = new TypeToken<Map<String[], Boolean>>(){}.getType();

Gson gson = new GsonBuilder().registerTypeAdapter(t, new MapSerializer())
                             .registerTypeAdapter(t, new MapDeserializer())
                             .setPrettyPrinting()
                             .create();

String repr =  gson.toJson(map, t);
Map<String[], Boolean> map2 = gson.fromJson(repr, t);

If you try to print repr, you’ll get:

{
  "[Hello]": false,
  "[Stack, Overflow]": true
}

which is better than your original output. You get back the “same” map with fromJson (I quoted “same” because the keys are equals according to Arrays.equals not .equals()).

Hope it helps!

Leave a Reply

Your email address will not be published. Required fields are marked *