Nouvel Travay Nouvel Travay - 15 days ago 3
Java Question

Convert json to Map.Entry object with Gson

EASY VERSION

If I ask Gson to convert some valid json to MyMap it has no problem doing it

public class MyMap{
Map<Long,String> content;
}


MyMap myMap = gson.fromJson(json, new TypeToken<MyMap>() {}.getType());


HARD VERSION:

How do I get Gson to do the following?

public class MyDS{
Map<Map.Entry<Long,String>,Map<Long,String>> content;
}

MyDS myDS = gson.fromJson(json, new TypeToken<MyDS>() {}.getType());


Example json if you really need it.

"content": {
"[1, dog]": {
"1": "max",
"2": "pi",
"3": "robot",
"4": "catcher",
"5": "reaper"
},
"[2, cat]": {
"6": "black",
"7": "white",
"8": "meow",
"9": "mice",
"10": "rat"
},
"[3, rabbit]": {
"16": "bunny",
"17": "ears",
"28": "burgerbun",
"39": "alice",
"50": "tweak"
}
}


more notes

For good measure, I try to run a unit test where all I do is try to read the json with Gson, and I get the following error trace:

at sun.misc.Unsafe.allocateInstance(Native method)
java.lang.reflect.Method.invoke!(Native method)
com.google.gson.internal.UnsafeAllocator$1.newInstance(UnsafeAllocator.java:48)
com.google.gson.internal.ConstructorConstructor$14.construct(ConstructorConstructor.java:223)
com.google.gson.internal.bind.ReflectiveTypeAdapterFactory$Adapter.read(ReflectiveTypeAdapterFactory.java:207)
com.google.gson.internal.bind.TypeAdapterRuntimeTypeWrapper.read(TypeAdapterRuntimeTypeWrapper.java:40)
com.google.gson.internal.bind.MapTypeAdapterFactory$Adapter.read(MapTypeAdapterFactory.java:186)
com.google.gson.internal.bind.MapTypeAdapterFactory$Adapter.read(MapTypeAdapterFactory.java:145)
com.google.gson.Gson.fromJson(Gson.java:861)
com.google.gson.Gson.fromJson(Gson.java:826)
com.google.gson.Gson.fromJson(Gson.java:775)


It does not matter if the keys are of the form
"[3, rabbit]"
for
"{3, rabbit}"

Answer

Assuming that you have a valid JSON content of type:

{
   "content": {
      "[1, dog]": {
        "1": "max",
        "2": "pi",
        "3": "robot",
        "4": "catcher",
        "5": "reaper"
      },
      "[2, cat]": {
        "6": "black",
        "7": "white",
        "8": "meow",
        "9": "mice",
        "10": "rat"
      },
      "[3, rabbit]": {
        "16": "bunny",
        "17": "ears",
        "28": "burgerbun",
        "39": "alice",
        "50": "tweak"
      }
   }
}

To achieve what you want, you could simply implement your own Map.Entry Deserializer since it cannot be deserialized out of the box because it is not an array and {3, rabbit} is not a valid JSON object.

So your Deserializer could rely on a regular expression to extract the key and the value then create an instance of AbstractMap.SimpleEntry using the extracted values, something like:

public class MapEntryDeserializer implements JsonDeserializer<Map.Entry<Long, String>> {

    /**
     * Pattern corresponding to:
     * [
     * <a non empty sequence of digit characters>, 
     * <a non empty sequence of any characters except "]"
     * ]
     */
    private static final Pattern PATTERN = Pattern.compile("\\[(\\d+), ?([^]]+)\\]");

    public Map.Entry<Long, String> deserialize(JsonElement json, Type typeOfT, 
        JsonDeserializationContext context) throws JsonParseException {
        // Extract the key/value pair from Strings of type [3, rabbit]
        String value = json.getAsString();
        Matcher matcher = PATTERN.matcher(value);
        if (!matcher.find()) {
            throw new JsonParseException(
                String.format("The map entry doesn't have the expected format: %s", value)
            );
        }
        return new AbstractMap.SimpleEntry<>(
            Long.valueOf(matcher.group(1)), matcher.group(2)
        );
    }
}

I can then deserialize my JSON content with:

Type type = new TypeToken<MyDS>() {}.getType();
Gson gson = new GsonBuilder()
    .registerTypeAdapter(Map.Entry.class, new MapEntryDeserializer())
    .create();

MyDS myDS = gson.fromJson(json, type);