How can I prevent gson from converting integers to doubles

Use Jackson instead of Gson, It solves your problem:

import com.fasterxml.jackson.databind.ObjectMapper;
import java.io.IOException;
import java.util.Map;

public class JacksonMapExample1 {

public static void main(String[] args) {

    ObjectMapper mapper = new ObjectMapper();
    String json = "{\"name\":\"mkyong\", \"age\":\"37\"}";
    try {
        // convert JSON string to Map
        Map<String, String> map = mapper.readValue(json, Map.class);
        System.out.println(map);

    } catch (IOException e) {
        e.printStackTrace();
    }

}
}

You have to use public T fromJson(JsonElement json, Type typeOfT)

public void keepsIntsAsIs(){
        String json="[{\"id\":1,\"quantity\":2},{\"id\":3,\"quantity\":4}]";
        GsonBuilder gsonBuilder = new GsonBuilder();
        Gson gson = gsonBuilder.create();
        Type objectListType = new TypeToken<List<Map<String, Integer>>>(){}.getType();
        List<Map<String, Integer>> l = gson.fromJson(json, objectListType);
        for(Map<String, Integer> item : l){
            System.out.println(item);
        }
    }

Output:

{id=1, quantity=2}
{id=3, quantity=4}

[EDIT]

If not all fields are integers then one way to resolve this is to map the json to an object and define a deserializer for that object.

Below is the example.

I am mapping json to IdQuantityName and IdQuantityDeserializer is the json deserializer.

package com.foo;



import java.lang.reflect.Type;
import java.util.List;

import com.google.gson.Gson;
import com.google.gson.GsonBuilder;
import com.google.gson.JsonDeserializationContext;
import com.google.gson.JsonDeserializer;
import com.google.gson.JsonElement;
import com.google.gson.JsonObject;
import com.google.gson.JsonParseException;
import com.google.gson.JsonPrimitive;
import com.google.gson.JsonSerializationContext;
import com.google.gson.JsonSerializer;
import com.google.gson.reflect.TypeToken;

public class TestGSON {

public void keepsIntsAsIs(){
    String json="[{\"id\":1,\"quantity\":2,\"name\":\"apple\"},{\"id\":3,\"quantity\":4,\"name\":\"orange\"}]";
    GsonBuilder gsonBuilder = new GsonBuilder();
    gsonBuilder.registerTypeHierarchyAdapter(IdQuantityName.class, new IdQuantityDeserializer());
    gsonBuilder.registerTypeAdapter(IdQuantityName.class, new IdQuantityDeserializer());

    Gson gson = gsonBuilder.create();
    Type objectListType = new TypeToken<List<IdQuantityName>>(){}.getType();
    List<IdQuantityName> l = gson.fromJson(json,objectListType);
    for (IdQuantityName idQuantityName : l) {
        System.out.println(idQuantityName);
    }
}



class IdQuantityName{
    private int id;
    private Object quantity;
    private String name;

    public int getId() {
        return id;
    }
    public void setId(int id) {
        this.id = id;
    }
    public Object getQuantity() {
        return quantity;
    }
    public void setQuantity(Object quantity) {
        this.quantity = quantity;
    }
    public Object getName() {
        return name;
    }
    public void setName(String name) {
        this.name = name;
    }
    @Override
    public String toString() {
        return "IdQuantityName [id=" + id + ", quantity=" + quantity
                + ", name=" + name + "]";
    }



}
private  class IdQuantityDeserializer implements JsonDeserializer<IdQuantityName>{

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

        JsonObject jo = json.getAsJsonObject();

        IdQuantityName idq = new IdQuantityName();
        idq.setId(jo.get("id").getAsInt());
        idq.setName(jo.get("name").getAsString());

        JsonElement jsonElement = jo.get("quantity");
        if(jsonElement instanceof JsonPrimitive){
            if(((JsonPrimitive) jsonElement).isNumber()){
                idq.setQuantity(jsonElement.getAsInt());
            };
        }
        return idq;

    }
}
public static void main(String[] args) {
    new TestGSON().keepsIntsAsIs();
}
}

1) You have to create custom JsonDeserializer and not JsonSerializer like in your question.

2) I don't think this behavior comes from Double deserializer. it is more like json object/map problem

Here is from source code:

case NUMBER:
      return in.nextDouble();

So you can try approach with custom deserializer for Map<String, Object> (or some more generic map if you want) :

public static class MapDeserializerDoubleAsIntFix implements JsonDeserializer<Map<String, Object>>{

    @Override  @SuppressWarnings("unchecked")
    public Map<String, Object> deserialize(JsonElement json, Type typeOfT, JsonDeserializationContext context) throws JsonParseException {
        return (Map<String, Object>) read(json);
    }

