Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

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}"

like image 868
Nouvel Travay Avatar asked Nov 15 '16 23:11

Nouvel Travay


2 Answers

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:
     * Starts with [
     * <a non empty sequence of digit characters>,
     * <a non empty sequence of any characters
     * Ends with ]
     */
    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);
like image 85
Nicolas Filotto Avatar answered Oct 29 '22 06:10

Nicolas Filotto


According to the documentation for Map.Entry:

The only way to obtain a reference to a map entry is from the iterator of this collection-view.

https://docs.oracle.com/javase/8/docs/api/java/util/Map.Entry.html

This means you can't get the Map.Entry until you've created the initial Map. In order to achieve what you want you would need to parse the JSON into a Map, then iterate over it to insert it into your MyDS object.

Having said that, depending on your end use there might be better ways of re-organising/keying the data after parsing it.

like image 1
Geoff Avatar answered Oct 29 '22 06:10

Geoff