17

I have json object with arbitary values inside. And I want to deserialize it in a Map. Everything is ok except converting integers to a doubles. See example:

{"id":1, "inner_obj":{"key":"value","num":666,"map":{"key":"value"}}}

deserializes to this(map.toString()):

{id=1.0, inner_obj={key=value, num=666.0, map={key=value}}}

Is there some easy way to deserialize "id" and "num" as Integers and not as Doubles?

Moses
  • 1,243
  • 3
  • 14
  • 21
  • Also see [this answer](http://stackoverflow.com/a/43346123/179864) I wrote for a similar question; the catch is that you would have to parse the data as an `Object` and then cast to whatever you need. – aditsu quit because SE is EVIL Apr 11 '17 at 13:18
  • Jackson does a much better job at detecting types for maps, and it is far easier to configure than GSON: https://github.com/FasterXML/jackson-databind – Tom Carchrae Jun 13 '13 at 15:22
  • 2
    The question explicitly asked for Gson implementation. Please stick to the question. – Megakoresh Aug 12 '16 at 10:52

9 Answers9

8

There are no integer type in JSON. 1 and 1.0 are the same. You need to parse that 1.0 to 1 in your code. Or you need to map the JSON to some VO class and define the type of fields of the class explicitly , so that GSON can understand what you are looking for.

AllTooSir
  • 48,828
  • 16
  • 130
  • 164
  • 4
    But in java there are Integers and Doubles. And it's easy to deserialize {"num":1} to Integer if I have a class with Integer field "num", but when gson deserializes json to Map it desides to use Double. So I'am seeking the way to disable this approach. – Moses Jun 13 '13 at 15:17
  • @Moses define a Map – John Vint Jun 13 '13 at 17:28
  • 4
    Map should contain not only integers. – Moses Jun 14 '13 at 06:56
5

Been searching for a solution to the nested Map problem myself and "이종은" above was the first answer that actually helped in the non trivial use cases.

Since the solution above only handled Number I updated the solution to provide generic parsing capability for String and booleans also, see the updated code below:

private static class MapDeserializer implements JsonDeserializer<Map<String, Object>> {

    public Map<String, Object> deserialize(JsonElement json, Type typeOfT, JsonDeserializationContext context) throws JsonParseException {
        Map<String, Object> m = new LinkedHashMap<String, Object>();
        JsonObject jo = json.getAsJsonObject();
        for (Entry<String, JsonElement> mx : jo.entrySet()) {
            String key = mx.getKey();
            JsonElement v = mx.getValue();
            if (v.isJsonArray()) {
                m.put(key, g.fromJson(v, List.class));
            } else if (v.isJsonPrimitive()) {
                Number num = null;
                ParsePosition position=new ParsePosition(0);
                String vString=v.getAsString();
                try {
                  num = NumberFormat.getInstance(Locale.ROOT).parse(vString,position);
                } catch (Exception e) {
                }
                //Check if the position corresponds to the length of the string
                if(position.getErrorIndex() < 0 && vString.length() == position.getIndex()) {
                  if (num != null) {
                    m.put(key, num);
                    continue;
                  }
                }
                JsonPrimitive prim = v.getAsJsonPrimitive();
                if (prim.isBoolean()) {
                    m.put(key, prim.getAsBoolean());
                } else if (prim.isString()) {
                    m.put(key, prim.getAsString());
                } else {
                    m.put(key, null);
                }

            } else if (v.isJsonObject()) {
                m.put(key, g.fromJson(v, Map.class));
            }

        }
        return m;
    }
}

private static class ListDeserializer implements JsonDeserializer<List<Object>> {

    public List<Object> deserialize(JsonElement json, Type typeOfT, JsonDeserializationContext context) throws JsonParseException {
        List<Object> m = new ArrayList<Object>();
        JsonArray arr = json.getAsJsonArray();
        for (JsonElement jsonElement : arr) {
            if (jsonElement.isJsonObject()) {
                m.add(g.fromJson(jsonElement, Map.class));
            } else if (jsonElement.isJsonArray()) {
                m.add(g.fromJson(jsonElement, List.class));
            } else if (jsonElement.isJsonPrimitive()) {
                Number num = null;
                try {
                    num = NumberFormat.getInstance().parse(jsonElement.getAsString());
                } catch (Exception e) {
                }
                if (num != null) {
                    m.add(num);
                    continue;
                }
                JsonPrimitive prim = jsonElement.getAsJsonPrimitive();
                if (prim.isBoolean()) {
                    m.add(prim.getAsBoolean());
                } else if (prim.isString()) {
                    m.add(prim.getAsString());
                } else {
                    m.add(null);
                }
            }
        }
        return m;
    }
}

private static Gson g = new GsonBuilder().registerTypeAdapter(Map.class, new MapDeserializer()).registerTypeAdapter(List.class, new ListDeserializer()).setDateFormat("yyyy-MM-dd HH:mm:ss").create();
  • This helped me a lot! I would just like a point out a small bug. In the ListDeserializer you should remove "m.add(num)" in the try{ } block, because it will be added anyway after the try/catch. If you use the code as-is, it will add each number to the result twice. – peanutman Nov 20 '17 at 11:28
  • 1
    I think you can replace the calls to g.fromJson(...) to context.deserialize(...) to avoid the need for a static instance of gson (at least it works for me for gson 2.6.2) – David Feb 16 '18 at 00:25
3

JSON only has a single Number type and there is no way for the parser to automatically tell what type to convert it to.

If you aren't going to use a strongly typed object graph, consider using the JsonElement types:

JsonObject root = new Gson().fromJson(json, JsonObject.class);
int num = root.getAsJsonObject("inner_obj").get("num").getAsInt();
McDowell
  • 107,573
  • 31
  • 204
  • 267
3

It's my code

private static class MapDeserializer implements JsonDeserializer<Map<String,Object>> {
    public Map<String,Object> deserialize(JsonElement json, Type typeOfT, JsonDeserializationContext context)  throws JsonParseException {
        Map<String,Object> m  = new LinkedHashMap<String, Object>();
        JsonObject jo = json.getAsJsonObject();
        for (Entry<String, JsonElement>  mx : jo.entrySet()){
            String key = mx.getKey();
            JsonElement v = mx.getValue();
            if(v.isJsonArray()){
                m.put(key, g.fromJson(v, List.class));
            }else if(v.isJsonPrimitive()){
                 Number num = null;
                  try {
                      num = NumberFormat.getInstance().parse(v.getAsString());
                  } catch (Exception e) {
                      e.printStackTrace();
                  }
                m.put(key,num);
            }else if(v.isJsonObject()){
                m.put(key,g.fromJson(v, Map.class));    
            }

        }
      return m;
   }
}

private static class ListDeserializer implements JsonDeserializer<List<Object>> {
    public List<Object> deserialize(JsonElement json, Type typeOfT, JsonDeserializationContext context)  throws JsonParseException {
        List<Object> m  = new ArrayList<Object>();
        JsonArray arr = json.getAsJsonArray();
        for (JsonElement jsonElement : arr) {
            if(jsonElement.isJsonObject()){
                m.add(g.fromJson(jsonElement, Map.class));
            }else if(jsonElement.isJsonArray()){
                m.add(g.fromJson(jsonElement, List.class));
            }else if(jsonElement.isJsonPrimitive()){
                Number num = null;
                try {
                  num = NumberFormat.getInstance().parse(jsonElement.getAsString());
                } catch (Exception e) {
                }
                m.add(num);
            }
        }
      return m;
   }
}

private static Gson g = new GsonBuilder().registerTypeAdapter(Map.class, new MapDeserializer()).registerTypeAdapter(List.class, new ListDeserializer()).setDateFormat("yyyy-MM-dd HH:mm:ss").serializeNulls().create();
이종은
  • 31
  • 1
2

To avoid possible ClassCastException, it is better to cast to Number first. In the following code map was deserialized from JSON as a Map with no generics.

int numberOfPages = ((Number) map.get("number_of_pages")).intValue();
hrzafer
  • 1,123
  • 1
  • 15
  • 35
0

Here is my example, the first part is the definition of the class that has an int type field.

import com.google.api.client.util.Key;

public class Folder {

    public static final String FIELD_NAME_CHILD_COUNT = "childCount";

    @Key(FIELD_NAME_CHILD_COUNT)
    public final int childCount;

    public Folder(int aChildCount) {
        childCount = aChildCount;
    }
}

Then the TypeAdapter to convert the number type in Gson to a Java object.

GsonBuilder gsb = new GsonBuilder();

gsb.registerTypeAdapter(Folder.class, new JsonDeserializer<Folder>() {

            @Override
            public Folder deserialize(JsonElement json, Type typeOfT, JsonDeserializationContext context) throws JsonParseException {

                int value = json.getAsJsonObject().get("childCount").getAsJsonPrimitive().getAsInt();

                return new Folder(value);

            }
        }
);

The third part is the test data, and it works.

String gsonData =  new String("\"folder\":{\"childCount\":0}");
Zephyr
  • 6,123
  • 34
  • 33
0

Register type adapter for Map:

   Gson gson = new GsonBuilder()
            .registerTypeAdapter(Map.class, new JsonDeserializer<Map>() {
                @Override
                public Map deserialize(JsonElement json, Type typeOfT, JsonDeserializationContext context) throws JsonParseException {
                    LinkedTreeMap<String,Object> m  = new LinkedTreeMap<String, Object>();
                    JsonObject jo = json.getAsJsonObject();
                    for (Map.Entry<String, JsonElement> mx : jo.entrySet()){
                        String key = mx.getKey();
                        JsonElement v = mx.getValue();
                        if(v.isJsonArray()){
                            m.put(key, context.deserialize(v, List.class));
                        }else if(v.isJsonPrimitive()){
                            Object value = v.getAsString();
                            try {
                                Object numValue = NumberFormat.getInstance().parse((String)value);
                                if (numValue != null && numValue.toString().equals(value)) {
                                    value = numValue;
                                }
                            } catch (Exception e) {
                            }
                            m.put(key, value);
                        }else if(v.isJsonObject()){
                            m.put(key,context.deserialize(v, Map.class));
                        }
                    }
                    return m;
                }
            })
            .create();

and deserialize using it like: gson.fromJson(instanceJson, Map.class), where instanceJson is an json of object which should be deserialized into Map.

0

here is my solution to solve the Problem. I tried to implement it as clean as possible. I didn' t find a better easy solution to check if a number is an integer.

public final class GSONUtil {

    private GSONUtil() {
    }

    public static Gson createGson() {

        // @formatter:off
        return new GsonBuilder()
                .registerTypeAdapter(Map.class, createMapDeserializer())
                .registerTypeAdapter(List.class, createListDeserializer())
                .create();
        // @formatter:on
    }

    private static JsonDeserializer<Map<String,Object>> createMapDeserializer() {
        return new JsonDeserializer<Map<String,Object>>() {

            @Override
            public Map<String, Object> deserialize(JsonElement json, Type typeOfT, JsonDeserializationContext context)
                    throws JsonParseException {

                return json.getAsJsonObject().entrySet().stream() // stream
                        .collect(Collectors.toMap(Entry::getKey, (e) -> JSONUtil.deserialize(e.getValue(), context)));
            }
        };
    }

    private static JsonDeserializer<List<Object>> createListDeserializer() {
        return new JsonDeserializer<List<Object>>() {

            @Override
            public List<Object> deserialize(JsonElement json, Type typeOfT, JsonDeserializationContext context)
                    throws JsonParseException {

                return StreamSupport.stream(json.getAsJsonArray().spliterator(), false) // stream
                        .map((e) -> JSONUtil.deserialize(e, context)).collect(Collectors.toList());
            }
        };
    }

    private static Object deserialize(JsonElement value, JsonDeserializationContext context) {

        if (value.isJsonNull()) {
            return null;
        }
        if (value.isJsonObject()) {
            return context.deserialize(value, Map.class);
        }
        if (value.isJsonArray()) {
            return context.deserialize(value, List.class);
        }
        if (value.isJsonPrimitive()) {
            return parsePrimitive(value);
        }

        throw new IllegalStateException("This exception should never be thrown!");
    }

    private static Object parsePrimitive(JsonElement value) {

        final JsonPrimitive jsonPrimitive = value.getAsJsonPrimitive();

        if (jsonPrimitive.isString()) {
            return jsonPrimitive.getAsString();
        }

        if (jsonPrimitive.isBoolean()) {
            return jsonPrimitive.getAsBoolean();
        }

        if (jsonPrimitive.isNumber()) {
            return parseNumber(jsonPrimitive);
        }

        throw new IllegalStateException("This exception should never be thrown!");
    }

    private static Number parseNumber(JsonPrimitive jsonPrimitive) {

        if (isInteger(jsonPrimitive)) {
            return jsonPrimitive.getAsLong();
        }

        return jsonPrimitive.getAsDouble();
    }

    private static boolean isInteger(final JsonPrimitive jsonPrimitive) {
        return jsonPrimitive.getAsString().matches("[-]?\\d+");
    }
}
CodingSamples
  • 194
  • 1
  • 1
  • 7
0

This bug has been solved in gson 2.8.9. Look at This PR.

Basically you can define object to number strategy as follows:

GsonBuilder gsonBuilder = new GsonBuilder().setObjectToNumberStrategy(ToNumberPolicy.LONG_OR_DOUBLE);
Gson gson = gsonBuilder.create();
Mohammad hp
  • 446
  • 4
  • 16