Skip to content

Commit ef91166

Browse files
committed
Issue #145 port upstream tests, verify #118 closed, bump frontier docs
- Port all nine upstream jtreg/junit test files from jdk-sandbox json branch at frontier 43325738c into json-java21 (TestAccess, TestGenerate, TestJsonArray, TestJsonLiteral, TestJsonNumber, TestJsonObject, TestJsonString, TestOtherImpl, TestParse): mechanical jtreg header removal, package rename, FieldSource to MethodSource (JUnit 5.10), boxed type patterns for the Java 21 backport of factoryTest. - Add JsonTestLoggingConfig base class (JUL per repo test rules) and JsonNumberOfDoubleMatrixTest proving issue #118 is closed by upstream: of(double) now computes decimal/exponent offsets via indexOf and the reworked JsonNumberImpl handles integral doubles, fractions, negatives, zero variants and out-of-range conversions identically to the historic of(String) delegation fix. - Add junit-jupiter-params test dependency to json-java21. - README: bump synced frontier anchor c1a4f80 to 43325738c (2026-08-27), mark the incubator migration DONE (issue #145), refresh stale accessor lists (asString/asLong/asInt/asBoolean/asList/asMap, tryGet/tryValue), fix JsonParseException getErrorLine/getErrorPosition example, record the #118 closed-by-upstream disposition in Upstream Bug Fixes. - ci.yml: exp_tests 1355 -> 1665 (full clean verify, all modules green).
1 parent 2afc50b commit ef91166

14 files changed

Lines changed: 2255 additions & 19 deletions

‎.github/workflows/ci.yml‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -39,7 +39,7 @@ jobs:
3939
for k in totals: totals[k]+=int(r.get(k,'0'))
4040
except Exception:
4141
pass
42-
exp_tests=1355
42+
exp_tests=1665
4343
exp_skipped=0
4444
if totals['tests']!=exp_tests or totals['skipped']!=exp_skipped:
4545
print(f"Unexpected test totals: {totals} != expected tests={exp_tests}, skipped={exp_skipped}")

‎README.md‎

Lines changed: 20 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -123,14 +123,16 @@ double ageDouble = obj.get("age").asDouble(); // Returns 30.0
123123
```
124124

125125
The accessor methods on `JsonValue`:
126-
- `string()` - Returns the String value (for JsonString)
127-
- `toLong()` - Returns the long value (for JsonNumber, if representable)
128-
- `toDouble()` - Returns the double value (for JsonNumber, if representable)
129-
- `bool()` - Returns the boolean value (for JsonBoolean)
130-
- `elements()` - Returns List<JsonValue> (for JsonArray)
131-
- `members()` - Returns Map<String, JsonValue> (for JsonObject)
126+
- `asString()` - Returns the String value (for JsonString)
127+
- `asLong()` - Returns the long value (for JsonNumber, if representable)
128+
- `asDouble()` - Returns the double value (for JsonNumber, if representable)
129+
- `asBoolean()` - Returns the boolean value (for JsonBoolean)
130+
- `asList()` - Returns List<JsonValue> (for JsonArray)
131+
- `asMap()` - Returns Map<String, JsonValue> (for JsonObject)
132132
- `get(String name)` - Access JsonObject member by name
133-
- `element(int index)` - Access JsonArray element by index
133+
- `get(int index)` - Access JsonArray element by index
134+
- `tryGet(String name)` - Returns Optional<JsonValue> for a JsonObject member
135+
- `tryValue()` - Returns Optional<JsonValue>, empty for JsonNull
134136

135137
### Realistic Record Mapping
136138

@@ -222,9 +224,9 @@ try {
222224
JsonValue value = Json.parse(userInput);
223225
// Process valid JSON
224226
} catch (JsonParseException e) {
225-
// Handle malformed JSON with line/column information
226-
System.err.println("Invalid JSON at line " + e.getLine() +
227-
", column " + e.getColumn() + ": " + e.getMessage());
227+
// Handle malformed JSON with line/position information
228+
System.err.println("Invalid JSON at line " + e.getErrorLine() +
229+
", position " + e.getErrorPosition() + ": " + e.getMessage());
228230
}
229231
```
230232

@@ -289,17 +291,17 @@ The test data is bundled as ZIP files and extracted automatically at runtime:
289291

290292
**Final `java.util.json` sandbox-era release** (2026-05-19).
291293

292-
This code is derived from the OpenJDK jdk-sandbox repository "json" branch at commit `c1a4f80` (2026-02-05), which was the last commit before the API was moved to `jdk.incubator.json`.
294+
This code is derived from the OpenJDK jdk-sandbox repository "json" branch at commit `43325738c` (2026-08-27), which is the current frontier of the incubator-era `jdk.incubator.json` API. The incubator promotion itself happened at commit `b956ae0` (2026-02-05); this branch completed the migration from the sandbox-era `java.util.json` naming to the incubator packages — see the notice below and issue #145.
293295

294296
### API Summary
295-
- `JsonValue` conversion methods: `asBoolean()`, `toInt()`, `toLong()`, `toDouble()`, `asString()`
296-
- `JsonValue` navigation methods: `get(String)`, `get(int)`, `getOrAbsent(String)`, `valueOrNull()`
297-
- `JsonArray`: `elements()`, `of(List)`
298-
- `JsonObject`: `members()`, `of(Map)`
297+
- `JsonValue` conversion methods: `asBoolean()`, `asString()`, `asInt()`, `asLong()`, `asDouble()`
298+
- `JsonValue` navigation methods: `get(String)`, `get(int)`, `tryGet(String)`, `tryValue()`
299+
- `JsonArray`: `asList()`, `of(List)`
300+
- `JsonObject`: `asMap()`, `of(Map)`
299301
- `Json`: `parse(String)`, `parse(char[])`, `toDisplayString(JsonValue, String indent)`
300302

301303
### Upstream Migration Notice
302-
The upstream `java.util.json` API has been promoted to `jdk.incubator.json` (commit `b956ae0`, 2026-02-05). The incubator version introduces significant API changes including method renames (`bool()`→`asBoolean()`, `string()`→`asString()`, etc.) and new methods (`asInt()`). A separate branch tracks the incubator upgrade — see issue #145.
304+
The upstream `java.util.json` API has been promoted to `jdk.incubator.json` (commit `b956ae0`, 2026-02-05). The incubator version introduces significant API changes including method renames (`bool()`→`asBoolean()`, `string()`→`asString()`, `toInt()`→`asInt()`, etc.), `tryGet()`/`tryValue()` navigation, and identity (non-value) `equals`/`hashCode`. **That migration is now DONE in this branch** (issue #145): the public API lives in `jdk.incubator.java.util.json` and the implementation in `jdk.incubator.internal.util.json`, matching upstream frontier `43325738c`.
303305

304306
The original proposal and design rationale can be found in the included PDF: [Towards a JSON API for the JDK.pdf](Towards%20a%20JSON%20API%20for%20the%20JDK.pdf)
305307

@@ -327,9 +329,9 @@ This is a simplified backport with the following changes from the original:
327329

328330
### Upstream Bug Fixes
329331

330-
The following fixes have been applied to address bugs in the upstream OpenJDK jdk-sandbox code. These are upstream issues that should be reported to the [core-libs-dev@openjdk.org](mailto:core-libs-dev@openjdk.org) mailing list per OpenJDK process:
332+
Historically this backport carried local fixes against the upstream OpenJDK jdk-sandbox code. With the uplift to upstream frontier `43325738c` their disposition is:
331333

332-
- **`JsonNumber.of(double)` offset bug** ([#118](https://github.com/simbo1905/java.util.json.Java21/issues/118)): The upstream implementation hardcodes `decimalOffset=0` and `exponentOffset=0`, causing `toLong()` to fail for integral doubles like `123.0`. Our fix delegates to `JsonNumber.of(String)` which correctly computes offsets via `Json.parse()`.
334+
- **`JsonNumber.of(double)` offset bug** ([#118](https://github.com/simbo1905/java.util.json.Java21/issues/118)): **CLOSED BY UPSTREAM — no longer carried.** Upstream reworked the numeric logic: `of(double)` now computes the decimal/exponent offsets from `Double.toString` output via `indexOf`, and `JsonNumberImpl` was rewritten with `LazyConstant`-cached conversions, trailing-zero stripping and sign/scale handling. Verified equivalent to our historic `of(String)` delegation fix by `JsonNumberOfDoubleMatrixTest` (integral doubles such as `123.0` and `1.0E2`, fractions, negatives, zero variants, out-of-range `asInt`/`asLong` throwing `JsonValueException`, and very large/small magnitudes) plus the ported upstream `TestJsonNumber`. The historic delegation hack has been removed with the uplifted upstream source.
333335

334336
## Security Considerations
335337

‎json-java21/pom.xml‎

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,11 @@
3434
<artifactId>junit-jupiter-engine</artifactId>
3535
<scope>test</scope>
3636
</dependency>
37+
<dependency>
38+
<groupId>org.junit.jupiter</groupId>
39+
<artifactId>junit-jupiter-params</artifactId>
40+
<scope>test</scope>
41+
</dependency>
3742
<dependency>
3843
<groupId>org.assertj</groupId>
3944
<artifactId>assertj-core</artifactId>
Lines changed: 116 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,116 @@
1+
package jdk.incubator.java.util.json;
2+
3+
import org.junit.jupiter.api.Test;
4+
5+
import static org.junit.jupiter.api.Assertions.assertEquals;
6+
import static org.junit.jupiter.api.Assertions.assertNotEquals;
7+
import static org.junit.jupiter.api.Assertions.assertThrows;
8+
9+
/// Evidence matrix for issue #118 (`JsonNumber.of(double)` decimal/exponent
10+
/// offsets). The historic local fix delegated `of(double)` to `of(String)`;
11+
/// upstream has since reworked the numeric logic to compute the offsets via
12+
/// `indexOf`. These tests prove the uplifted upstream implementation handles
13+
/// the full matrix that motivated #118, making the carried fix unnecessary.
14+
public class JsonNumberOfDoubleMatrixTest extends JsonTestLoggingConfig {
15+
16+
private static void assertIntegralDouble(double d, long expected) {
17+
var jn = JsonNumber.of(d);
18+
// of(double) must be equivalent to the historic of(String) delegation
19+
var viaString = JsonNumber.of(Double.toString(d));
20+
assertEquals(viaString.toString(), jn.toString(), "toString for " + d);
21+
assertEquals(expected, jn.asLong(), "asLong for " + d);
22+
assertEquals((int) expected, jn.asInt(), "asInt for " + d);
23+
assertEquals(d, jn.asDouble(), "asDouble for " + d);
24+
}
25+
26+
@Test
27+
void integralDoublesConvertExactly() {
28+
assertIntegralDouble(123.0, 123L);
29+
assertIntegralDouble(1.0E2, 100L);
30+
assertIntegralDouble(42.0, 42L);
31+
assertIntegralDouble(420e-1, 42L);
32+
assertIntegralDouble(42e6, 42_000_000L);
33+
assertIntegralDouble(0.0, 0L);
34+
assertIntegralDouble(1.0, 1L);
35+
assertIntegralDouble(5.000, 5L);
36+
}
37+
38+
@Test
39+
void negativeIntegralDoublesConvertExactly() {
40+
assertIntegralDouble(-123.0, -123L);
41+
assertIntegralDouble(-42e6, -42_000_000L);
42+
assertIntegralDouble(-1.0, -1L);
43+
}
44+
45+
@Test
46+
void negativeZeroBehaves() {
47+
var jn = JsonNumber.of(-0.0);
48+
assertEquals("-0.0", jn.toString());
49+
assertEquals(0L, jn.asLong());
50+
assertEquals(0, jn.asInt());
51+
assertEquals(-0.0, jn.asDouble());
52+
assertEquals(0.0, jn.asDouble() + 0.0);
53+
}
54+
55+
@Test
56+
void fractionalDoublesAreNotIntegral() {
57+
assertNotIntegral(123.45);
58+
assertNotIntegral(0.1);
59+
assertNotIntegral(0.002);
60+
assertNotIntegral(-123.45);
61+
}
62+
63+
private static void assertNotIntegral(double d) {
64+
var jn = JsonNumber.of(d);
65+
assertThrows(JsonValueException.class, jn::asLong, "asLong for " + d);
66+
assertThrows(JsonValueException.class, jn::asInt, "asInt for " + d);
67+
assertEquals(d, jn.asDouble(), "asDouble for " + d);
68+
// of(double) must match the of(String) representation exactly
69+
assertEquals(JsonNumber.of(Double.toString(d)).toString(), jn.toString());
70+
}
71+
72+
@Test
73+
void outOfRangeIntegralThrowsJsonValueException() {
74+
assertNotConvertibleToLong(1e300);
75+
assertNotConvertibleToLong(-1e300);
76+
assertNotConvertibleToLong(Double.MAX_VALUE);
77+
assertNotConvertibleToLong(9.3e18);
78+
}
79+
80+
private static void assertNotConvertibleToLong(double d) {
81+
var jn = JsonNumber.of(d);
82+
assertThrows(JsonValueException.class, jn::asLong, "asLong for " + d);
83+
assertThrows(JsonValueException.class, jn::asInt, "asInt for " + d);
84+
assertEquals(d, jn.asDouble(), "asDouble for " + d);
85+
}
86+
87+
@Test
88+
void tinyMagnitudesRoundTripAsDouble() {
89+
var tiny = JsonNumber.of(4.9E-324);
90+
assertEquals(4.9E-324, tiny.asDouble());
91+
assertThrows(JsonValueException.class, tiny::asLong);
92+
var small = JsonNumber.of(5e-100);
93+
assertEquals(5e-100, small.asDouble());
94+
assertThrows(JsonValueException.class, small::asLong);
95+
}
96+
97+
@Test
98+
void ofDoubleMatchesParsedEquivalent() {
99+
// of(double) goes through Double.toString; parsing that same text must
100+
// produce the same conversions (the equivalence #118 was about)
101+
for (double d : new double[]{123.0, 1.0E2, 42e6, 0.0, -0.0, 123.45, 1e300, 4.9E-324}) {
102+
var factory = JsonNumber.of(d);
103+
var parsed = Json.parse(Double.toString(d));
104+
assertNotEquals(parsed, factory); // identity semantics; compare behaviour
105+
assertEquals(factory.toString(), parsed.toString(), "toString for " + d);
106+
assertEquals(factory.asDouble(), ((JsonNumber) parsed).asDouble(), "asDouble for " + d);
107+
}
108+
}
109+
110+
@Test
111+
void nonFiniteDoublesRejected() {
112+
assertThrows(IllegalArgumentException.class, () -> JsonNumber.of(Double.NaN));
113+
assertThrows(IllegalArgumentException.class, () -> JsonNumber.of(Double.POSITIVE_INFINITY));
114+
assertThrows(IllegalArgumentException.class, () -> JsonNumber.of(Double.NEGATIVE_INFINITY));
115+
}
116+
}
Lines changed: 57 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,57 @@
1+
package jdk.incubator.java.util.json;
2+
3+
import org.junit.jupiter.api.BeforeAll;
4+
import org.junit.jupiter.api.BeforeEach;
5+
import org.junit.jupiter.api.TestInfo;
6+
7+
import java.util.Locale;
8+
import java.util.logging.Level;
9+
import java.util.logging.Logger;
10+
11+
/// Base class for the ported upstream JSON tests. Configures JUL logging from
12+
/// the `java.util.logging.ConsoleHandler.level` system property so that
13+
/// `-Djava.util.logging.ConsoleHandler.level=FINE` (etc.) works uniformly, and
14+
/// announces each test method execution at INFO level.
15+
public class JsonTestLoggingConfig {
16+
17+
static final Logger LOG = Logger.getLogger(JsonTestLoggingConfig.class.getName());
18+
19+
@BeforeAll
20+
static void enableJulDebug() {
21+
Logger root = Logger.getLogger("");
22+
String levelProp = System.getProperty("java.util.logging.ConsoleHandler.level");
23+
Level targetLevel = resolveLevel(levelProp);
24+
// Ensure the root logger honors the most verbose configured level
25+
if (root.getLevel() == null || root.getLevel().intValue() > targetLevel.intValue()) {
26+
root.setLevel(targetLevel);
27+
}
28+
for (var handler : root.getHandlers()) {
29+
Level handlerLevel = handler.getLevel();
30+
if (handlerLevel == null || handlerLevel.intValue() > targetLevel.intValue()) {
31+
handler.setLevel(targetLevel);
32+
}
33+
}
34+
LOG.config(() -> "JUL level configured for JSON tests: " + targetLevel);
35+
}
36+
37+
private static Level resolveLevel(String levelProp) {
38+
Level targetLevel = Level.INFO;
39+
if (levelProp != null) {
40+
try {
41+
targetLevel = Level.parse(levelProp.trim());
42+
} catch (IllegalArgumentException ex) {
43+
try {
44+
targetLevel = Level.parse(levelProp.trim().toUpperCase(Locale.ROOT));
45+
} catch (IllegalArgumentException ignored) {
46+
LOG.warning(() -> "Unrecognized logging level from 'java.util.logging.ConsoleHandler.level': " + levelProp);
47+
}
48+
}
49+
}
50+
return targetLevel;
51+
}
52+
53+
@BeforeEach
54+
void announceTest(TestInfo testInfo) {
55+
LOG.info(() -> "Running test: " + testInfo.getDisplayName());
56+
}
57+
}

0 commit comments

Comments
 (0)