question-mark
Stuck on an issue?

Lightrun Answers was designed to reduce the constant googling that comes with debugging 3rd party libraries. It collects links to all the places you might be looking at while hunting down a tough bug.

And, if you’re still stuck at the end, we’re happy to hop on a call to see how we can help out.

Gson doesn't deserialise Long numbers correctly

See original GitHub issue

Gson shouldn’t cast a number to a Double if a number does not have decimal digits. It’s clearly wrong.

public class GsonVsJackson {


    public static void main(String[] args) throws Exception {
        testGson();
        System.out.println("========================");
        testJackson();
    }

    public static void testGson() {
        System.out.println("Testing Gson 2.8");
        Gson gson = new Gson();
        HashMap<String, Object> newTest = new HashMap<>();
        newTest.put("first", 6906764140092371368L);
        String jsonString = gson.toJson(newTest);
        System.out.println(jsonString); // output ok: {"first":6906764140092371368}
        Map<String, Object> mapFromJson = gson.fromJson(jsonString, Map.class);
        Number numberFromJson = (Number) mapFromJson.get("first");
        System.out.println(numberFromJson.getClass() + " = " + numberFromJson); // java.lang.Double val 6.9067641400923709E18
        long longVal = numberFromJson.longValue();
        System.out.println(longVal); // output rounded: 6906764140092370944
    }

    public static void testJackson() throws Exception {
        System.out.println("Testing Jackson");
        ObjectMapper jackson = new ObjectMapper();
        HashMap<String, Object> newTest = new HashMap<>();
        newTest.put("first", 6906764140092371368L);
        String jsonString = jackson.writeValueAsString(newTest);
        System.out.println(jsonString); // output ok: {"first":6906764140092371368}
        Map<String, Object> mapFromJson = jackson.readValue(jsonString, Map.class);
        Number numberFromJson = (Number) mapFromJson.get("first");
        System.out.println(numberFromJson.getClass() + " = " + numberFromJson); // java.math.BigInteger = 6906764140092371368
        long longVal = numberFromJson.longValue();
        System.out.println(longVal); // output OK: 6906764140092371368
    }

}

Kind Regards, Daniele

Issue Analytics

  • State:closed
  • Created 6 years ago
  • Reactions:32
  • Comments:37 (15 by maintainers)

github_iconTop GitHub Comments

16reactions
zengliancommented, Mar 28, 2018

It's not a bug, JSON does not distinguish between integers or floats, and does not care the size of numbers.

Gson is created for java not for javascript. Java does care the data type and size. JSON is not used for javascript only. Hotdog is not dog, there is no need to stick to its literal meaning. Number adapter won’t work for Object type, e.g., ArrayList<Object>.

This is an old issue existed for many years. It’s easy for to fix it. I hope it will be fixed in 2018. Other json libs do not have this bug (I think it is).

16reactions
lyubomyr-shaydarivcommented, May 24, 2017

It’s not a bug, and gson does it fully legit: JSON does not distinguish between integers or floats, and does not care the size of numbers. Numerics in JSON are just numbers, and java.lang.Double is the best and largest primitive-counterpart candidate to hold a JSON number.

If you need a long value at a call-site, then just call its longValue() method. If, for any particular reason, you need a behavior you are talking about, then you have to implement a custom type adapter factory. Say, something like this (not sure if it’s implemented right, though):

