96

Gson has some odd behavior when I try to convert a string to json. The code below transforms string draft into json responses. Is there a way to prevent gson from adding the '.0 to all integer values?

ArrayList<Hashtable<String, Object>> responses;
Type ResponseList = new TypeToken<ArrayList<Hashtable<String, Object>>>() {}.getType();
responses = new Gson().fromJson(draft, ResponseList);

draft:
[ {"id":4077395,"field_id":242566,"body":""},
  {"id":4077398,"field_id":242569,"body":[[273019,0],[273020,1],[273021,0]]},
  {"id":4077399,"field_id":242570,"body":[[273022,0],[273023,1],[273024,0]]}
]

responses:
[ {id=4077395.0, body=, field_id=242566.0},
  {id=4077398.0, body=[[273019.0, 0.0], [273020.0, 1.0], [273021.0, 0.0]], field_id=242569.0},
  {id=4077399.0, body=[[273022.0, 0.0], [273023.0, 1.0], [273024.0, 0.0]], field_id=242570.0}
]
dimo414
  • 47,227
  • 18
  • 148
  • 244
Mark Murphy
  • 1,580
  • 1
  • 16
  • 29

10 Answers10

50

You're telling Gson it's looking for a list of maps of Strings to Objects, which essentially says for it to make a best guess as to the type of the Object. Since JSON doesn't distinguish between integer and floating point fields Gson has to default to Float/Double for numeric fields.

Gson is fundamentally built to inspect the type of the object you want to populate in order to determine how to parse the data. If you don't give it any hint, it's not going to work very well. 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.

class Response {
  int id;
  int field_id;
  ArrayList<ArrayList<Integer>> body; // or whatever type is most apropriate
}

responses = new Gson()
            .fromJson(draft, new TypeToken<ArrayList<Response>>(){}.getType());

Again, the whole point of Gson is to seamlessly convert structured data into structured objects. If you ask it to create a nearly undefined structure like a list of maps of objects, you're defeating the whole point of Gson, and might as well use some more simplistic JSON parser.

dimo414
  • 47,227
  • 18
  • 148
  • 244
  • Just for completeness, a Float is not more generic than an Integer, because it can't accurately represent all the same values as an Integer. But a Double can. – brianmearns Aug 13 '15 at 12:36
  • I meant generic in terms of types - floating point can represent non-integer values. Certainly in practice float/double cannot represent more values than int/long, but the limitations of floating point aren't really at issue here. – dimo414 Aug 13 '15 at 13:46
  • terrible advice, sorry but if value changes from int to string to something else but double you are screwed... – Enerccio Nov 01 '17 at 13:15
  • @Enerccio what do you mean by "*if value changes from int to string to something else but double*"? When would the value's type change? If your document's schema changes you'll need to update your Java class definition. – dimo414 Nov 01 '17 at 22:07
  • @dimo414 well maybe it can store different values of different types, anyways I sorted this by having `TaggedValue` and store type with the value – Enerccio Nov 02 '17 at 17:59
  • @Enerccio right, you can pass along the type information as well and use a custom deserializer, but that isn't related to this question. *Unless type metadata is included in the schema* you cannot generally deserialize data whose schema changes arbitrarily. – dimo414 Nov 02 '17 at 20:01
46

There is a solution provided by the library from 2.8.9 version.

We can set how Object is converted to a number by using the GsonBuilder.setObjectToNumberStrategy() method.

Implementation of LONG_OR_DOUBLE will work in this case. Can be used as

Gson gson = new GsonBuilder()
    .setObjectToNumberStrategy(ToNumberPolicy.LONG_OR_DOUBLE)
    .create();
responses = gson.fromJson(draft, ResponseList);

Refer to the GitHub pull request for details: Support arbitrary Number implementation for Object and Number deserialization by lyubomyr-shaydariv · Pull Request #1290 · google/gson · GitHub.

JaredCS
  • 427
  • 4
  • 11
Seeker
  • 1,030
  • 10
  • 10
42

This works:

 Gson gson = new GsonBuilder().
        registerTypeAdapter(Double.class,  new JsonSerializer<Double>() {   

    @Override
    public JsonElement serialize(Double src, Type typeOfSrc, JsonSerializationContext context) {
        if(src == src.longValue())
            return new JsonPrimitive(src.longValue());          
        return new JsonPrimitive(src);
    }
 }).create();
