Skip to content

Gson 反序列化会将 int 转换为 double 类型的问题与解决方案

使用 Gson 反序列化,在可以确定 JSON 内容(Key 数量不变)的情况下,我们通常会建立与之对应的 Java POJO,Gson 会自动帮我们映射到对应的类中;而在 JSON 内容不确定(Key 数量未知)的情况下,映射到一个 Map 中是比较合适的。

在 Maven 项目中添加 Gson 依赖:

xml
<dependency>
    <groupId>com.google.code.gson</groupId>
    <artifactId>gson</artifactId>
    <version>2.8.6</version>
</dependency>

Gson 反序列化映射到 POJO

比如说有这样一个 JSON:

json
{
    "id":6,
    "name":"小明",
    "score":88.5
}

建立对应的 POJO:

java
package study.helloworld.gson;

public class Student {
    private int id;
    private String name;
    private double score;

    @Override
    public String toString() {
        return "Student{" +
                "id=" + id +
                ", name='" + name + '\'' +
                ", score=" + score +
                '}';
    }

    public int getId() {
        return id;
    }

    public void setId(int id) {
        this.id = id;
    }

    public String getName() {
        return name;
    }

    public void setName(String name) {
        this.name = name;
    }

    public double getScore() {
        return score;
    }

    public void setScore(double score) {
        this.score = score;
    }
}

使用 Gson 反序列化:

java
String json = "{\"id\": 6, \"name\": \"小明\", \"score\": 88.5}";
Gson gson = new Gson();
Student student = gson.fromJson(json, Student.class);
System.out.println(student); // Student{id=6, name='小明', score=88.5}

Gson 反序列化映射到 Map

如果将上面的 JSON 映射到 Map 呢?

java
String json = "{\"id\": 6, \"name\": \"小明\", \"score\": 88.5}";
Gson gson = new Gson();
Map map = gson.fromJson(json, Map.class);

最好返回一个带泛型的 Map。JSON 的 Key 都是字符串,Map 中的 Key 可以定义成 String。但是 JSON 里的 Value 的类型是不同的,有整数、字符串和小数,所以 Map 中的 Value 只能定义成 Object

java
Map<String, Object> map = gson.fromJson(json, new TypeToken<Map<String, Object>>() {}.getType());

结果都是一样的:

java
System.out.println(map); // {id=6.0, name=小明, score=88.5}

问题来啦!“id” 是一个整数,转换后却变成了小数。虽然在语义上“6 和 6.0 相等”,但是在逻辑上,“id”从 int 类型变成了 double 类型,这是不被允许的。

Gson 反序列化的过程

反序列化时,根据要序列化的 Type, 得到 TypeToken,再使用图中标注的 getAdapter 方法获取一个 TypeAdapter

(图片来源:自己截的)

TypeAdapter 是怎么生成的呢?Gson 内置了许多不同的 TypeAdapterFactory,在实例化 Gson 对象的时候,将其全部放到了一个 List 中。

(图片来源:自己截的)

TypeAdapter 是一个抽象类,里面的 writeread 抽象方法分别表示“序列化”和“反序列化”,需要由子类实现。下图是子类实现(部分):

(图片来源:自己截的)

TypeAdapter 是怎么获取的呢?在 getAdapter 方法中,根据不同的 Type,调用不同的 TypeAdapterFactory,从而得到对应的 TypeAdapter

(图片来源:自己截的)

得到具体的 TypeAdapter 后,开始读取(read)具体的值。由于我们使用的 TypeMap<String, Object>,在匹配到 Object 类型的时候,会返回一个 ObjectTypeAdapter

ObjectTypeAdapter 是如何 read 值的呢?首先,会 peek 这个值,然后确定这个值具体属于什么类型(数组、字符串、数字等),从而返回对应的类型。

(图片来源:自己截的)

int 为什么变成了 double

注意上图中的标注,如果 Object 属于一个 NUMBER 类型,那么会返回一个 double 类型的值。“6”是一个数字,自然而然,“6”会变成“6.0”。

为什么会这样呢?

查阅不少资料,普遍有两种说法:

  1. 在 JSON 规范中,只有数字类型,而没有整数类型和小数类型。显然,使用精度最高的 double 是最合适的方式。
  2. 这是一个 Bug,Gson 开发者可能还没有找到更合适的处理方式来解决这个问题。

解决方案

这里推荐几种解决方案供大家选择。首先说明,这些方案都不完美,各有优缺点。

编写自己的 ObjectTypeAdapter

既然官方的 ObjectTypeAdapter 达不到我们想要的效果,那么就自己编写一个。

java
package study.helloworld.gson;

import com.google.gson.TypeAdapter;
import com.google.gson.internal.LinkedTreeMap;
import com.google.gson.stream.JsonReader;
import com.google.gson.stream.JsonToken;
import com.google.gson.stream.JsonWriter;

import java.io.IOException;
import java.util.ArrayList;
import java.util.List;
import java.util.Map;

