Skip to content
Merged
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
4 changes: 2 additions & 2 deletions .github/copilot-instructions.md
Original file line number Diff line number Diff line change
Expand Up @@ -81,8 +81,8 @@ return switch (jsonValue) {
```

### Project Architecture
- Core package: `jdk.sandbox.java.util.json`
- Internal implementation: `jdk.sandbox.internal.util.json`
- Core package: `jdk.incubator.java.util.json`
- Internal implementation: `jdk.incubator.internal.util.json`
- JSON Schema validator in separate module
- Use appropriate logging configuration per module

Expand Down
2 changes: 1 addition & 1 deletion .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -39,7 +39,7 @@ jobs:
for k in totals: totals[k]+=int(r.get(k,'0'))
except Exception:
pass
exp_tests=1355
exp_tests=1676
exp_skipped=0
if totals['tests']!=exp_tests or totals['skipped']!=exp_skipped:
print(f"Unexpected test totals: {totals} != expected tests={exp_tests}, skipped={exp_skipped}")
Expand Down
16 changes: 8 additions & 8 deletions AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -124,7 +124,7 @@ throw new IllegalArgumentException("enum contains duplicate values: " +

// Include the problematic schema portion
throw new IllegalArgumentException("Type schema contains unknown key: " + key +
" in schema: " + Json.toDisplayString(obj, 0));
" in schema: " + Json.toDisplayString(obj, ""));

// Include both expected and actual values
throw new IllegalArgumentException("unknown type: '" + typeStr +
Expand All @@ -138,7 +138,7 @@ throw new IllegalArgumentException("invalid schema"); // Too vague
throw new IllegalArgumentException("bad value"); // No specifics
```

Use `Json.toDisplayString(value, depth)` to render JSON fragments in error messages, and include relevant context like schema paths, actual vs expected values, and specific constraint violations.
Use `Json.toDisplayString(value, indent)` to render JSON fragments in error messages, and include relevant context like schema paths, actual vs expected values, and specific constraint violations.

## JSON Compatibility Suite

Expand All @@ -151,15 +151,15 @@ See `README.md` for user-facing commands. When running locally as an agent, use
- `json-java21-api-tracker`: API evolution tracking utilities.
- `json-compatibility-suite`: JSON Test Suite compatibility validation.
- `json-java21-jtd`: JSON Type Definition (JTD) validator based on RFC 8927.
- `json-java21-jsonpath`: JsonPath query engine over `jdk.sandbox.java.util.json` values.
- `json-java21-jsonpath`: JsonPath query engine over `jdk.incubator.java.util.json` values.

Only when you are asked to work on a specific module, start by reading that module's `README.md`, then its `AGENTS.md`.

These modules are treated as separate subsystems; do not read their docs unless you are actively working on them. They are not interlinked and each depends only on the core `json-java21` API.

### Core Components

#### Public API (`jdk.sandbox.java.util.json`)
#### Public API (`jdk.incubator.java.util.json`)
- `Json`: Static utilities for parsing, formatting, and conversion.
- `JsonValue`: Sealed root interface for all JSON types.
- `JsonObject`: JSON objects (key-value pairs).
Expand All @@ -169,14 +169,14 @@ These modules are treated as separate subsystems; do not read their docs unless
- `JsonBoolean`: JSON booleans.
- `JsonNull`: JSON null.

IMPORTANT: This API **MUST NOT** deviate from the upstream jdk.sandbox repo which is will track.
IMPORTANT: This API **MUST NOT** deviate from the upstream jdk.incubator repo which is will track.

#### Internal Implementation (`jdk.sandbox.internal.util.json`)
#### Internal Implementation (`jdk.incubator.internal.util.json`)
- `JsonParser`: Recursive descent JSON parser.
- `Json*Impl`: Immutable implementations of `Json*` types.
- `Utils`: Internal utilities and factory methods.

IMPORTANT: Bugs in upstream-derived core logic MUST be fixed upstream. Do not patch `jdk.sandbox.*` sources in this repo unless the user explicitly agrees (for example, to carry a temporary local backport while the upstream fix is in progress).
IMPORTANT: Bugs in upstream-derived core logic MUST be fixed upstream. Do not patch `jdk.incubator.*` sources in this repo unless the user explicitly agrees (for example, to carry a temporary local backport while the upstream fix is in progress).

Only bugs in local, non-upstream code (for example, backporting shims/polyfills or other modules in this repo) should be fixed here using normal TDD.

Expand Down Expand Up @@ -480,7 +480,7 @@ flowchart LR
python3 - <<'PY'
import os, sys, re
src = 'updates/2025-09-04/upstream/jdk.internal.util.json'
dst = 'json-java21/src/main/java/jdk/sandbox/internal/util/json'
dst = 'json-java21/src/main/java/jdk/incubator/internal/util/json'
def xform(text):
# old old python3 stuff here
print('OK')
Expand Down
98 changes: 53 additions & 45 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@ This repo is organized into the following modules:
| `json-java21-jtd` | JTD (RFC 8927) stack-machine interpreter — ideal for infrequent config parsing and one-time validation | 21+ |
| `json-java21-jtd-codegen` | Bytecode code generator for JTD schemas — ahead-of-time compiled validators for repeated hot-path validation | 24+ (auto-skipped on JDK 21) |
| `jdt2jar` | CLI + distroless container to pre-compile JTD schemas into standalone validator JARs (eliminates JDK 24+ runtime requirement) | 24+ (auto-skipped on JDK 21) |
| `json-java21-jsonpath` | JsonPath query engine over `jdk.sandbox.java.util.json` values (Goessner-style: filters, slices, recursive descent, unions) | 21+ |
| `json-java21-jsonpath` | JsonPath query engine over `jdk.incubator.java.util.json` values (Goessner-style: filters, slices, recursive descent, unions) | 21+ |
| `json-compatibility-suite` | JSON Test Suite conformance reporter (tests against [nst/JSONTestSuite](https://github.com/nst/JSONTestSuite)) | 21+ |
| `json-java21-api-tracker` | Daily upstream API drift detector — fetches OpenJDK sandbox sources, compares public API signatures, reports differences | 25+ |

Expand All @@ -31,7 +31,7 @@ To try the examples from this README, build the project and run the standalone e

```bash
./mvnw package
java -cp ./json-java21/target/test-classes/:./json-java21/target/classes/ jdk.sandbox.java.util.json.examples.ReadmeExamples
java -cp ./json-java21/target/test-classes/:./json-java21/target/classes/ jdk.incubator.java.util.json.examples.ReadmeExamples
```

## API Overview
Expand Down Expand Up @@ -60,9 +60,9 @@ JsonValue value = Json.parse(json);

// Access as map-like structure
JsonObject obj = (JsonObject) value;
String name = ((JsonString) obj.members().get("name")).string();
long age = ((JsonNumber) obj.members().get("age")).toLong();
boolean active = ((JsonBoolean) obj.members().get("active")).bool();
String name = ((JsonString) obj.asMap().get("name")).asString();
long age = ((JsonNumber) obj.asMap().get("age")).asLong();
boolean active = ((JsonBoolean) obj.asMap().get("active")).asBoolean();
```

### Simple Record Mapping
Expand All @@ -77,9 +77,9 @@ JsonObject jsonObj = (JsonObject) Json.parse(userJson);

// Map to record
User user = new User(
((JsonString) jsonObj.members().get("name")).string(),
((JsonNumber) jsonObj.members().get("age")).toLong(),
((JsonBoolean) jsonObj.members().get("active")).bool()
((JsonString) jsonObj.asMap().get("name")).asString(),
((JsonNumber) jsonObj.asMap().get("age")).asLong(),
((JsonBoolean) jsonObj.asMap().get("active")).asBoolean()
);

// Convert records back to JSON using typed factories
Expand Down Expand Up @@ -117,20 +117,22 @@ JsonValue parsed = Json.parse("{\"name\":\"John\",\"age\":30}");
JsonObject obj = (JsonObject) parsed;

// Use the new type-safe accessor methods
String name = obj.get("name").string(); // Returns "John"
long age = obj.get("age").toLong(); // Returns 30L
double ageDouble = obj.get("age").toDouble(); // Returns 30.0
String name = obj.get("name").asString(); // Returns "John"
long age = obj.get("age").asLong(); // Returns 30L
double ageDouble = obj.get("age").asDouble(); // Returns 30.0
```

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

### Realistic Record Mapping

Expand Down Expand Up @@ -162,14 +164,14 @@ JsonValue teamJson = JsonObject.of(Map.of(
// Parse JSON back to records
JsonObject parsed = (JsonObject) Json.parse(teamJson.toString());
Team reconstructed = new Team(
((JsonString) parsed.members().get("teamName")).string(),
((JsonArray) parsed.members().get("members")).elements().stream()
((JsonString) parsed.asMap().get("teamName")).asString(),
((JsonArray) parsed.asMap().get("members")).asList().stream()
.map(v -> {
JsonObject member = (JsonObject) v;
return new User(
((JsonString) member.members().get("name")).string(),
((JsonString) member.members().get("email")).string(),
((JsonBoolean) member.members().get("active")).bool()
((JsonString) member.asMap().get("name")).asString(),
((JsonString) member.asMap().get("email")).asString(),
((JsonBoolean) member.asMap().get("active")).asBoolean()
);
})
.toList()
Expand Down Expand Up @@ -206,10 +208,10 @@ Process JSON arrays efficiently with Java streams:
```java
// Filter active users from a JSON array
JsonArray users = (JsonArray) Json.parse(jsonArrayString);
List<String> activeUserEmails = users.elements().stream()
List<String> activeUserEmails = users.asList().stream()
.map(v -> (JsonObject) v)
.filter(obj -> ((JsonBoolean) obj.members().get("active")).bool())
.map(obj -> ((JsonString) obj.members().get("email")).string())
.filter(obj -> ((JsonBoolean) obj.asMap().get("active")).asBoolean())
.map(obj -> ((JsonString) obj.asMap().get("email")).asString())
.toList();
```

Expand All @@ -222,9 +224,9 @@ try {
JsonValue value = Json.parse(userInput);
// Process valid JSON
} catch (JsonParseException e) {
// Handle malformed JSON with line/column information
System.err.println("Invalid JSON at line " + e.getLine() +
", column " + e.getColumn() + ": " + e.getMessage());
// Handle malformed JSON with line/position information
System.err.println("Invalid JSON at line " + e.getErrorLine() +
", position " + e.getErrorPosition() + ": " + e.getMessage());
}
```

Expand All @@ -242,7 +244,7 @@ JsonObject data = JsonObject.of(Map.of(
))
));

String formatted = Json.toDisplayString(data, 2);
String formatted = Json.toDisplayString(data, " ");
// Output:
// {
// "name": "Alice",
Expand Down Expand Up @@ -289,41 +291,47 @@ The test data is bundled as ZIP files and extracted automatically at runtime:

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

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`.
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.

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

### Upstream Migration Notice
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.
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`.

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)

The JSON compatibitlity tests in this repo suggest 99% conformance with a leading test suite when in "strict" mode. The two conformance expecatations that fail assume that duplicated keys in a JSON document are okay. The upstream code at this time appear to take a strict stance that it should not siliently ignore duplicate keys in a json object.

### CI: Upstream API Tracking

**Note**: The daily API tracker workflow currently targets the old `java.util.json` paths which no longer exist upstream. It needs to be updated to track `jdk.incubator.json` — see issue #145.
The `daily-api-tracker.yml` workflow runs daily at 02:00 UTC: it fetches the upstream `jdk.incubator.json` sources from the [jdk-sandbox `json` branch](https://github.com/openjdk/jdk-sandbox/tree/json) HEAD and compares public API signatures against the local `jdk.incubator.java.util.json` classes. When they differ it creates a fingerprint-deduplicated "API drift detected" issue; reports are uploaded as workflow artifacts (`target/api-tracker/`) with 90-day retention. The check can also be run locally:

```bash
$(command -v mvnd || command -v mvn || command -v ./mvnw) -pl json-java21-api-tracker exec:java \
-Dexec.mainClass="io.github.simbo1905.tracker.ApiTrackerRunner" \
-Dexec.args="INFO"
```

## Modifications

This is a simplified backport with the following changes from the original:
- Replaced `LazyConstant` with a package-local polyfill using double-checked locking pattern.
- Added `Utils.powExact()` polyfill for `Math.powExact(long, int)` which is not available in Java 21.
- Replaced unnamed variables `_` with `ignored` for Java 21 compatibility.
- Replaced unnamed variables `_` with named variables (`e`, `v`, `k`) for Java 21 compatibility.
- Removed `@ValueBased` annotations.
- Removed `@PreviewFeature` annotations.
- Compatible with JDK 21.

### Upstream Bug Fixes

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:
Historically this backport carried local fixes against the upstream OpenJDK jdk-sandbox code. With the uplift to upstream frontier `43325738c` their disposition is:

- **`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()`.
- **`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.

## Security Considerations

Expand Down Expand Up @@ -355,7 +363,7 @@ Per **RFC 8927 (JSON Typedef)**, the empty schema `{}` is the **empty form** and

```java
import json.java21.jtd.Jtd;
import jdk.sandbox.java.util.json.*;
import jdk.incubator.java.util.json.*;

JsonValue schema = Json.parse("{\"properties\":{\"name\":{\"type\":\"string\"}}}");
JsonValue data = Json.parse("{\"name\":\"Alice\"}");
Expand Down Expand Up @@ -394,7 +402,7 @@ This repo also includes a JsonPath query engine (module `json-java21-jsonpath`),
https://goessner.net/articles/JsonPath/

```java
import jdk.sandbox.java.util.json.*;
import jdk.incubator.java.util.json.*;
import json.java21.jsonpath.JsonPath;
import json.java21.jsonpath.JsonPathStreams;

Expand All @@ -409,7 +417,7 @@ JsonValue doc = Json.parse("""
var authors = JsonPath.parse("$.store.book[*].author")
.query(doc)
.stream()
.map(JsonValue::string)
.map(JsonValue::asString)
.toList();

System.out.println("Authors count: " + authors.size()); // prints '3'
Expand All @@ -419,7 +427,7 @@ System.out.println("Last author: " + authors.getLast()); // prints 'Marek Ily
var cheapTitles = JsonPath.parse("$.store.book[?(@.price < 10)].title")
.query(doc)
.stream()
.map(JsonValue::string)
.map(JsonValue::asString)
.toList();

var priceStats = JsonPath.parse("$.store.book[*].price")
Expand Down
Loading
Loading