0

I have a string of JSON that I use Gson to convert to JSON Object. However, I'd like to use containsKey as a case insensitive way.

Here's what I'm doing right now.

    ByteBuffer buffer = ByteBuffer.wrap(payload);
    String data = forName("UTF-8").newDecoder().decode(buffer).toString();

    Gson gson = new Gson();
    Map<String, String> map = gson.fromJson(data, new TypeToken<HashMap<String, Object>>() {
    }.getType());

And then I have this method isTupleValid to check if the tuple is valid.

private boolean isTupleValid(Map<String, String> map) {
    return map != null && map.containsKey(TYPE)
            && map.containsKey(XID)
            && map.containsKey(CTP)
            && map.get(TYPE).equals("pageview");
}

However, sometimes the json string could be

{"xid": "xid"} or it can be {"xID":"xid"}

Is there a way to use containsKey in a case sensitive way?

toy
  • 11,711
  • 24
  • 93
  • 176
  • http://stackoverflow.com/questions/3092710/how-to-check-for-key-in-a-map-irrespective-of-the-case – Andrew V Sep 21 '16 at 18:54
  • I tried that and it didn't work in Gson case. Map mapClass = new TreeMap<>(INSTANCE); Map map = gson.fromJson(data, mapClass.getClass()); – toy Sep 21 '16 at 19:04
  • hmm that INSTANCE looks rather weird to me but can you try this: Map map = new TreeMap<>(String.CASE_INSENSITIVE_ORDER); – Andrew V Sep 21 '16 at 19:10
  • I tried this Map mapClass = new TreeMap<>(CASE_INSENSITIVE_ORDER); Map map = gson.fromJson(data, mapClass.getClass()); Still doesn't work. – toy Sep 21 '16 at 19:11
  • can you do some debug and check out what is exactly going wrong? – Andrew V Sep 21 '16 at 19:17
  • I did some debugging. I can see that GSON converts to TreeMap. However, when I use `containsKey`. It has to be exact match. – toy Sep 21 '16 at 19:18

2 Answers2

0

gson.fromJson(data, new TreeMap<>(...).getClass()) does not make much sense since getClass() only returns the object class, and not its internal state -- simply speaking, you always get TreeMap.class regardless the arguments passed to the TreeMap constructor.

To me, there are two options. They two use "normalized" maps and have their own advantages and disadvantages.

First off, let's create the commons class in order to share the basics between two approaches demos:

Commons.java

final class Commons {

    private Commons() {
    }

    static final String CTP = "ctp";
    static final String TYPE = "type";
    static final String XID = "xid";

    static final String JSON_1 = "{\"xid\":\"foo\",\"type\":\"pageview\",\"ctp\":\"ctp\"}";
    static final String JSON_2 = "{\"xID\":\"foo\",\"tYPE\":\"pageview\",\"cTP\":\"ctp\"}";

    static Map<String, Object> normalizeMap(final Map<String, Object> map) {
        final Map<String, Object> normalized = new TreeMap<>(CASE_INSENSITIVE_ORDER);
        normalized.putAll(map);
        return normalized;
    }

}

CaseInsensitive1.java

This option does not touch the original deserialization strategy, so the consuming code must check against case-insensitivity itself. It's necessary to put the "map normalization" code where it makes sense. Note that the original map remains unchanged.

public final class CaseInsensitive1 {

    private CaseInsensitive1() {
    }

    public static void main(final String... args) {
        final Gson gson = new Gson();
        @SuppressWarnings("unchecked")
        final Map<String, Object> map1 = gson.fromJson(JSON_1, Map.class);
        @SuppressWarnings("unchecked")
        final Map<String, Object> map2 = gson.fromJson(JSON_2, Map.class);
        out.println(isTupleValid(map1));
        out.println(isTupleValid(map2));
    }

    private static boolean isTupleValid(final Map<String, Object> map) {
        if ( map == null ) {
            return false;
        }
        final Map<String, Object> ciMap = normalizeMap(map);
        return ciMap.containsKey(TYPE)
                && ciMap.containsKey(XID)
                && ciMap.containsKey(CTP)
                && ciMap.get(TYPE).equals("pageview");
    }

}

CaseInsensitive2.java

This option creates a special GSON instance that deserializes Map<String, Object> in a "normalized" way. Note that mapStringObjectType is used everywhere since I mapped the deserialization strategy for that concrete type. It might be mapped just to Map.class of course. The decorateGson takes another GSON instance in order to "inherit" the previosly built behavior you might need. Despite the isTupleValid method is now equivalent to yours, a disadvantage of this option is that you lose the properties order of the original JSON object. However, you might want to use case-insensitive maps not based on TreeMap (not sure, but it could make the map behave in case-insensitive way keeping the original properties order).

public final class CaseInsensitive2 {

    private CaseInsensitive2() {
    }

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

    public static void main(final String... args) {
        final Gson originalGson = new Gson();
        final Gson gson = decorateGson(originalGson);
        @SuppressWarnings("unchecked")
        final Map<String, Object> map1 = gson.fromJson(JSON_1, mapStringObjectType);
        @SuppressWarnings("unchecked")
        final Map<String, Object> map2 = gson.fromJson(JSON_2, mapStringObjectType);
        out.println(isTupleValid(map1));
        out.println(isTupleValid(map2));
    }

    private static boolean isTupleValid(final Map<String, Object> map) {
        return map != null
                && map.containsKey(TYPE)
                && map.containsKey(XID)
                && map.containsKey(CTP)
                && map.get(TYPE).equals("pageview");
    }

    private static Gson decorateGson(final Gson originalGson) {
        @SuppressWarnings({ "unchecked", "rawtypes" })
        final TypeAdapter<Map<String, Object>> adapter = (TypeAdapter) originalGson.getAdapter(Map.class);
        final JsonDeserializer<Map<String, Object>> typeAdapter = (json, type, context) -> normalizeMap(adapter.fromJsonTree(json));
        return new GsonBuilder()
                .registerTypeAdapter(mapStringObjectType, typeAdapter)
                .create();
    }

}

To me, the option #1 looks better, since it does not decorate the originally deserialized object in any way (what if I need it to be case-sensitive later in the code?) and the case-insensitive check is performed only when it's really needed.

Community
  • 1
  • 1
Lyubomyr Shaydariv
  • 20,327
  • 12
  • 64
  • 105
0

If you just need to check if the key exist without retrieving the value. You could access the keySet. Stream over it and call anyMatch with an ignoreCase predicate. In your case :

return ciMap.keySet().stream().anyMatch(key -> key.equalsIgnoreCase(TYPE))
                && ciMap.keySet().stream().anyMatch(key -> key.equalsIgnoreCase(XID))
                && ciMap.keySet().stream().anyMatch(key -> key.equalsIgnoreCase(CTP))
                && ciMap.get(TYPE).equals("pageview");

PS : You may consider using an appropriate data structure. The keys in this case could not be equal and nevertheless return true. This violates the Map contract.

Mehdi
  • 1,494
  • 5
  • 33
  • 53