9

I am having a hard time with GSON.

I have a simple JSON that I want to deserialize to a Map<String,Object>.

It's really intuitive to me that 123 should be parsed as an int (or long), 123.4 as a float( or double).

GSON on the other hand creates Doubles all the time.

Can I tell GSON to not abuse double all the time?

My actual code:

Type mapType = new TypeToken<Map<String, Object>>() {}.getType();
GSON gson = new Gson();
Map<String, Object> map = gson.fromJson(someString, mapType);
Merchuk Hul
  • 245
  • 1
  • 2
  • 11
  • The root problem is that there is only a single _Number_ type. [JSON](http://json.org/) does not have an int type - they're all floating point and decimal places are omitted where possible. – McDowell Jul 24 '14 at 07:40
  • 1
    @McDowell yeah I know this. But GSON's behavior makes deserializing and serializing produce a different output than the original input was, which is really nasty; e.g. I have a `timestamp` field that originaly is 1234567890, and after deser-ser cycle it's 1.23456789E9 – Merchuk Hul Jul 24 '14 at 08:40
  • are there any solutions for this ? – Jeff Bootsholz Oct 02 '18 at 02:18
  • 1
    The root problem is that gson fails to provide an option to convert whole numbers to integers. Can gson detect whole numbers? Yes, and you can use the LongSerializationPolicy.STRING to convert to string. Why isn't there a LongSerializationPolicy,LONG policy? I get this is not as simple as it seems owing to JSON not having integer types (same field could be integer in one instance and decimal in another) but that's where the policy comes in. – Rick O'Shea Dec 03 '20 at 20:40

4 Answers4

6

The following code compiles & works:

package test;

import java.lang.reflect.Type;
import java.text.NumberFormat;
import java.util.ArrayList;
import java.util.Arrays;
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.JsonParseException;
import com.google.gson.reflect.TypeToken;

public class Main {

    public static void main(String[] args) {
        GsonBuilder builder = new GsonBuilder();
        builder.registerTypeAdapter(Object.class, new MyObjectDeserializer());
        Gson gson = builder.create();
        String array = "[1, 2.5, 4, 5.66]";

        Type objectListType = new TypeToken<ArrayList<Object>>() {}.getType();
        List<Object> obj = gson.fromJson(array, objectListType);

        System.out.println(Arrays.toString(obj.toArray()));
    }

    public static class MyObjectDeserializer implements JsonDeserializer<Object> {

        public Object deserialize(JsonElement json, Type typeOfT, JsonDeserializationContext context) 
            throws JsonParseException {

          Number num = null;
          try {
              num = NumberFormat.getInstance().parse(json.getAsJsonPrimitive().getAsString());
          } catch (Exception e) {
              //ignore
          }
          if (num == null) {
              return context.deserialize(json, typeOfT);
          } else {
              return num;
          }
      }
    }

}

My solution will first try to parse the string as a number, if that fails it will let the standard Gson deserializer do the work.

If you need a number parser that is not locale specific use this method to parse a number:

private static Number parse(String str) {
    Number number = null;
    try {
        number = Float.parseFloat(str);
    } catch(NumberFormatException e) {
    try {
        number = Double.parseDouble(str);
    } catch(NumberFormatException e1) {
        try {
            number = Integer.parseInt(str);
        } catch(NumberFormatException e2) {
            try {
                number = Long.parseLong(str);
            } catch(NumberFormatException e3) {
                throw e3;
            }       
        }       
    }       
}
    return number;
}
g00dnatur3
  • 1,173
  • 9
  • 16
  • Thanks. in fact I will deserialize a full map of many objects and classes; GSON does this file, I dont dont like how it handles numbers.. – Merchuk Hul Jul 24 '14 at 09:21
  • I updated my answer based on your feedback, if I understand you correctly, then I think that should work – g00dnatur3 Jul 24 '14 at 09:35
  • looks promising, BUT it is locale dependent (separators), and should not be :-( – Merchuk Hul Jul 24 '14 at 09:44
  • What do you mean locale dependent? I do not understand. There i fixed it by using the context.. that should work. – g00dnatur3 Jul 24 '14 at 10:11
  • I mean that `Numberformat.getInstance()` returns an instance for current default locale. – Merchuk Hul Jul 24 '14 at 10:40
  • I tried your code since it looks best, but it doesn't work. I placed a breakpoint inside `deserialize` and it didn't go there even once. – Merchuk Hul Jul 25 '14 at 06:51
  • i don't know what to tell ya, the code looks correct: http://stackoverflow.com/questions/16590377/custom-json-deserializer-using-gson – g00dnatur3 Jul 25 '14 at 07:14
  • It seems GSON's internal `ObjectTypeAdapter` takes **hardcoded** precedence over the added adapter when you call it with `gson.fromGSON(data, Object.class)` https://i.imgur.com/fYgPJei.png – Mark Jeronimus Jan 11 '21 at 15:27
  • I found a dirty workaround: https://stackoverflow.com/a/24013487/1052284 – Mark Jeronimus Jan 11 '21 at 15:38
2

If you are not bound to specifically use gson library you can solve this deserializing using jackson one as follow:

new ObjectMapper().readValue(yourJson, new TypeReference<Map<String,Object>>() {});

Here a complete junit test that expose the differences between the two libraries deserializing an integer value:

@Test
public void integerDeserializationTest() throws Exception {

    final String jsonSource = "{\"intValue\":1,\"doubleValue\":2.0,\"stringValue\":\"value\"}";

    //Using gson library "intValue" is deserialized to 1.0
    final String gsonWrongResult = "{\"intValue\":1.0,\"doubleValue\":2.0,\"stringValue\":\"value\"}";
    Map<String,Object> gsonMap = new Gson().fromJson(jsonSource, new TypeToken<Map<String, Object>>() {
    }.getType());
    assertThat(new Gson().toJson(gsonMap),is(gsonWrongResult));


    //Using jackson library "intValue" is deserialized to 1
    Map<String,Object> jacksonMap = new ObjectMapper().readValue(jsonSource, new TypeReference<Map<String,Object>>() {});
    assertThat(new ObjectMapper().writeValueAsString(jacksonMap),is(jsonSource));

}
gregorycallea
  • 1,218
  • 1
  • 9
  • 28
1

It's not a good aproach to mix types like this (integers with doubles). Since you are using Object as a type, you won't be able to get both Integers and Doubles from the map. Gson decides which type is more apropriate for Object. In your case it is Double, because all values CAN BE doubles, but all values CAN'T BE integers.

If you really need to mix types, try to use Number class instead of Object. Example:

public static void main(String[] args){
        String array = "[1, 2.5, 4, 5.66]";
        Gson gson = new Gson();

        Type type = new TypeToken<ArrayList<Number>>() {}.getType();
        List<Number> obj = gson.fromJson(array, type);

        System.out.println(Arrays.toString(obj.toArray()));
    }

Output: [1, 2.5, 4, 5.66]

While this:

public static void main(String[] args){
    String array = "[1, 2.5, 4, 5.66]";
    Gson gson = new Gson();

    Type type = new TypeToken<ArrayList<Object>>() {}.getType();
    List<Object> obj = gson.fromJson(array, type);

    System.out.println(Arrays.toString(obj.toArray()));
}

will give you output: [1.0, 2.5, 4.0, 5.66]

mayr
  • 451
  • 6
  • 14
  • 1
    Thank you. But I need an `Object`, since other non-numerical fields might be present - just as in any JSON. I only want to change handling of numbers.. – Merchuk Hul Jul 24 '14 at 08:42
  • All values can not be represented doubles. The precision of a double is less than an Int. You will truncate some large integer values if you convert to double. For example, 2^53 +1 is a valid java Long, but can not be stored as a Double, it will truncate it to 2^53. – Scott Carey Nov 01 '22 at 17:22
0

You can do the following changes in order to parse that data:

   testData1={"GOAL1":123, "GOAL2":123.45,"GOAL5":1256,"GOAL6":345.98}

and below is your code to actually parse it.

Type mapType = new TypeToken<Map<String, Object>>() {
}.getType();
String str = prop.getProperty("testData1");
System.out.println(str);
Gson gson = new Gson();
Map<String, Object> map = gson.fromJson(str, mapType);
for (String key : map.keySet()) {
    String a = null;
    try {

        if (map.get(key) instanceof Double) {
            a = "" + map.get(key);

        } else {
            if (map.get(key) instanceof String) {
                a = (String) map.get(key);

            } else {
                a = null;
            }
        }

        if (a != null && a.contains(".") && !a.endsWith(".0")) {

            // Convert it into Double/Float
        } else {
            // Work as Integer
        }

    } catch (Exception ex) {
        ex.printStackTrace();
    }

}
Praveen Kumar
  • 190
  • 5
  • 15