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.