Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion gson/src/main/java/com/google/gson/Gson.java
Original file line number Diff line number Diff line change
Expand Up @@ -1195,7 +1195,7 @@ public <T> T fromJson(JsonElement json, TypeToken<T> typeOfT) throws JsonSyntaxE

private static void assertFullConsumption(Object obj, JsonReader reader) {
try {
if (obj != null && reader.peek() != JsonToken.END_DOCUMENT) {
if (reader.peek() != JsonToken.END_DOCUMENT) {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This accidentally includes changes from your other PR?

throw new JsonSyntaxException("JSON document was not fully consumed.");
}
} catch (MalformedJsonException e) {
Expand Down
2 changes: 1 addition & 1 deletion gson/src/main/java/com/google/gson/JsonParser.java
Original file line number Diff line number Diff line change
Expand Up @@ -108,7 +108,7 @@ public static JsonElement parseReader(Reader reader) throws JsonIOException, Jso
try {
JsonReader jsonReader = new JsonReader(reader);
JsonElement element = parseReader(jsonReader);
if (!element.isJsonNull() && jsonReader.peek() != JsonToken.END_DOCUMENT) {
if (jsonReader.peek() != JsonToken.END_DOCUMENT) {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This accidentally includes changes from your other PR?

throw new JsonSyntaxException("Did not consume the entire document.");
}
return element;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -258,7 +258,29 @@ public long nextLong() throws IOException {
throw new IllegalStateException(
"Expected " + JsonToken.NUMBER + " but was " + token + locationString());
}
long result = ((JsonPrimitive) peekStack()).getAsLong();
Object o = peekStack();
long result;
if (o instanceof JsonPrimitive) {
JsonPrimitive primitive = (JsonPrimitive) o;
if (primitive.isNumber()) {
Number number = primitive.getAsNumber();
if (number instanceof Long || number instanceof Integer) {
result = number.longValue();
} else {
Comment on lines +267 to +269

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This will probably need explicit handling for BigInteger and BigDecimal (for nextLong() only) because values near Long.MAX_VALUE cannot be represented exactly by double.
So with your changes this would break behavior which was working previously for such values.

Maybe what you could do is:

  • For BigDecimal
    1. call BigDecimal#longValueExact()
    2. catch the ArithmeticException and wrap it in a descriptive NumberFormatException
  • For BigInteger
    1. check bitLength() >= 64 and throw NumberFormatException in that case
      Cannot call BigInteger#longValueExact() because it seems that method was only added recently to Android, for an API Level higher than Gson's minimum one. But maybe add a TODO comment to use that method in the future?
      See also Issue 1994 #2031 (comment).
    2. call BigInteger#longValue()

This is just a suggestion though. Maybe there are other ways to solve this as well.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Similarly, might also need special handling for LazilyParsedNumber since that might also be common inside JsonPrimitive.

Maybe LazilyParsedNumber would need a new longValueExact() for that, maybe something like this?

public long longValueExact() {
  try {
    return Long.parseLong(value);
  } catch (NumberFormatException ignored) {
    BigDecimal bigDecimal = asBigDecimal();
    try {
      return bigDecimal.longValueExact();
    } catch (ArithmeticException arithmeticException) {
      NumberFormatException e = new NumberFormatException(arithmeticException.getMessage());
      e.initCause(arithmeticException);
      throw e;
    }
  }
}

(but might have to catch the NumberFormatException here then, wrap it and include locationString()?)

// For other number types (e.g., Double, BigDecimal), check for loss of precision
double doubleValue = number.doubleValue();
result = (long) doubleValue;
if (result != doubleValue) {
throw new NumberFormatException(
"Expected a long but was " + number + locationString());
}
}
} else {
result = Long.parseLong(primitive.getAsString());

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Maybe this could instead still call getAsLong()?

Suggested change
result = Long.parseLong(primitive.getAsString());
result = primitive.getAsLong();

}
} else {
throw new IllegalStateException("Unexpected token: " + o);

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Might be better to use AssertionError to make it clear that this should be unreachable.

Suggested change
throw new IllegalStateException("Unexpected token: " + o);
throw new AssertionError("Unexpected token: " + o);

}
popStack();
if (stackSize > 0) {
pathIndices[stackSize - 1]++;
Expand All @@ -273,7 +295,29 @@ public int nextInt() throws IOException {
throw new IllegalStateException(
"Expected " + JsonToken.NUMBER + " but was " + token + locationString());
}
int result = ((JsonPrimitive) peekStack()).getAsInt();
Object o = peekStack();
int result;
if (o instanceof JsonPrimitive) {
JsonPrimitive primitive = (JsonPrimitive) o;
if (primitive.isNumber()) {
Number number = primitive.getAsNumber();
if (number instanceof Integer) {
result = number.intValue();
} else {
// For other number types (e.g., Long, Double, BigDecimal), check for loss of precision
double doubleValue = number.doubleValue();
result = (int) doubleValue;
if (result != doubleValue) {
throw new NumberFormatException(
"Expected an int but was " + number + locationString());
}
}
} else {
result = Integer.parseInt(primitive.getAsString());

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Maybe this could instead still call getAsInt()?

Suggested change
result = Integer.parseInt(primitive.getAsString());
result = primitive.getAsInt();

}
} else {
throw new IllegalStateException("Unexpected token: " + o);

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Might be better to use AssertionError to make it clear that this should be unreachable.

Suggested change
throw new IllegalStateException("Unexpected token: " + o);
throw new AssertionError("Unexpected token: " + o);

}
popStack();
if (stackSize > 0) {
pathIndices[stackSize - 1]++;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -199,4 +199,27 @@ public void testOverrides() {
"getNestingLimit()");
MoreAsserts.assertOverridesMethods(JsonReader.class, JsonTreeReader.class, ignoredMethods);
}

// Tests for issue #2817: Inconsistency JsonReader vs JsonTreeReader about double precision loss
@Test
public void testNextIntThrowsForDouble() throws IOException {
JsonObject json = new JsonObject();
json.addProperty("value", 42.123);
JsonTreeReader reader = new JsonTreeReader(json);
reader.beginObject();
reader.nextName();
NumberFormatException e = assertThrows(NumberFormatException.class, reader::nextInt);
assertThat(e).hasMessageThat().startsWith("Expected an int but was");
}

@Test
public void testNextLongThrowsForDouble() throws IOException {
JsonObject json = new JsonObject();
json.addProperty("value", 42.123);
JsonTreeReader reader = new JsonTreeReader(json);
reader.beginObject();
reader.nextName();
NumberFormatException e = assertThrows(NumberFormatException.class, reader::nextLong);
assertThat(e).hasMessageThat().startsWith("Expected a long but was");
}
}
Loading