27

I've got integers in my json, and I do not want gson to convert them to doubles. The following does not work:

@Test
public void keepsIntsAsIs(){
    String json="[{\"id\":1,\"quantity\":2,\"name\":\"apple\"},{\"id\":3,\"quantity\":4,\"name\":\"orange\"}]";
    GsonBuilder gsonBuilder = new GsonBuilder();
    gsonBuilder.registerTypeAdapter(Double.class,  new DoubleSerializerAsInt());
    Gson gson = gsonBuilder.create();
    List<Map<String, Object>> l = gson.fromJson(json, List.class);
    for(Map<String, Object> item : l){
        System.out.println(item);
    }
}

private static class DoubleSerializerAsInt implements JsonSerializer<Double>{

    @Override
    public JsonElement serialize(Double aDouble, Type type, JsonSerializationContext jsonSerializationContext) {
        int value = (int)Math.round(aDouble);
        return new JsonPrimitive(value);
    }
}

The output is not what I want:

{id=1.0, quantity=2.0, name=apple}
{id=3.0, quantity=4.0, name=orange}

Is there a way to have Integers instead of Doubles in my Map?

{id=1, quantity=2, name=apple}
{id=3, quantity=4, name=orange}

Edit: not all my fields are integer. I've modified my example accordingly. I've read quite a few examples online, including some answers on this site, but it does not work in this particular case.

AlexC
  • 3,343
  • 6
  • 29
  • 38
  • same question : http://stackoverflow.com/questions/15507997/how-to-prevent-gson-from-expressing-integers-as-floats – Danda Apr 08 '16 at 20:11
  • 3
    You don't need a custom deserializer if your values are always integers. Just provide the appropriate type when parsing the JSON content: `List> l = new Gson().fromJson(json, new TypeToken>>(){}.getType());` (aside from that you may create a class and have a `List` as a result instead). – Alexis C. Apr 08 '16 at 20:14
  • http://stackoverflow.com/a/21960998/1393766 – Pshemo Apr 08 '16 at 20:17
  • @Danda I've seen that question, but that solution does not work for me. – AlexC Apr 08 '16 at 20:38
  • @AlexisC. Unfortunately, not all my fields are integer. I've modified my example accordingly. – AlexC Apr 08 '16 at 20:38
  • @Pshemo does it mean that there is no way to accomplish what I want in Java? The json I'm paring has been generated by Postgres, which has no problem doing exactly what I want. – AlexC Apr 08 '16 at 20:41
  • Not quite "in Java", but specifically in gson. Maybe try with other JSON parser (Jackson comes to mind). – Pshemo Apr 08 '16 at 20:54
  • @AlexC Why don't you create a class in this case? A `List>` is less useful than `List` in my opinion. – Alexis C. Apr 08 '16 at 20:54
  • @AlexisC. We do create DTOs in such cases. We are exploring lightweight alternatives. Thank you. – AlexC Apr 08 '16 at 20:57

8 Answers8

27

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

varren
  • 14,551
  • 2
  • 41
  • 72
20

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.

cybersoft
  • 1,453
  • 13
  • 31
  • This actually worked for me, thanks! You might want to edit your example where it's CustomizedObjectTypeAdapter in one place and CustomObjectTypeAdapter in another. – Michael Oct 20 '17 at 15:46
  • Thank you, I have fixed (it was copy-paste from IDE;) – cybersoft Oct 20 '17 at 15:59
  • Thank you!. For kotlin we need to use `TypeToken`. See [here](https://gist.github.com/VincentSit/474cf311eeeab169f0e674f755b33058) for details, it took me an hour. – Vincent Sit Apr 21 '20 at 17:58
4

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();
}
}
Sanj
  • 3,879
  • 2
  • 23
  • 26
4

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();
    }

}
}
Dhruv Tyagi
  • 814
  • 1
  • 9
  • 28
nitinsridar
  • 684
  • 1
  • 12
  • 25
4

Since gson 2.8.9, you can now provide number conversion policy. So the commented code from ObjectTypeAdapter

case NUMBER:
      return in.nextDouble();

now do conversion based on policy you provide. So using GsonBuilder use the function setObjectToNumberStrategy to provide strategy for conversion. You can also write your own if you wish. Use ToNumberPolicy.LONG_OR_DOUBLE for your case.

ata
  • 8,853
  • 8
  • 42
  • 68
  • Nice! Exactly what I was looking for. This should be the accepted answer. No need for the verbose code given in other answers. – Pubudu Jul 14 '22 at 20:32
2

This works fine for me:

private static class DoubleSerializer implements JsonSerializer<Double> {
    @Override
    public JsonElement serialize(Double src, Type typeOfSrc, JsonSerializationContext context) {
        return src == src.longValue() ? new JsonPrimitive(src.longValue()) : new JsonPrimitive(src);
    }
}

Gson gson = new GsonBuilder().registerTypeAdapter(Double.class, new DoubleSerializer()).setPrettyPrinting().create();
Leon
  • 3,124
  • 31
  • 36
2

This worked for me, I have a "specs" field which is a Map<String, Object>:

public class MyClass {

  public Map<String, Object> specs;

}

Before the fix I was getting this output for a list of these objects:

{  
   "hits":{  
      "content":[  
         {  
            "specs":{  
               "fiscalHorsePower":4.0,
               "nbOfDoors":5.0,
               "consumption":4.3
            }
         }
      ]
   }
}

fiscalHorsePower and nbOfDoors are integer.

Here is the fix I used, first create a new Adapter and a Factory:

public class CustomizedObjectTypeAdapter extends TypeAdapter<Object> {

public static final TypeAdapterFactory FACTORY = new TypeAdapterFactory() {
    @SuppressWarnings("unchecked")
    @Override public <T> TypeAdapter<T> create(Gson gson, TypeToken<T> type) {
        if (Map.class.isAssignableFrom(type.getRawType())) {
            return (TypeAdapter<T>) new CustomizedObjectTypeAdapter();
        }
        return null;
    }
};

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();
    }
}
}

And then register the factory:

Gson gson = new GsonBuilder().registerTypeAdapterFactory(CustomizedObjectTypeAdapter.FACTORY);

and here is the result with the fix:

{  
   "hits":{  
      "content":[  
         {  
            "specs":{  
               "fiscalHorsePower":4,
               "nbOfDoors":5,
               "consumption":4.3
            }
         }
      ]
   }
}
anthofo
  • 93
  • 12
  • 1
    A bit of explanation of your answer and why you think it solves the issue would be good. – sim Dec 14 '18 at 14:37
0

here is my solution to solve the Problem. I tried to implement it as clean as possible. I didn' t find a better easy solution to check if a number is an integer.

public final class GSONUtil {

    private GSONUtil() {
    }

    public static Gson createGson() {

        // @formatter:off
        return new GsonBuilder()
                .registerTypeAdapter(Map.class, createMapDeserializer())
                .registerTypeAdapter(List.class, createListDeserializer())
                .create();
        // @formatter:on
    }

    private static JsonDeserializer<Map<String,Object>> createMapDeserializer() {
        return new JsonDeserializer<Map<String,Object>>() {

            @Override
            public Map<String, Object> deserialize(JsonElement json, Type typeOfT, JsonDeserializationContext context)
                    throws JsonParseException {

                return json.getAsJsonObject().entrySet().stream() // stream
                        .collect(Collectors.toMap(Entry::getKey, (e) -> JSONUtil.deserialize(e.getValue(), context)));
            }
        };
    }

    private static JsonDeserializer<List<Object>> createListDeserializer() {
        return new JsonDeserializer<List<Object>>() {

            @Override
            public List<Object> deserialize(JsonElement json, Type typeOfT, JsonDeserializationContext context)
                    throws JsonParseException {

                return StreamSupport.stream(json.getAsJsonArray().spliterator(), false) // stream
                        .map((e) -> JSONUtil.deserialize(e, context)).collect(Collectors.toList());
            }
        };
    }

    private static Object deserialize(JsonElement value, JsonDeserializationContext context) {

        if (value.isJsonNull()) {
            return null;
        }
        if (value.isJsonObject()) {
            return context.deserialize(value, Map.class);
        }
        if (value.isJsonArray()) {
            return context.deserialize(value, List.class);
        }
        if (value.isJsonPrimitive()) {
            return parsePrimitive(value);
        }

        throw new IllegalStateException("This exception should never be thrown!");
    }

    private static Object parsePrimitive(JsonElement value) {

        final JsonPrimitive jsonPrimitive = value.getAsJsonPrimitive();

        if (jsonPrimitive.isString()) {
            return jsonPrimitive.getAsString();
        }

        if (jsonPrimitive.isBoolean()) {
            return jsonPrimitive.getAsBoolean();
        }

        if (jsonPrimitive.isNumber()) {
            return parseNumber(jsonPrimitive);
        }

        throw new IllegalStateException("This exception should never be thrown!");
    }

    private static Number parseNumber(JsonPrimitive jsonPrimitive) {

        if (isInteger(jsonPrimitive)) {
            return jsonPrimitive.getAsLong();
        }

        return jsonPrimitive.getAsDouble();
    }

    private static boolean isInteger(final JsonPrimitive jsonPrimitive) {
        return jsonPrimitive.getAsString().matches("[-]?\\d+");
    }
}
CodingSamples
  • 194
  • 1
  • 1
  • 7