Skip to content

Commit 3dd4c87

Browse files
runningcodeclaude
andauthored
perf(json): Avoid exceptions when typing JSON numbers (JAVA-536) (#5783)
* perf(json): Avoid exceptions when typing JSON numbers (JAVA-536) JsonObjectDeserializer typed numbers by calling nextInt() and catching the NumberFormatException it throws for every non-integer value, then falling back to nextDouble(). For payloads full of floating-point values (timestamps, measurements) this threw and filled a stack trace on nearly every number, dominating the cost of deserializing arbitrary objects. Parse the value as a double once and narrow it back to an int only when it is integral and fits, which avoids the throws. Return types are unchanged (Integer for values that fit an int, Double otherwise), so callers that read integers out of the generic object tree are unaffected. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * changelog --------- Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
1 parent cb92748 commit 3dd4c87

3 files changed

Lines changed: 60 additions & 10 deletions

File tree

CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@
1414
### Performance
1515

1616
- Reduce the number of SDK threads: `LifecycleWatcher` now schedules the session-end task on the shared timer executor instead of creating a dedicated `java.util.Timer` thread ([#5819](https://github.com/getsentry/sentry-java/pull/5819))
17+
- Speed up deserialization of arbitrary JSON objects by typing numbers without throwing exceptions ([#5783](https://github.com/getsentry/sentry-java/pull/5783))
1718

1819
## 8.50.1
1920

sentry/src/main/java/io/sentry/JsonObjectDeserializer.java

Lines changed: 9 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -172,17 +172,16 @@ private boolean handlePrimitive(NextValue callback) throws IOException {
172172
}
173173

174174
private Object nextNumber(JsonObjectReader reader) throws IOException {
175-
try {
176-
return reader.nextInt();
177-
} catch (Exception exception) {
178-
// Need to try/fail as there are no int/double/long tokens.
175+
// JSON has no int/double token distinction. Probing with reader.nextInt() and catching the
176+
// NumberFormatException it throws for every non-integer (e.g. timestamps) dominated the cost of
177+
// deserializing arbitrary objects. Read once as a double and narrow to an int only when the
178+
// value is integral and fits, which reproduces the previous return types without any throws.
179+
final double value = reader.nextDouble();
180+
final int intValue = (int) value;
181+
if (intValue == value) {
182+
return intValue;
179183
}
180-
try {
181-
return reader.nextDouble();
182-
} catch (Exception exception) {
183-
// Need to try/fail as there are no int/double/long tokens.
184-
}
185-
return reader.nextLong();
184+
return value;
186185
}
187186

188187
private @Nullable Token getCurrentToken() {

sentry/src/test/java/io/sentry/JsonObjectDeserializerTest.kt

Lines changed: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,10 @@
11
package io.sentry
22

3+
import java.io.IOException
34
import java.io.StringReader
45
import java.lang.Exception
56
import kotlin.test.assertEquals
7+
import kotlin.test.assertFailsWith
68
import kotlin.test.assertNull
79
import kotlin.test.fail
810
import org.junit.Test
@@ -42,6 +44,54 @@ class JsonObjectDeserializerTest {
4244
assertEquals(1.1, actual)
4345
}
4446

47+
// A value that is integral and fits an int is typed as Integer (regardless of how it was
48+
// written); anything else is a Double. This matches the behavior prior to removing the
49+
// exception-based number typing.
50+
51+
@Test
52+
fun `deserialize negative int`() {
53+
assertEquals(-5, deserialize("-5"))
54+
}
55+
56+
@Test
57+
fun `deserialize negative double`() {
58+
assertEquals(-3.14, deserialize("-3.14"))
59+
}
60+
61+
@Test
62+
fun `deserialize integral exponent notation as int`() {
63+
assertEquals(100, deserialize("1e2"))
64+
assertEquals(100, deserialize("1E2"))
65+
}
66+
67+
@Test
68+
fun `deserialize fractional exponent notation as double`() {
69+
assertEquals(0.0025, deserialize("2.5e-3"))
70+
}
71+
72+
@Test
73+
fun `deserialize whole-valued decimal as int`() {
74+
assertEquals(1, deserialize("1.0"))
75+
}
76+
77+
@Test
78+
fun `deserialize integer larger than int range as double`() {
79+
assertEquals(1.0e10, deserialize("10000000000"))
80+
assertEquals(2147483648.0, deserialize("2147483648"))
81+
}
82+
83+
@Test
84+
fun `deserialize max int as int`() {
85+
assertEquals(Int.MAX_VALUE, deserialize("2147483647"))
86+
}
87+
88+
@Test
89+
fun `deserialize rejects literal overflowing to infinity`() {
90+
// Strict JSON forbids non-finite numbers, so an out-of-range literal must fail rather than be
91+
// stored as Infinity.
92+
assertFailsWith<IOException> { deserialize("1e400") }
93+
}
94+
4595
@Test
4696
fun `deserialize array`() {
4797
val json = "[\"a\",\"b\"]"

0 commit comments

Comments
 (0)