    public Object read(JsonElement in) {

        if(in.isJsonArray()){
            List<Object> list = new ArrayList<Object>();
            JsonArray arr = in.getAsJsonArray();
            for (JsonElement anArr : arr) {
                list.add(read(anArr));
            }
            return list;
        }else if(in.isJsonObject()){
            Map<String, Object> map = new LinkedTreeMap<String, Object>();
            JsonObject obj = in.getAsJsonObject();
            Set<Map.Entry<String, JsonElement>> entitySet = obj.entrySet();
            for(Map.Entry<String, JsonElement> entry: entitySet){
                map.put(entry.getKey(), read(entry.getValue()));
            }
            return map;
        }else if( in.isJsonPrimitive()){
            JsonPrimitive prim = in.getAsJsonPrimitive();
            if(prim.isBoolean()){
                return prim.getAsBoolean();
            }else if(prim.isString()){
                return prim.getAsString();
            }else if(prim.isNumber()){

                Number num = prim.getAsNumber();
                // here you can handle double int/long values
                // and return any type you want
                // this solution will transform 3.0 float to long values
                if(Math.ceil(num.doubleValue())  == num.longValue())
                   return num.longValue();
                else{
                    return num.doubleValue();
                }
           }
        }
        return null;
    }
}

To use it you will have to give proper TypeToken to registerTypeAdapter and gson.fromJson function:

String json="[{\"id\":1,\"quantity\":2,\"name\":\"apple\"}, {\"id\":3,\"quantity\":4,\"name\":\"orange\"}]";

GsonBuilder gsonBuilder = new GsonBuilder();

gsonBuilder.registerTypeAdapter(new TypeToken<Map <String, Object>>(){}.getType(),  new MapDeserializerDoubleAsIntFix());

Gson gson = gsonBuilder.create();
List<Map<String, Object>> l = gson.fromJson(json, new TypeToken<List<Map<String, Object>>>(){}.getType() );

for(Map<String, Object> item : l)
    System.out.println(item);

String serialized = gson.toJson(l);
System.out.println(serialized);

Result:

{id=1, quantity=2, name=apple}
{id=3, quantity=4, name=orange}
Serialized back to: [{"id":1,"quantity":2,"name":"apple"},{"id":3,"quantity":4,"name":"orange"}]

PS: It is just one more option you can try. Personally i feel like creating custom object for your json instead of List<Map<String, Integer>> is much cooler and easier to read way


Streaming version of @varren's answer:

class CustomizedObjectTypeAdapter extends TypeAdapter<Object> {

    private final TypeAdapter<Object> delegate = new Gson().getAdapter(Object.class);

    @Override
    public void write(JsonWriter out, Object value) throws IOException {
        delegate.write(out, value);
    }

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

            case BEGIN_OBJECT:
                Map<String, Object> map = new LinkedTreeMap<String, Object>();
                in.beginObject();
                while (in.hasNext()) {
                    map.put(in.nextName(), read(in));
                }
                in.endObject();
                return map;

            case STRING:
                return in.nextString();

            case NUMBER:
                //return in.nextDouble();
                String n = in.nextString();
                if (n.indexOf('.') != -1) {
                    return Double.parseDouble(n);
                }
                return Long.parseLong(n);

            case BOOLEAN:
                return in.nextBoolean();

            case NULL:
                in.nextNull();
                return null;

            default:
                throw new IllegalStateException();
        }
    }
}

It is modified version of ObjectTypeAdapter.java. These original lines:

case NUMBER:
    return in.nextDouble();

are replaced by this:

case NUMBER:
    String n = in.nextString();
    if (n.indexOf('.') != -1) {
        return Double.parseDouble(n);
    }
    return Long.parseLong(n);

In this code, number is read as string and number's type is selected based on existence of dot: number is double only if it has a dot in its string representation and it is long otherwise. Such solution preserves original values of source JSON.

This modified adapter could be used as universal if you could register it for Object type but Gson prevents it:

// built-in type adapters that cannot be overridden
factories.add(TypeAdapters.JSON_ELEMENT_FACTORY);
factories.add(ObjectTypeAdapter.FACTORY);

You have to register this type adapter to those types that you need, e.g. Map and List:

CustomizedObjectTypeAdapter adapter = new CustomizedObjectTypeAdapter();
Gson gson = new GsonBuilder()
        .registerTypeAdapter(Map.class, adapter)
        .registerTypeAdapter(List.class, adapter)
        .create();

Now Gson can deserialize numbers as is.

Tags:

Java

Json

Gson