Martin Wickman
  • 19,662
  • 12
  • 82
  • 106
  • 13
    Hi, I found this answer and using the way you mentioned in this post, but still, I got double when it should be int :-( – armnotstrong Mar 25 '15 at 02:56
  • @armnotstrong For which number did this not work? The code above should work for all 32 bit int values because all have corresponding exact values for the Java double type (which has 64 bits). Casts between (integral) double and int values and back are exact in the int range. Going into the 64 bit long range however, positive or negative values exceeding 2 to the power of 52 (4,503,599,627,370,496) can no longer be converted correctly in all cases. – Alexander233 Aug 07 '18 at 10:12
13

Basically, there is no perfect answer for this issue. All "solutions" work for some cases only. This is an issue reported to gson team, unfortunately seems they insist that "javascript has no integer type" as if they do not realize that gson is for java not javascript. So they refused to fix it until today (2018 now), despite other lib like jackson does not have such issue at all, despite how easy to fix it. So you may have to fix the issue yourself from gson source code and build your own gson.jar. The source file is gson/src/main/java/com/google/gson/internal/bind/ObjectTypeAdapter.java

case NUMBER:
   return in.nextDouble();
Leon
  • 3,124
  • 31
  • 36
7

I'm late to the party, but I just ran into this myself. In my case, I didn't want to specify an Integer type in my ArrayList - since it could be a String or an Integer.

My solution is as follows:

GsonBuilder gsonBuilder = new GsonBuilder();
gsonBuilder.registerTypeAdapter(Double.class,  new JsonSerializer<Double>() {

    public JsonElement serialize(Double src, Type typeOfSrc,
                JsonSerializationContext context) {
            Integer value = (int)Math.round(src);
            return new JsonPrimitive(value);
        }
    });

Gson gs = gsonBuilder.create();

Rather than using the default Gson definition with Gson gs = new Gson();, I have overridden the Double.class serialization to return an integer.

In my case, I have Strings and Integers within my JSON, but I do not have any doubles, so this doesn't pose a problem.

If you need a double or a float value, I suppose it would be possible to add some logic that tested the value for attributes specific to each datatype and returned an appropriate value. Something like

if(/*source has a decimal point*/){
  return new JsonPrimitive(src); 
} else if (/* source has some attribute of a Float */){
  Float value = /*convert the src value from double to a Float */;
  return new JsonPrimitive(value);
} else {
  //it looks like an integer
  Integer value = (int)Math.round(src);
  return new JsonPrimitive(value);
}

I don't know how to test for or convert those datatypes off the top of my head, but this should put you on the right path.

Steve Kallestad
  • 3,484
  • 2
  • 23
  • 31
  • 3
    Hearing "*it could be a String or an Integer*" is a pretty big red flag in my mind. It sounds like your JSON data isn't well structured - you're creating a list of both integers and Strings? Technically the JSON specification allows for that, but it's going to create pain for every deserializer that tries to interface with it. Instead consider a) leaving the whole list Strings, if it's just a coincidence that some are numbers, b) splitting the numbers off into their own list, or c) changing the list type to be some more complex object that better reflects the intent of the data. – dimo414 May 27 '14 at 13:24
  • @dimo414 well all three solutions have their flaws: a) is useless since you don't then know which was number and which was string; b) that loses the ordering information (which then needs spearate list of indexes and overhead); c) complex objects will inflate result json – Enerccio Nov 03 '17 at 09:00
  • @Enerccio complex data requires complex representations, so c) is often an acceptable tradeoff. Your points are well taken, but in practice I'll content that mixing data types is *in general* going to be more trouble than it's worth, and usually re-examining your requirements will reveal an alternative structure that works for your purposes without needing to jump through such hoops. Feel free to post a question with a concrete use-case, I'd be happy to weigh in. – dimo414 Nov 04 '17 at 01:39
2

Custom serializer solution in Kotlin, it's a bit tricky because you have to distinguish between java.lang.Double and Double (kotlin.Double).

private val Gson: Gson = GsonBuilder().registerTypeAdapter(java.lang.Double::class.java, object : JsonSerializer<Double> {
    override fun serialize(src: Double, typeOfSrc: Type, context: JsonSerializationContext): JsonElement {
        return if (src == src.toLong().toDouble()) JsonPrimitive(src.toLong()) else JsonPrimitive(src)
    }
}).create()
Trevor
  • 1,349
  • 10
  • 16
1

This work for me.

Step 1: Copy the ObjectTypeAdapter in gson into the project, keeping the path the same as in gson Like this

com
  - xxx
    - xxx
com
  - google
    - gson
      - internal
        - bind
          ObjectTypeAdapter

Step 2: Modify ObjectTypeAdapter

case NUMBER:
  return in.nextDouble();

Modified to

case NUMBER:
  String number = in.nextString();
  try {
    return Long.valueOf(number);
  } catch (NumberFormatException e) {
    return Double.valueOf(number);
  }

OK. Gson will prioritizes the ObjectTypeAdapter in the project.

XiangYun
  • 27
  • 1
  • 2
  • That's not a solution rather a dangerous workaround which will render the whole project a rubbish bin – Farid Nov 29 '20 at 19:05
1

Use Jackson

    public static Map<String, Object> jsonToMap(final String jsonString) {
    try {
        final ObjectMapper objectMapper = new ObjectMapper();
        return objectMapper.convertValue(objectMapper.readTree(jsonString), new TypeReference<Map<String, Object>>() {
        });
    } catch (final Exception e) {
        throw new InternalServiceException("lol");
    }
}
swarnim gupta
  • 213
  • 1
  • 5
0
    fun jsonToMap(json: JSONObject): Map<String, Any> {
        val doubles = Gson().fromJson<Map<String, Any>>(json.toString(), Map::class.java)
        fun doublesToLong(doubles: Map<String, Any>): Map<String, Any> = doubles
                .map { entry ->
                    Pair(entry.key, entry.value.let {
                        when (it) {
                            is Map<*, *> -> doublesToLong(it as Map<String, Any>)
                            is Double -> it.toLong()
                            else -> it
                        }
                    })
                }
                .toMap()
        return doublesToLong(doubles)
    }
k4dima
  • 6,070
  • 5
  • 41
  • 39
0

google fixed issue https://github.com/google/gson/commit/fe30b85224316cabf19f5dd3223843437c297802#diff-9bf510cca1fa5b32b008e7daa417abf15602571dbc87f5436d9f3558ded492a5 please update gson version to 2.8.9

  • 1
    Your answer could be improved with additional supporting information. Please [edit] to add further details, such as citations or documentation, so that others can confirm that your answer is correct. You can find more information on how to write good answers [in the help center](/help/how-to-answer). – Community Jan 06 '22 at 09:41
  • Also it is not sufficient to just update the version you also have to specify a strategy as @seeker correctly points out below. – crowmagnumb Jun 27 '22 at 20:23
  • 2.8.9 exhibits the same behavior – rmirabelle Sep 13 '22 at 18:09