简体   繁体   中英

Why does Gson parse an Integer as a Double?

A complex json string and I want to convert it to map, I have a problem.

Please look at this simple test:

public class Test {

    @SuppressWarnings("serial")
    public static void main(String[] args) {
        Map<String, Object> hashMap = new HashMap<String, Object>();
        hashMap.put("data", "{\"rowNum\":0,\"colNum\":2,\"text\":\"math\"}");

        Map<String,Object> dataMap = JsonUtil.getGson().fromJson(
                hashMap.get("data").toString(),new TypeToken<Map<String,Object>>() {}.getType());

        System.out.println(dataMap.toString());

    }
}

result:
console print: {rowNum=0.0, colNum=2.0, text=math}
Int is converted to Double;
Why does gson change the type and how can I fix it?

Gson is a simple parser. It uses always Double as a default number type if you are parsing data to Object .

Check this question for more information: How to prevent Gson from expressing integers as floats

I suggest you to use Jackson Mapper . Jackson distinguish between type even if you are parsing to an Object:

  • "2" as Integer
  • "2.0" as Double

Here is an example:

Map<String, Object> hashMap = new HashMap<String, Object>();
hashMap.put("data", "{\"rowNum\":0,\"colNum\":2,\"text\":\"math\"}");
ObjectMapper mapper = new ObjectMapper();
TypeReference<HashMap<String, Object>> typeRef = new TypeReference<HashMap<String, Object>>() {};

HashMap<String, Object> o = mapper.readValue(hashMap.get("data").toString(), typeRef);

maven:

<dependency>
    <groupId>com.fasterxml.jackson.core</groupId>
    <artifactId>jackson-core</artifactId>
    <version>2.9.0</version>
</dependency>

<dependency>
    <groupId>com.fasterxml.jackson.core</groupId>
    <artifactId>jackson-databind</artifactId>
    <version>2.9.0</version>
</dependency>

JSON makes no distinction between the different type of numbers the way Java does. It sees all kind of numbers as a single type.

That the numbers are parsed as a Double is an implementation detail of the Gson library. When it encounters a JSON number, it defaults to parsing it as a Double .

Instead of using a Map , it would be better to define a POJO that encapsulates all fields of the JSON structure. This makes it much easier to access the data afterwards and the numbers are automatically parsed as an Integer .

class Cell {
    private Integer rowNum;
    private Integer colNum;
    private String text;
}

public static void main(String[] args) throws Exception {
    Map<String, Object> hashMap = new HashMap<String, Object>();
    hashMap.put("data", "{\"rowNum\":0,\"colNum\":2,\"text\":\"math\"}");

    Cell cell = new Gson().fromJson(hashMap.get("data").toString(), Cell.class);
    System.out.println(cell);
}

I needed to get this working as well. It makes sense why GSON does it this way when de-serializing from a JSON string to a Map, since a Double is the largest container for a number.

In my case it is very inconvenient if the types change so I implemented what could be considered a hack, but it works so here it is.

The starting point is a function to take the JSON string and guess if it is a List, Map or a plain String

public static Object parseJson(String json) {
    if (json == null || json.isBlank()) {
        return null;
    }

    var input = json.trim();
    if (input.startsWith("[")) {
        var list = GSON.fromJson(input, List.class);
        for (Object o : list) {
            if (o instanceof Map) {
                visit((Map)o);
            }
        }
        return list;
    } else if (input.startsWith("{")) {
        var map = GSON.fromJson(input, Map.class);
        visit(map);
        return map;
    } else {
        return GSON.fromJson(input, String.class);
    }
}

I then implemented a visitor pattern (I used Java 11 in this case). It is a recursive function that visits every property in the map and if the property is Iterable or another Map it will visit those properties as well.

public static void visit(Map<String, Object> map) {
    for (var entry : map.entrySet()) {
        var value = entry.getValue();
        if (value == null) {
            continue;
        }
        if (value instanceof Map) {
            visit((Map<String, Object>)value);
        }
        if (value instanceof Iterable) {
            var it = (Iterable)value;
            visit(it, o -> {
                if (o instanceof Map) {
                    visit((Map<String, Object>)o);
                }
            });
        }
        if (value instanceof Double) {
            var d = (Double) value;
            var bigDecimal = new BigDecimal(d);
            long longValue = bigDecimal.longValue();
            var integerPart = String.valueOf(longValue);
            var decimalPart = String.valueOf(bigDecimal.subtract(
                    new BigDecimal(longValue)));
            if (decimalPart.matches("^0+$")) {
                if (longValue > Integer.MAX_VALUE || longValue < Integer.MIN_VALUE) {
                    entry.setValue(d.longValue());
                } else {
                    entry.setValue(d.intValue());
                }
            }
        }
    }
}

public static void visit(Iterable it, Consumer consumer) {
    it.forEach(o -> {
        if (o instanceof Iterable) {
            visit((Iterable) o, consumer);
        } else {
            consumer.accept(o);
        }
    });
}

Each time a Double type is encountered, it tries to determine if it has a decimal point. If it has one it remains a double. Otherwise it checks if the integer part should be a Long or Integer type by comparing Integer.MAX_VALUE and Integer.MIN_VALUE respectively.

If a type change was detected, then the map entry value is replaced with the new primitive type.

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM