Skip to content
Closed
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
5 changes: 5 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,11 @@ target/

.idea/

# Eclipse
.classpath
.project
.settings/

.claude/
.aider*
CLAUDE.md
Expand Down
104 changes: 35 additions & 69 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -41,9 +41,9 @@ JsonValue value = Json.parse(json);

// Access as map-like structure
JsonObject obj = (JsonObject) value;
String name = ((JsonString) obj.members().get("name")).value();
int age = ((JsonNumber) obj.members().get("age")).intValue();
boolean active = ((JsonBoolean) obj.members().get("active")).value();
String name = ((JsonString) obj.members().get("name")).string();
int age = Math.toIntExact(((JsonNumber) obj.members().get("age")).toLong());
boolean active = ((JsonBoolean) obj.members().get("active")).bool();
```

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

// Map to record
User user = new User(
((JsonString) jsonObj.members().get("name")).value(),
((JsonNumber) jsonObj.members().get("age")).intValue(),
((JsonBoolean) jsonObj.members().get("active")).value()
((JsonString) jsonObj.members().get("name")).string(),
Math.toIntExact(((JsonNumber) jsonObj.members().get("age")).toLong()),
((JsonBoolean) jsonObj.members().get("active")).bool()
);

// Convert records back to JSON
JsonValue backToJson = Json.fromUntyped(Map.of(
"name", user.name(),
"age", user.age(),
"active", user.active()
JsonValue backToJson = JsonObject.of(Map.of(
"name", JsonString.of(user.name()),
"age", JsonNumber.of(user.age()),
"active", JsonBoolean.of(user.active())
));

// Covert back to a JSON string
String jsonString = backToJson.toString();
```

### Converting from Java Objects to JSON (`fromUntyped`)

```java
// Convert standard Java collections to JsonValue
Map<String, Object> data = Map.of(
"name", "John",
"age", 30,
"scores", List.of(85, 92, 78)
);
JsonValue json = Json.fromUntyped(data);
```

### Converting from JSON to Java Objects (`toUntyped`)

```java
// Convert JsonValue back to standard Java types
JsonValue parsed = Json.parse("{\"name\":\"John\",\"age\":30}");
Object data = Json.toUntyped(parsed);
// Returns a Map<String, Object> with standard Java types
```

The conversion mappings are:
- `JsonObject` ↔ `Map<String, Object>`
- `JsonArray` ↔ `List<Object>`
- `JsonString` ↔ `String`
- `JsonNumber` ↔ `Number` (Long, Double, BigInteger, or BigDecimal)
- `JsonBoolean` ↔ `Boolean`
- `JsonNull` ↔ `null`

This is useful for:
- Integrating with existing code that uses standard collections
- Serializing/deserializing to formats that expect Java types
- Working with frameworks that use reflection on standard types

### Realistic Record Mapping

A powerful feature is mapping between Java records and JSON:
Expand All @@ -124,28 +90,28 @@ Team team = new Team("Engineering", List.of(
));

// Convert records to JSON
JsonValue teamJson = Json.fromUntyped(Map.of(
"teamName", team.teamName(),
"members", team.members().stream()
.map(u -> Map.of(
"name", u.name(),
"email", u.email(),
"active", u.active()
))
.toList()
JsonValue teamJson = JsonObject.of(Map.of(
"teamName", JsonString.of(team.teamName()),
"members", JsonArray.of(team.members().stream()
.map(u -> JsonObject.of(Map.of(
"name", JsonString.of(u.name()),
"email", JsonString.of(u.email()),
"active", JsonBoolean.of(u.active())
)))
.toList())
));

// Parse JSON back to records
JsonObject parsed = (JsonObject) Json.parse(teamJson.toString());
Team reconstructed = new Team(
((JsonString) parsed.members().get("teamName")).value(),
((JsonArray) parsed.members().get("members")).values().stream()
((JsonString) parsed.members().get("teamName")).string(),
((JsonArray) parsed.members().get("members")).elements().stream()
.map(v -> {
JsonObject member = (JsonObject) v;
return new User(
((JsonString) member.members().get("name")).value(),
((JsonString) member.members().get("email")).value(),
((JsonBoolean) member.members().get("active")).value()
((JsonString) member.members().get("name")).string(),
((JsonString) member.members().get("email")).string(),
((JsonBoolean) member.members().get("active")).bool()
);
})
.toList()
Expand Down Expand Up @@ -182,10 +148,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.values().stream()
List<String> activeUserEmails = users.elements().stream()
.map(v -> (JsonObject) v)
.filter(obj -> ((JsonBoolean) obj.members().get("active")).value())
.map(obj -> ((JsonString) obj.members().get("email")).value())
.filter(obj -> ((JsonBoolean) obj.members().get("active")).bool())
.map(obj -> ((JsonString) obj.members().get("email")).string())
.toList();
```

Expand All @@ -198,9 +164,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 Down Expand Up @@ -263,15 +229,15 @@ mvn exec:java -pl json-compatibility-suite -Dexec.args="--json"

## Current Status

This code (as of 2025-09-04) is derived from the OpenJDK jdk-sandbox repository json branch at commit [a8e7de8b49e4e4178eb53c94ead2fa2846c30635](https://github.com/openjdk/jdk-sandbox/commit/a8e7de8b49e4e4178eb53c94ead2fa2846c30635) ("Produce path/col during path building", 2025-08-14 UTC).
This code (as of 2026-01-25) is derived from the OpenJDK jdk-sandbox repository "json" branch at commit [4de0eb4f0c867df2d420501cf6741e50dee142d9](https://github.com/openjdk/jdk-sandbox/commit/4de0eb4f0c867df2d420501cf6741e50dee142d9) ("initial integration into the JDK repository", 2024-10-10 UTC).

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.
The JSON compatibility tests in this repo suggest 99.3% conformance with a leading test suite. The two conformance expectations that fail assume that duplicate keys in a JSON document are okay. The upstream code takes a strict stance that it should not silently ignore duplicate keys in a JSON object.

### CI: Upstream API Tracking

A daily workflow runs an API comparison against the OpenJDK sandbox and prints a JSON report. Implication: differences do not currently fail the build or auto‑open issues; check the workflow logs (or adjust the workflow to fail on diffs) if you need notifications.
A daily workflow runs an API comparison against the OpenJDK sandbox and prints a JSON report. API drift is automatically detected and issues are created when differences are found, with fingerprint deduplication to avoid duplicate issues for the same drift.

## Modifications

Expand All @@ -282,12 +248,12 @@ This is a simplified backport with the following changes from the original:

## Security Considerations

**⚠️ This unstable API historically contained a undocumented security vulnerabilities.** The compatibility test suite (documented below) includes crafted attack vectors that expose these issues:
**⚠️ This unstable API contains known security considerations.** The parser uses recursion internally which means:

- **Stack exhaustion attacks**: Deeply nested JSON structures can trigger `StackOverflowError`, potentially leaving applications in an undefined state and enabling denial-of-service attacks
- **API contract violations**: The `Json.parse()` method documentation only declares `JsonParseException` and `NullPointerException`, but malicious inputs can trigger undeclared exceptions

Such vulnerabilities existed at one point in the upstream OpenJDK sandbox implementation and were reported here for transparency. Until the upstream code is stable it is probably better to assume that such issue or similar may be present or may reappear. If you are only going to use this library in small cli programs where the json is configuration you write then you will not parse objects nested to tens of thousands of levels designed crash a parser. Yet you should not at this tiome expose this parser to the internet where someone can choose to attack it in that manner.
The upstream OpenJDK sandbox implementation uses a recursive descent parser. Until the upstream code is stable it is probably better to assume that such issues or similar may be present or may reappear. If you are only going to use this library in small CLI programs where the JSON is configuration you write, then you will not parse objects nested to tens of thousands of levels designed to crash a parser. However, you should not at this time expose this parser to the internet where someone can choose to attack it in that manner.

## JSON Type Definition (JTD) Validator

Expand Down
22 changes: 11 additions & 11 deletions index.html
Original file line number Diff line number Diff line change
Expand Up @@ -95,8 +95,8 @@ <h3>Simple Example</h3>
JsonObject obj = (JsonObject) value;

// Access values
String name = ((JsonString) obj.members().get("name")).value();
int age = ((JsonNumber) obj.members().get("age")).toNumber().intValue();</code></pre>
String name = ((JsonString) obj.members().get("name")).string();
int age = Math.toIntExact(((JsonNumber) obj.members().get("age")).toLong());</code></pre>

<h2>Key Features</h2>
<ul>
Expand All @@ -118,15 +118,15 @@ <h2>Record Mapping Example</h2>
new User("Bob", "bob@example.com", false)
));

JsonValue teamJson = Json.fromUntyped(Map.of(
"teamName", team.teamName(),
"members", team.members().stream()
.map(u -> Map.of(
"name", u.name(),
"email", u.email(),
"active", u.active()
))
.toList()
JsonValue teamJson = JsonObject.of(Map.of(
"teamName", JsonString.of(team.teamName()),
"members", JsonArray.of(team.members().stream()
.map(u -> JsonObject.of(Map.of(
"name", JsonString.of(u.name()),
"email", JsonString.of(u.email()),
"active", JsonBoolean.of(u.active())
)))
.toList())
));</code></pre>

<h2>Resources</h2>
Expand Down
Loading
Loading