Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

How can I prevent gson from converting integers to doubles

Tags:

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.

like image 460
AlexC Avatar asked Apr 08 '16 19:04

AlexC


People also ask

Why does Gson parse an integer as a double?

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 .

How to prevent Gson from expressing integers as floats?

One option is to define a custom JsonDeserializer, however better would be to not use a HashMap (and definitely don't use Hashtable!) and instead give Gson more information about the type of data it's expecting. Show activity on this post. Show activity on this post. Show activity on this post.

Does Gson ignore transient fields?

Gson serializer will ignore every field declared as transient: String jsonString = new Gson().

Does Gson ignore extra fields?

3. Deserialize JSON With Extra Unknown Fields to Object. As you can see, Gson will ignore the unknown fields and simply match the fields that it's able to.


2 Answers

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

like image 58
varren Avatar answered Sep 21 '22 17:09

varren


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.

like image 31
cybersoft Avatar answered Sep 22 '22 17:09

cybersoft