JonasB
JonasB

Reputation: 141

Gson deserialize map of objects to map of strings

I have a bit of a problem concerning JSON deserialization. Background: The response is coming from a graph database (ArangoDB, so its JavaScript serverside), which I would usually process using JavaScript clientside. This works quite nicely, especially since the JavaScript concept of objects has proven quite useful (for me) when querying graph data. I. e. I can attach edges and nodes in whatever way seems useful for one particular endpoint.

This all works nicely, but I am starting to do some performance testing of the server and have decided to use a simple Java application for that. But since I don't really on strong typing, I'm having trouble getting this to work. Basically what I want is to imitate the JavaScript concept and to deserialize any object into a Map. Then I'd deserialize properties, that are object once again using the same mechanism and properties that are list as a List. This needn't be manifest in the object structure, I'd do that on the fly in temporary variables (clientside performance doesn't matter).

The problem is Gson doesn't deserialize (and I know, it shouldn't), f.e.

{"a": {"b":"c"}}

using

private static final Type TYPE = new TypeToken<Map<String, String>>(){}.getType();

to the map "a"->"{\"b\":\"c\"}".

Is there any way to Gson to do that or do I have to manually parse the string adding quotation marks and escaping other quotation marks?

I'm really not a fan of RegEx, so any help is appreciated.

Thanks in advance, Jonas

Upvotes: 1

Views: 2479

Answers (1)

Lyubomyr Shaydariv
Lyubomyr Shaydariv

Reputation: 21145

I seem to understand your problem better now. Still not sure about the real purpose of your code, so I would like to make some assumptions here since the comments are too short.

Unfortunately, Java static typing nature doesn't let you write that short code as you could write in JavaScript. However, you can either follow the idiomatic way, or make a sort of of a workaround.

Simple Java

The reason of why you can't use new TypeToken<Map<String, String>>(){}.getType() is that GSON expects to parse always a plain string-to-string map (Map<String,String>). Thus {"a": "b"} is fine, but {"a":{"b":"c"}} is not. While parsing such a JSON, you should make some assumptions/expectations on the every map value type. By default, GSON uses Map<String, Object mapping for the Map.class target type, so the following code works and you have to check the value types yourself in order to check if you can go deeper.

final Gson gson = new Gson();
final String json = "{\"a\": {\"b\":\"c\"}}";
@SuppressWarnings("unchecked")
final Map<String, Object> outerMap = gson.fromJson(json, Map.class);
@SuppressWarnings("unchecked")
final Map<String, String> innerMap = (Map<String, String>) outerMap.get("a");
out.println(innerMap.get("b"));

c

Note the type casts above and the fact taht (Map<String, String>) outerMap.get("a") can work only if an a child is really a Map (no matter what it is parameterized with due to the nature of generics in Java).

Alternative: a wrapper

The following way reconsiders the object model you're trying to deserialize. However, the this way can be an overkill for you, but it makes the type analysis itself getting rid of manual type checking. Another disadvantage is that it requires another two different maps to be created (but at least it does it once). An advantage is that you can distiguish between the "nodes" and "values" maps in a more or less convenient way. The following wrapper implementation loses the original Map<String, ...> interface, but you could make it implement it if it's fine (it will definitely require to find a compromise between the API-fullness and performance/memory hits)

final class Wrapper {

    private final Map<String, String> values;
    private final Map<String, Wrapper> wrappers;

    private Wrapper(final Map<String, String> values, final Map<String, Wrapper> wrappers) {
        this.values = values;
        this.wrappers = wrappers;
    }

    static Wrapper wrap(final Map<String, ?> map) {
        final Map<String, String> values = new LinkedHashMap<>();
        final Map<String, Wrapper> wrappers = new LinkedHashMap<>();
        for ( final Entry<String, ?> e : map.entrySet() ) {
            final String key = e.getKey();
            final Object value = e.getValue();
            if ( value instanceof String ) {
                values.put(key, (String) value);
            } else if ( value instanceof Map ) {
                @SuppressWarnings("unchecked")
                final Map<String, ?> m = (Map<String, ?>) value;
                wrappers.put(key, wrap(m));
            } else {
                throw new IllegalArgumentException("The value has inappropriate type: " + value.getClass());
            }
        }
        return new Wrapper(values, wrappers);
    }

    String valueBy(final String key) {
        return values.get(key);
    }

    Wrapper wrapperBy(final String key) {
        return wrappers.get(key);
    }

}

And then:

final Gson gson = new Gson();
final String json = "{\"a\": {\"b\":\"c\"}}";
@SuppressWarnings("unchecked")
final Map<String, ?> outerMap = (Map<String, ?>) gson.fromJson(json, Map.class);
final Wrapper outerWrapper = wrap(outerMap);
final Wrapper innerWrapper = outerWrapper.wrapperBy("a");
out.println(innerWrapper.valueBy("b"));

c

The wrapper deserializer

Manual wrapping is fine, but you can make it even more "Gson-ish" using the wrapper deserializer:

final class WrapperDeserializer
        implements JsonDeserializer<Wrapper> {

    private final Gson backingGson;

    private WrapperDeserializer(final Gson backingGson) {
        this.backingGson = backingGson;
    }

    static JsonDeserializer<Wrapper> getWrapperDeserializer(final Gson backingGson) {
        return new WrapperDeserializer(backingGson);
    }

    @Override
    public Wrapper deserialize(final JsonElement json, final Type type, final JsonDeserializationContext context) {
        @SuppressWarnings("unchecked")
        final Map<String, ?> map = backingGson.fromJson(json, Map.class);
        return wrap(map);
    }

}

Note that it uses a backing Gson instance to deserilize Map instances (json.getAsJsonObject().entrySet() will not work because it will contain JsonObject instances as values thus making the Wrapper.wrap method GSON-aware without any real reason -- so that's why it's better to convert it to a "native" JDK class). And then:

final Gson gson = new GsonBuilder()
        .registerTypeAdapter(Wrapper.class, getWrapperDeserializer(new Gson()))
        .create();
final String json = "{\"a\": {\"b\":\"c\"}}";
final Wrapper outerWrapper = (Wrapper) gson.fromJson(json, Wrapper.class);
final Wrapper innerWrapper = outerWrapper.wrapperBy("a");
out.println(innerWrapper.valueBy("b"));

c


And, as the conclusion, static-typing requires more work and may look not very convenient for some cases.

Upvotes: 2

Related Questions