final class BestNumberTypeAdapterFactory
		implements TypeAdapterFactory {

	private static final TypeAdapterFactory bestNumberTypeAdapterFactory = new BestNumberTypeAdapterFactory();

	private static final TypeToken<Number> numberTypeToken = new TypeToken<Number>() {
	};

	private BestNumberTypeAdapterFactory() {
	}

	static TypeAdapterFactory getBestNumberTypeAdapterFactory() {
		return bestNumberTypeAdapterFactory;
	}

	@Override
	public <T> TypeAdapter<T> create(final Gson gson, final TypeToken<T> typeToken) {
		if ( Number.class.isAssignableFrom(typeToken.getRawType()) ) {
			final TypeAdapter<Number> delegateNumberTypeAdapter = gson.getDelegateAdapter(this, numberTypeToken);
			final TypeAdapter<Number> numberTypeAdapter = new NumberTypeAdapter(delegateNumberTypeAdapter).nullSafe();
			@SuppressWarnings("unchecked")
			final TypeAdapter<T> typeAdapter = (TypeAdapter<T>) numberTypeAdapter;
			return typeAdapter;
		}
		return null;
	}

	private static final class NumberTypeAdapter
			extends TypeAdapter<Number> {

		private final TypeAdapter<Number> delegateNumberTypeAdapter;

		private NumberTypeAdapter(final TypeAdapter<Number> delegateNumberTypeAdapter) {
			this.delegateNumberTypeAdapter = delegateNumberTypeAdapter;
		}

		@Override
		public void write(final JsonWriter out, final Number value)
				throws IOException {
			delegateNumberTypeAdapter.write(out, value);
		}

		@Override
		public Number read(final JsonReader in)
				throws IOException {
			final String s = in.nextString();
			return parsers.stream()
					.map(parser -> parser.apply(s))
					.filter(Objects::nonNull)
					.findFirst()
					.get();
		}
	}

	private static <N extends Number> Function<String, N> parseOnNull(final Function<? super String, ? extends N> parser) {
		return s -> {
			try {
				return parser.apply(s);
			} catch ( final NumberFormatException ignored ) {
				return null;
			}
		};
	}

	private static final ImmutableList<Function<? super String, ? extends Number>> parsers = ImmutableList.<Function<? super String, ? extends Number>>builder()
			.add(parseOnNull(Byte::parseByte))
			.add(parseOnNull(Short::parseShort))
			.add(parseOnNull(Integer::parseInt))
			.add(parseOnNull(Long::parseLong))
			.add(parseOnNull(Float::parseFloat))
			.add(parseOnNull(Double::parseDouble))
			.build();

}

However, it can only work if your code can tell Gson to deserialize numbers. This won’t work:

@SuppressWarnings("unchecked")
final Map<String, Object> mapFromJson = jackson.readValue(jsonString, Map.class);

But this will:

private static final TypeToken<Map<String, Number>> stringToNumberMapTypeToken = new TypeToken<Map<String, Number>>() {
};
...
final Map<String, Object> mapFromJson = gson.fromJson(jsonString, stringToNumberMapTypeToken.getType());
Read more comments on GitHub >

github_iconTop Results From Across the Web

How to prevent Gson from converting a long number (a json ...
Gson should only convert a JSON value from 1 to 1.0 if the value property above was of type Float or Double ....
Read more >
Gson Deserialization Cookbook - Baeldung
In this cookbook, we're exploring the various ways to unmarshall JSON into Java objects, using the popular Gson library. 1. Deserialize JSON to ......
Read more >
Gson User Guide - Google Sites
It is now used by a number of public projects and companies. ... However, Gson can not automatically deserialize the pure inner classes...
Read more >
Integer value will always deserialize to be java.lang.Double
com.google.gson.internal.bind.ObjectTypeAdapter.read(JsonReader in) case NUMBER: return in.nextDouble(); It's not so good, is it? Should JsonToken add enum ...
Read more >
Leveraging the Gson Library | CodePath Android Cliffnotes
The generator will generated a large number of files that may be a bit ... We do not need any special annotations unless...
Read more >

github_iconTop Related Medium Post

No results found

github_iconTroubleshoot Live Code

Lightrun enables developers to add logs, metrics and snapshots to live code - no restarts or redeploys required.
Start Free

github_iconTop Related Reddit Thread

No results found

github_iconTop Related Hackernoon Post

No results found

github_iconTop Related Tweet

No results found

github_iconTop Related Dev.to Post

No results found

github_iconTop Related Hashnode Post

No results found