/**
 * 自己的 ObjectTypeAdapter,只能处理反序列化,可以解决 int 变成 double 的问题
 */
public final class MyObjectTypeAdapter extends TypeAdapter<Object> {
    /**
     * 不支持序列化,使用官方库进行序列化
     * @param jsonWriter
     * @param o
     */
    @Override
    public void write(JsonWriter jsonWriter, Object o) {
        throw new UnsupportedOperationException();
    }

    @Override
    public Object read(JsonReader in) throws IOException {
        JsonToken token = in.peek();
        switch (token) {
            case BEGIN_ARRAY:
                List<Object> list = new ArrayList();
                in.beginArray();

                while (in.hasNext()) {
                    list.add(this.read(in));
                }

                in.endArray();
                return list;
            case BEGIN_OBJECT:
                Map<String, Object> map = new LinkedTreeMap();
                in.beginObject();

                while (in.hasNext()) {
                    map.put(in.nextName(), this.read(in));
                }

                in.endObject();
                return map;
            case STRING:
                return in.nextString();
            case NUMBER:
                // 优点——可以解决 `int` 到 `int`,且不丢失数据类型(`instanceof = Number`)
                // 缺点——无法解决 `double` 中的小数部分恰好是“0”的情况(比如“6.0”,反序列化后反而变成了“6”)
                double aDouble = in.nextDouble();
                long aLong = (long)aDouble;
                if (aDouble == aLong) {
                    return aLong;
                }
                return aDouble;
            case BOOLEAN:
                return in.nextBoolean();
            case NULL:
                in.nextNull();
                return null;
            default:
                throw new IllegalStateException();
        }
    }
}

深色部分,也就是 switch 中的 NUMBER 代码块中,改写了原来的代码。

  • 优点——可以解决 intint,且不丢失数据类型(instanceof = Number
  • 缺点——无法解决 double 中的小数部分恰好是“0”的情况(比如“6.0”,反序列化后反而变成了“6”)

还有一种方案是直接返回一个字符串类型的数字:

java
// 优点——可以完美解决反序列化结果不一致的问题
// 缺点——数据类型都变成了字符串(`instanceof = String`)
double aDouble = in.nextDouble();
String number = String.valueOf(aDouble);
return number;
  • 优点——可以完美解决反序列化结果不一致的问题
  • 缺点——数据类型都变成了字符串(instanceof = String

这两种方案的使用方法都是一样的,通过 GsonBuilder 注册一个 TypeAdapter 来创建一个 Gson 对象:

java
String json = "{\"id\": 6, \"name\": \"小明\", \"score\": 88.5}";
Type type = new TypeToken<Map<String, Object>>() {}.getType();
Gson gson = new GsonBuilder().registerTypeAdapter(type, new MyObjectTypeAdapter()).create();
Map<String, Object> map = gson.fromJson(json, type);
System.out.println(map); // {id=6, name=小明, score=88.5}

编写自己的 JsonDeserializer

在 Gson 2.0 版本之前,序列化和反序列化是通过 JsonSerializerJsonDeserializer 来完成的。所以我们也可以定义自己的 JsonDeserializer

java
package study.helloworld.gson;

import com.google.gson.*;

import java.lang.reflect.Type;
import java.util.HashMap;
import java.util.LinkedHashMap;
import java.util.Map;
import java.util.Set;

/**
 * 自己的 JsonDeserializer,可以完美解决 int 变成 double 的问题
 * 但是数据类型都变成了 Object(`instanceof = Object`)
 * 更诡异的是字符串反序列化后带有双引号
 */
public class MyJsonDeserializer implements JsonDeserializer<Map<String, Object>> {
    @Override
    public Map<String, Object> deserialize(JsonElement jsonElement, Type type, JsonDeserializationContext jsonDeserializationContext) throws JsonParseException {
        HashMap<String, Object> hashMap = new LinkedHashMap<>();
        JsonObject jsonObject = jsonElement.getAsJsonObject();
        Set<Map.Entry<String, JsonElement>> entrySet = jsonObject.entrySet();
        for (Map.Entry<String, JsonElement> entry : entrySet) {
            hashMap.put(entry.getKey(), entry.getValue());
        }
        return hashMap;
    }
}

同样的,也是通过 GsonBuilder 构建一个 Gson 对象:

java
String json = "{\"id\": 6, \"name\": \"小明\", \"score\": 88.5}";
Type type = new TypeToken<Map<String, Object>>() {}.getType();
Gson gson = new GsonBuilder().registerTypeAdapter(type, new MyJsonDeserializer()).create();
Map<String, Object> map = gson.fromJson(json, type);
System.out.println(map); // {id=6, name="小明", score=88.5}
  • 优点——可以完美解决反序列化结果不一致的问题
  • 缺点——数据类型都变成了 Object(instanceof = Object

这个方案更诡异的是字符串反序列化后会带有双引号(注意打印的输出内容)。

Released under the MIT License.