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.