Skip to content

JsonNumber.of(double).toLong() fails for whole numbers #116

@simbo1905

Description

@simbo1905

Summary

JsonNumber.of(double) passes hardcoded (decimalOffset=0, exponentOffset=0) to JsonNumberImpl, which causes toLong() to fail even for whole numbers like 1.0, 1000.0, or 1e10.

This bug exists in upstream OpenJDK jdk-sandbox json branch as well.

Steps to Reproduce

On the sync-upstream-api branch, run the demo class:

git checkout sync-upstream-api
mvn -pl json-java21 clean compile test-compile
java -cp json-java21/target/classes:json-java21/target/test-classes \
  jdk.sandbox.internal.util.json.PrReviewIssuesDemo

Expected Behavior

JsonNumber.of(1.0).toLong()  // Should return 1L
JsonNumber.of(1000.0).toLong()  // Should return 1000L
JsonNumber.of(1e10).toLong()  // Should return 10000000000L

Actual Behavior

Test: JsonNumber.of(1.0)
  toString() = '1.0'
  toLong() threw: 1.0 cannot be represented as a long. Path: "". Location: line 0, position 0.
  [CONFIRMED BUG]

Test: JsonNumber.of(1000.0)
  toString() = '1000.0'
  toLong() threw: 1000.0 cannot be represented as a long. Path: "". Location: line 0, position 0.
  [CONFIRMED BUG]

Test: JsonNumber.of(1e10) - produces '1.0E10'
  toString() = '1.0E10'
  toLong() threw: 1.0E10 cannot be represented as a long. Path: "". Location: line 0, position 0.
  [CONFIRMED BUG]

However, parsing the same value from a JSON string works correctly:

Control: Json.parse("1.0") - parsed from string
  toString() = '1.0'
  toLong() = 1 [Parser sets correct offsets]

Root Cause

In JsonNumber.java line 79-80:

static JsonNumber of(double num) {
    if (!Double.isFinite(num)) {
        throw new IllegalArgumentException("Not a valid JSON number");
    }
    var str = Double.toString(num);
    return new JsonNumberImpl(str.toCharArray(), 0, str.length(), 0, 0);
    //                                                           ^  ^
    //                                               decimalOffset  exponentOffset
}

The problem is decimalOffset=0 and exponentOffset=0 are passed regardless of where (or if) the decimal point and exponent actually appear in the string.

For "1.0":

  • Actual decimal position is at index 1
  • Passed decimalOffset=0 (wrong)

The initNumLong() method in JsonNumberImpl uses these offsets to determine how to parse the number. When decimalOffset != -1, it takes a branch that expects the decimal at that position, leading to incorrect parsing.

Upstream Reference

This bug exists in upstream OpenJDK:

Potential Fix

Compute the actual decimal and exponent offsets from the string:

static JsonNumber of(double num) {
    if (!Double.isFinite(num)) {
        throw new IllegalArgumentException("Not a valid JSON number");
    }
    var str = Double.toString(num);
    var chars = str.toCharArray();
    int decOff = str.indexOf('.');
    int expOff = Math.max(str.indexOf('e'), str.indexOf('E'));
    return new JsonNumberImpl(chars, 0, chars.length, decOff, expOff);
}

Decision Required

Since this is a backport, we have options:

  1. Keep as-is - faithfully reproduce upstream bug, document the limitation
  2. Fix locally - diverge from upstream to fix the bug (may complicate future syncs)
  3. Report upstream - file a bug with OpenJDK and wait for their fix

Related

Metadata

Metadata

Assignees

No one assigned

    Labels

    bugSomething isn't working

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions