diff --git a/.gitignore b/.gitignore index 530eade..03d35ef 100644 --- a/.gitignore +++ b/.gitignore @@ -7,6 +7,11 @@ target/ .idea/ +# Eclipse +.classpath +.project +.settings/ + .claude/ .aider* CLAUDE.md diff --git a/README.md b/README.md index 4f86841..2bc6f70 100644 --- a/README.md +++ b/README.md @@ -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 @@ -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 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 with standard Java types -``` - -The conversion mappings are: -- `JsonObject` ↔ `Map` -- `JsonArray` ↔ `List` -- `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: @@ -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() @@ -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 activeUserEmails = users.values().stream() +List 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(); ``` @@ -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()); } ``` @@ -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 @@ -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 diff --git a/index.html b/index.html index 32855c1..5bb042c 100644 --- a/index.html +++ b/index.html @@ -95,8 +95,8 @@

Simple Example

JsonObject obj = (JsonObject) value; // Access values -String name = ((JsonString) obj.members().get("name")).value(); -int age = ((JsonNumber) obj.members().get("age")).toNumber().intValue(); +String name = ((JsonString) obj.members().get("name")).string(); +int age = Math.toIntExact(((JsonNumber) obj.members().get("age")).toLong());

Key Features

    @@ -118,15 +118,15 @@

    Record Mapping Example

    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()) ));

    Resources

    diff --git a/json-java21-api-tracker/src/main/java/io/github/simbo1905/tracker/ApiTracker.java b/json-java21-api-tracker/src/main/java/io/github/simbo1905/tracker/ApiTracker.java index 589363f..46efd33 100644 --- a/json-java21-api-tracker/src/main/java/io/github/simbo1905/tracker/ApiTracker.java +++ b/json-java21-api-tracker/src/main/java/io/github/simbo1905/tracker/ApiTracker.java @@ -599,7 +599,7 @@ static JsonObject compareApis(JsonObject local, JsonObject upstream) { // Extract class name safely final var localClassName = local.members().get("className"); final var className = localClassName instanceof JsonString js ? - js.value() : "Unknown"; + js.string() : "Unknown"; diffMap.put("className", JsonString.of(className)); @@ -612,7 +612,7 @@ static JsonObject compareApis(JsonObject local, JsonObject upstream) { // Check if status is NOT_IMPLEMENTED (from parsing) if (upstream.members().containsKey("status")) { - final var status = ((JsonString) upstream.members().get("status")).value(); + final var status = ((JsonString) upstream.members().get("status")).string(); if ("NOT_IMPLEMENTED".equals(status)) { diffMap.put("status", JsonString.of("PARSE_NOT_IMPLEMENTED")); return JsonObject.of(diffMap); @@ -681,11 +681,11 @@ static boolean compareModifiers(JsonObject local, JsonObject upstream, List ((JsonString) v).value()) + final var localSet = localMods.elements().stream() + .map(v -> ((JsonString) v).string()) .collect(Collectors.toSet()); - final var upstreamSet = upstreamMods.values().stream() - .map(v -> ((JsonString) v).value()) + final var upstreamSet = upstreamMods.elements().stream() + .map(v -> ((JsonString) v).string()) .collect(Collectors.toSet()); if (!localSet.equals(upstreamSet)) { @@ -708,11 +708,11 @@ static boolean compareInheritance(JsonObject local, JsonObject upstream, List normalizeTypeName(((JsonString) v).value())) + final var localTypes = localExtends.elements().stream() + .map(v -> normalizeTypeName(((JsonString) v).string())) .collect(Collectors.toSet()); - final var upstreamTypes = upstreamExtends.values().stream() - .map(v -> normalizeTypeName(((JsonString) v).value())) + final var upstreamTypes = upstreamExtends.elements().stream() + .map(v -> normalizeTypeName(((JsonString) v).string())) .collect(Collectors.toSet()); if (!localTypes.equals(upstreamTypes)) { @@ -786,8 +786,8 @@ static boolean compareMethods(JsonObject local, JsonObject upstream, List matchingCount++; case "UPSTREAM_ERROR" -> missingUpstream++; @@ -964,21 +964,21 @@ static String generateFingerprint(JsonObject report) { final var differences = (JsonArray) report.members().get("differences"); final var stableLines = new ArrayList(); - for (final var diff : differences.values()) { + for (final var diff : differences.elements()) { final var diffObj = (JsonObject) diff; - final var status = ((JsonString) diffObj.members().get("status")).value(); + final var status = ((JsonString) diffObj.members().get("status")).string(); if (!"DIFFERENT".equals(status)) continue; - final var className = ((JsonString) diffObj.members().get("className")).value(); + final var className = ((JsonString) diffObj.members().get("className")).string(); final var classDiffs = (JsonArray) diffObj.members().get("differences"); if (classDiffs != null) { - for (final var change : classDiffs.values()) { + for (final var change : classDiffs.elements()) { final var changeObj = (JsonObject) change; - final var type = ((JsonString) changeObj.members().get("type")).value(); + final var type = ((JsonString) changeObj.members().get("type")).string(); final var methodValue = changeObj.members().get("method"); - final var method = methodValue instanceof JsonString js ? js.value() : ""; + final var method = methodValue instanceof JsonString js ? js.string() : ""; // Create stable line: "ClassName:changeType:methodName" stableLines.add(className + ":" + type + ":" + method); } @@ -1013,7 +1013,7 @@ private static long getDifferentApiCount(JsonObject report) { } final var differentApiValue = summary.members().get("differentApi"); if (differentApiValue instanceof JsonNumber num) { - return num.toNumber().longValue(); + return num.toLong(); } return 0; } @@ -1027,10 +1027,10 @@ static String generateSummary(JsonObject report) { final var summary = (JsonObject) report.members().get("summary"); final var differences = (JsonArray) report.members().get("differences"); - final var totalClasses = ((JsonNumber) summary.members().get("totalClasses")).toNumber().longValue(); - final var matchingClasses = ((JsonNumber) summary.members().get("matchingClasses")).toNumber().longValue(); + final var totalClasses = ((JsonNumber) summary.members().get("totalClasses")).toLong(); + final var matchingClasses = ((JsonNumber) summary.members().get("matchingClasses")).toLong(); final var differentApi = getDifferentApiCount(report); - final var missingUpstream = ((JsonNumber) summary.members().get("missingUpstream")).toNumber().longValue(); + final var missingUpstream = ((JsonNumber) summary.members().get("missingUpstream")).toLong(); sb.append("## API Comparison Summary\n\n"); sb.append("| Metric | Count |\n"); @@ -1043,22 +1043,22 @@ static String generateSummary(JsonObject report) { if (differentApi > 0) { sb.append("## Changes Detected\n\n"); - for (final var diff : differences.values()) { + for (final var diff : differences.elements()) { final var diffObj = (JsonObject) diff; - final var status = ((JsonString) diffObj.members().get("status")).value(); + final var status = ((JsonString) diffObj.members().get("status")).string(); if (!"DIFFERENT".equals(status)) continue; - final var className = ((JsonString) diffObj.members().get("className")).value(); + final var className = ((JsonString) diffObj.members().get("className")).string(); sb.append("### ").append(className).append("\n\n"); final var classDiffs = (JsonArray) diffObj.members().get("differences"); if (classDiffs != null) { - for (final var change : classDiffs.values()) { + for (final var change : classDiffs.elements()) { final var changeObj = (JsonObject) change; - final var type = ((JsonString) changeObj.members().get("type")).value(); + final var type = ((JsonString) changeObj.members().get("type")).string(); final var methodValue = changeObj.members().get("method"); - final var method = methodValue instanceof JsonString js ? js.value() : "unknown"; + final var method = methodValue instanceof JsonString js ? js.string() : "unknown"; final var emoji = switch (type) { case "methodRemoved" -> "➖"; @@ -1078,7 +1078,7 @@ static String generateSummary(JsonObject report) { } sb.append("---\n"); - final var timestamp = ((JsonString) report.members().get("timestamp")).value(); + final var timestamp = ((JsonString) report.members().get("timestamp")).string(); sb.append("*Generated by API Tracker on ").append(timestamp.split("T")[0]).append("*\n"); return sb.toString(); @@ -1090,4 +1090,4 @@ static String generateSummary(JsonObject report) { static boolean hasDifferences(JsonObject report) { return getDifferentApiCount(report) > 0; } -} \ No newline at end of file +} diff --git a/json-java21-api-tracker/src/test/java/io/github/simbo1905/tracker/ApiTrackerTest.java b/json-java21-api-tracker/src/test/java/io/github/simbo1905/tracker/ApiTrackerTest.java index c971395..3a7da94 100644 --- a/json-java21-api-tracker/src/test/java/io/github/simbo1905/tracker/ApiTrackerTest.java +++ b/json-java21-api-tracker/src/test/java/io/github/simbo1905/tracker/ApiTrackerTest.java @@ -71,15 +71,15 @@ void testExtractLocalApiJsonObject() { // Check if extraction succeeded or failed if (api.members().containsKey("error")) { // If file not found, that's expected for some source setups - final var error = ((JsonString) api.members().get("error")).value(); + final var error = ((JsonString) api.members().get("error")).string(); assertThat(error).contains("LOCAL_FILE_NOT_FOUND"); } else { // If extraction succeeded, validate structure assertThat(api.members()).containsKey("className"); - assertThat(((JsonString) api.members().get("className")).value()).isEqualTo("JsonObject"); + assertThat(((JsonString) api.members().get("className")).string()).isEqualTo("JsonObject"); assertThat(api.members()).containsKey("packageName"); - assertThat(((JsonString) api.members().get("packageName")).value()).isEqualTo("jdk.sandbox.java.util.json"); + assertThat(((JsonString) api.members().get("packageName")).string()).isEqualTo("jdk.sandbox.java.util.json"); assertThat(api.members()).containsKey("isInterface"); assertThat(api.members().get("isInterface")).isEqualTo(JsonBoolean.of(true)); @@ -94,7 +94,7 @@ void testExtractLocalApiJsonValue() { // Check if extraction succeeded or failed if (api.members().containsKey("error")) { // If file not found, that's expected for some source setups - final var error = ((JsonString) api.members().get("error")).value(); + final var error = ((JsonString) api.members().get("error")).string(); assertThat(error).contains("LOCAL_FILE_NOT_FOUND"); } else { // If extraction succeeded, validate structure @@ -114,7 +114,7 @@ void testExtractLocalApiMissingFile() { final var api = ApiTracker.extractLocalApiFromSource("jdk.sandbox.java.util.json.NonExistentClass"); assertThat(api.members()).containsKey("error"); - final var error = ((JsonString) api.members().get("error")).value(); + final var error = ((JsonString) api.members().get("error")).string(); assertThat(error).contains("LOCAL_FILE_NOT_FOUND"); } } @@ -179,7 +179,7 @@ void testCompareApisUpstreamError() { final var result = ApiTracker.compareApis(local, upstream); assertThat(result.members()).containsKey("status"); - assertThat(((JsonString) result.members().get("status")).value()).isEqualTo("UPSTREAM_ERROR"); + assertThat(((JsonString) result.members().get("status")).string()).isEqualTo("UPSTREAM_ERROR"); assertThat(result.members()).containsKey("error"); } } diff --git a/json-java21-jtd/src/main/java/json/java21/jtd/Jtd.java b/json-java21-jtd/src/main/java/json/java21/jtd/Jtd.java index 9470efd..4c0faf1 100644 --- a/json-java21-jtd/src/main/java/json/java21/jtd/Jtd.java +++ b/json-java21-jtd/src/main/java/json/java21/jtd/Jtd.java @@ -175,7 +175,7 @@ void pushChildFrames(Frame frame, java.util.Deque stack) { case JtdSchema.ElementsSchema elementsSchema -> { if (instance instanceof JsonArray arr) { int index = 0; - for (JsonValue element : arr.values()) { + for (JsonValue element : arr.elements()) { String childPtr = frame.ptr() + "/" + index; Crumbs childCrumbs = frame.crumbs().withArrayIndex(index); Frame childFrame = new Frame(elementsSchema.elements(), element, childPtr, childCrumbs); @@ -249,7 +249,7 @@ void pushChildFrames(Frame frame, java.util.Deque stack) { if (instance instanceof JsonObject obj) { JsonValue discriminatorValue = obj.members().get(discSchema.discriminator()); if (discriminatorValue instanceof JsonString discStr) { - String discriminatorValueStr = discStr.value(); + String discriminatorValueStr = discStr.string(); JtdSchema variantSchema = discSchema.mapping().get(discriminatorValueStr); if (variantSchema != null) { @@ -418,7 +418,7 @@ JtdSchema compileObjectSchema(JsonObject obj, boolean isRoot) { nullableValue.getClass().getSimpleName() + " in schema: " + Json.toDisplayString(obj, 0)); } // If nullable is valid, this becomes a nullable empty schema - if (bool.value()) { + if (bool.bool()) { return new JtdSchema.NullableSchema(new JtdSchema.EmptySchema()); } } @@ -461,7 +461,7 @@ JtdSchema compileObjectSchema(JsonObject obj, boolean isRoot) { throw new IllegalArgumentException("nullable must be a boolean, found: " + nullableValue.getClass().getSimpleName() + " in schema: " + Json.toDisplayString(obj, 0)); } - if (bool.value()) { + if (bool.bool()) { return new JtdSchema.NullableSchema(schema); } } @@ -474,7 +474,7 @@ JtdSchema compileRefSchema(JsonObject obj) { if (!(refValue instanceof JsonString str)) { throw new IllegalArgumentException("ref must be a string"); } - String ref = str.value(); + String ref = str.string(); // RFC 8927: Validate that ref points to an existing definition at compile time if (!definitions.containsKey(ref)) { @@ -501,7 +501,7 @@ JtdSchema compileTypeSchema(JsonObject obj) { throw new IllegalArgumentException("type must be a string"); } - String typeStr = str.value(); + String typeStr = str.string(); // RFC 8927 §2.2.3: Validate that type is one of the supported primitive types if (!VALID_TYPES.contains(typeStr)) { @@ -520,11 +520,11 @@ JtdSchema compileEnumSchema(JsonObject obj) { } List values = new ArrayList<>(); - for (JsonValue value : arr.values()) { + for (JsonValue value : arr.elements()) { if (!(value instanceof JsonString str)) { throw new IllegalArgumentException("enum values must be strings"); } - values.add(str.value()); + values.add(str.string()); } if (values.isEmpty()) { @@ -596,7 +596,7 @@ JtdSchema compilePropertiesSchema(JsonObject obj, boolean isRoot) { if (!(addPropsValue instanceof JsonBoolean bool)) { throw new IllegalArgumentException("additionalProperties must be a boolean"); } - additionalProperties = bool.value(); + additionalProperties = bool.bool(); } // Empty schema with no properties defined rejects additional properties by default return new JtdSchema.PropertiesSchema(properties, optionalProperties, additionalProperties); @@ -623,7 +623,7 @@ JtdSchema compileDiscriminatorSchema(JsonObject obj, boolean isRoot) { if (!(discriminatorValue instanceof JsonString discStr)) { throw new IllegalArgumentException("discriminator must be a string"); } - String discriminatorKey = discStr.value(); + String discriminatorKey = discStr.string(); JsonValue mappingValue = members.get("mapping"); if (!(mappingValue instanceof JsonObject mappingObj)) { @@ -644,7 +644,7 @@ JtdSchema compileDiscriminatorSchema(JsonObject obj, boolean isRoot) { // Check for nullable flag before compiling if (variantObj.members().containsKey("nullable") && variantObj.members().get("nullable") instanceof JsonBoolean bool && - bool.value()) { + bool.bool()) { throw new IllegalArgumentException("Discriminator mapping '" + key + "' cannot be nullable"); } diff --git a/json-java21-jtd/src/main/java/json/java21/jtd/JtdSchema.java b/json-java21-jtd/src/main/java/json/java21/jtd/JtdSchema.java index d3773b7..c13bb51 100644 --- a/json-java21-jtd/src/main/java/json/java21/jtd/JtdSchema.java +++ b/json-java21-jtd/src/main/java/json/java21/jtd/JtdSchema.java @@ -148,7 +148,7 @@ boolean validateStringWithFrame(Frame frame, java.util.List errors, bool boolean validateTimestampWithFrame(Frame frame, java.util.List errors, boolean verboseErrors) { JsonValue instance = frame.instance(); if (instance instanceof JsonString str) { - String value = str.value(); + String value = str.string(); if (RFC3339.matcher(value).matches()) { try { // Replace :60 with :59 to allow leap seconds through parsing @@ -165,44 +165,19 @@ boolean validateTimestampWithFrame(Frame frame, java.util.List errors, b return false; } - private boolean hasFractionalComponent(Number value) { - return switch (value) { - case null -> false; - case Double d -> d != Math.floor(d); - case Float f -> f != Math.floor(f); - case java.math.BigDecimal bd -> bd.remainder(java.math.BigDecimal.ONE).signum() != 0; - default -> - // Long, Integer, Short, Byte are always integers - false; - }; - } - boolean validateIntegerWithFrame(Frame frame, String type, java.util.List errors, boolean verboseErrors) { JsonValue instance = frame.instance(); if (instance instanceof JsonNumber num) { - Number value = num.toNumber(); - - // Check for fractional component first (applies to all Number types) - if (hasFractionalComponent(value)) { + final long longValue; + try { + longValue = num.toLong(); + } catch (JsonAssertionException ex) { String error = Jtd.Error.EXPECTED_INTEGER.message(); errors.add(Jtd.enrichedError(error, frame, instance)); return false; } - - // Handle precision loss for large Double values - if (value instanceof Double d) { - if (d > Long.MAX_VALUE || d < Long.MIN_VALUE) { - String error = verboseErrors - ? Jtd.Error.EXPECTED_NUMERIC_TYPE.message(instance, type, "out of range") - : Jtd.Error.EXPECTED_NUMERIC_TYPE.message(type, "out of range"); - errors.add(Jtd.enrichedError(error, frame, instance)); - return false; - } - } - + // Now check if the value is within range for the specific integer type - // Convert to long for range checking (works for all Number types) - long longValue = value.longValue(); boolean inRange = switch (type) { case "int8" -> longValue >= -128 && longValue <= 127; case "uint8" -> longValue >= 0 && longValue <= 255; @@ -250,12 +225,12 @@ record EnumSchema(List values) implements JtdSchema { public boolean validateWithFrame(Frame frame, java.util.List errors, boolean verboseErrors) { JsonValue instance = frame.instance(); if (instance instanceof JsonString str) { - if (values.contains(str.value())) { + if (values.contains(str.string())) { return true; } String error = verboseErrors - ? Jtd.Error.VALUE_NOT_IN_ENUM.message(instance, str.value(), values) - : Jtd.Error.VALUE_NOT_IN_ENUM.message(str.value(), values); + ? Jtd.Error.VALUE_NOT_IN_ENUM.message(instance, str.string(), values) + : Jtd.Error.VALUE_NOT_IN_ENUM.message(str.string(), values); errors.add(Jtd.enrichedError(error, frame, instance)); return false; } @@ -281,7 +256,7 @@ public Jtd.Result validate(JsonValue instance) { @Override public Jtd.Result validate(JsonValue instance, boolean verboseErrors) { if (instance instanceof JsonArray arr) { - for (JsonValue element : arr.values()) { + for (JsonValue element : arr.elements()) { Jtd.Result result = elements.validate(element, verboseErrors); if (!result.isValid()) { return result; @@ -483,7 +458,7 @@ public Jtd.Result validate(JsonValue instance, boolean verboseErrors) { return Jtd.Result.failure(error); } - String discriminatorValueStr = discStr.value(); + String discriminatorValueStr = discStr.string(); JtdSchema variantSchema = mapping.get(discriminatorValueStr); if (variantSchema == null) { String error = verboseErrors @@ -526,7 +501,7 @@ public boolean validateWithFrame(Frame frame, java.util.List errors, boo return false; } - String discriminatorValueStr = discStr.value(); + String discriminatorValueStr = discStr.string(); JtdSchema variantSchema = mapping.get(discriminatorValueStr); if (variantSchema == null) { String error = verboseErrors diff --git a/json-java21-jtd/src/test/java/json/java21/jtd/JtdPropertyTest.java b/json-java21-jtd/src/test/java/json/java21/jtd/JtdPropertyTest.java index ea3ea39..e6ee61f 100644 --- a/json-java21-jtd/src/test/java/json/java21/jtd/JtdPropertyTest.java +++ b/json-java21-jtd/src/test/java/json/java21/jtd/JtdPropertyTest.java @@ -98,7 +98,7 @@ private static JsonValue buildCompliantTypeValue(String type) { case "uint16" -> JsonNumber.of(50000); case "int32" -> JsonNumber.of(1000000); case "uint32" -> JsonNumber.of(3000000000L); - case "float32", "float64" -> JsonNumber.of(new BigDecimal("3.14159")); + case "float32", "float64" -> JsonNumber.of("3.14159"); default -> JsonString.of("unknown-type-value"); }; } @@ -110,10 +110,10 @@ private static List createFailingJtdDocuments(JtdTestSchema schema, J case TypeSchema(var type) -> createFailingTypeValues(type); case EnumSchema(var ignored) -> List.of(JsonString.of("invalid-enum-value")); case ElementsSchema(var elementSchema) -> { - if (compliant instanceof JsonArray arr && !arr.values().isEmpty()) { - final var invalidElement = createFailingJtdDocuments(elementSchema, arr.values().getFirst()); + if (compliant instanceof JsonArray arr && !arr.elements().isEmpty()) { + final var invalidElement = createFailingJtdDocuments(elementSchema, arr.elements().getFirst()); if (!invalidElement.isEmpty()) { - final var mixedArray = JsonArray.of(List.of(arr.values().getFirst(), invalidElement.getFirst())); + final var mixedArray = JsonArray.of(List.of(arr.elements().getFirst(), invalidElement.getFirst())); yield List.of(mixedArray, JsonNull.of()); } } @@ -153,7 +153,7 @@ private static List createFailingTypeValues(String type) { case "boolean" -> List.of(JsonString.of("not-boolean"), JsonNumber.of(1)); case "string", "timestamp" -> List.of(JsonNumber.of(123), JsonBoolean.of(false)); case "int8", "uint8", "int16", "int32", "uint32", "uint16" -> - List.of(JsonString.of("not-integer"), JsonNumber.of(new BigDecimal("3.14"))); + List.of(JsonString.of("not-integer"), JsonNumber.of("3.14")); case "float32", "float64" -> List.of(JsonString.of("not-float"), JsonBoolean.of(true)); default -> List.of(JsonNull.of()); }; diff --git a/json-java21-jtd/src/test/java/json/java21/jtd/TestRfc8927.java b/json-java21-jtd/src/test/java/json/java21/jtd/TestRfc8927.java index 10e7491..ed37f07 100644 --- a/json-java21-jtd/src/test/java/json/java21/jtd/TestRfc8927.java +++ b/json-java21-jtd/src/test/java/json/java21/jtd/TestRfc8927.java @@ -447,7 +447,7 @@ public void testRefSchemaRecursiveBad() { @Test public void testInt32RejectsDecimal() { JsonValue schema = Json.parse("{\"type\": \"int32\"}"); - JsonValue decimalValue = JsonNumber.of(new java.math.BigDecimal("3.14")); + JsonValue decimalValue = JsonNumber.of("3.14"); LOG.info(() -> "Testing int32 validation against decimal value 3.14"); LOG.fine(() -> "Schema: " + schema); @@ -479,10 +479,10 @@ public void testIntegerTypesAcceptTrailingZeros() { // Valid integer representations with trailing zeros JsonValue[] validIntegers = { - JsonNumber.of(new java.math.BigDecimal("3.0")), - JsonNumber.of(new java.math.BigDecimal("3.000")), - JsonNumber.of(new java.math.BigDecimal("42.00")), - JsonNumber.of(new java.math.BigDecimal("0.0")) + JsonNumber.of("3.0"), + JsonNumber.of("3.000"), + JsonNumber.of("42.00"), + JsonNumber.of("0.0") }; Jtd validator = new Jtd(); @@ -509,10 +509,10 @@ public void testIntegerTypesRejectFractionalComponents() { // Invalid values with actual fractional components JsonValue[] invalidValues = { - JsonNumber.of(new java.math.BigDecimal("3.1")), - JsonNumber.of(new java.math.BigDecimal("3.0001")), - JsonNumber.of(new java.math.BigDecimal("3.14")), - JsonNumber.of(new java.math.BigDecimal("0.1")) + JsonNumber.of("3.1"), + JsonNumber.of("3.0001"), + JsonNumber.of("3.14"), + JsonNumber.of("0.1") }; Jtd validator = new Jtd(); @@ -878,9 +878,8 @@ public void testDiscriminatorInOptionalProperties() { .isTrue(); } - /// Test for the critical integer range validation bug - /// This test specifically targets the issue where Double values bypass range checks - /// JsonNumber.toNumber() commonly returns Double, which falls through validation + /// Test for integer range validation + /// This test ensures out-of-range numeric values are rejected @Test public void testInt8RangeValidationWithDoubleValues() { JsonValue schema = Json.parse("{\"type\": \"int8\"}"); @@ -899,9 +898,7 @@ public void testInt8RangeValidationWithDoubleValues() { for (JsonValue outOfRange : outOfRangeValues) { Jtd.Result result = validator.validate(schema, outOfRange); - LOG.fine(() -> "Testing int8 range with Double value: " + outOfRange + - " (JsonNumber.toNumber() type: " + - ((JsonNumber)outOfRange).toNumber().getClass().getSimpleName() + ")"); + LOG.fine(() -> "Testing int8 range with numeric value: " + outOfRange); // This should fail but currently passes due to the bug assertThat(result.isValid()) @@ -1035,30 +1032,7 @@ public void testUint32RangeValidationWithDoubleValues() { } } - /// Test that demonstrates the specific problem: JsonNumber.toNumber() returns Double - /// This test shows the root cause of the bug - @Test - public void testJsonNumberToNumberReturnsDouble() { - JsonValue numberValue = Json.parse("1000"); - - // Verify that JsonNumber.toNumber() returns Double for typical JSON numbers - assertThat(numberValue).isInstanceOf(JsonNumber.class); - Number number = ((JsonNumber) numberValue).toNumber(); - - LOG.info(() -> "JsonNumber.toNumber() returns: " + number.getClass().getSimpleName() + - " for value: " + numberValue); - - // This demonstrates what type JsonNumber.toNumber() returns for typical values - // The actual type depends on the JSON parser implementation - LOG.info(() -> "JsonNumber.toNumber() returns: " + number.getClass().getSimpleName() + - " for value: " + numberValue); - - // The key test is that regardless of the Number type, range validation should work - // Our fix ensures all Number types go through proper range validation - } - /// Test integer validation with explicit Double creation - /// Shows the bug occurs even when we know the type is Double @Test public void testIntegerValidationExplicitDouble() { JsonValue schema = Json.parse("{\"type\": \"int8\"}"); @@ -1069,8 +1043,8 @@ public void testIntegerValidationExplicitDouble() { Jtd validator = new Jtd(); Jtd.Result result = validator.validate(schema, doubleValue); - LOG.fine(() -> "Explicit Double validation - value: " + doubleValue + - ", toNumber() type: " + doubleValue.toNumber().getClass().getSimpleName()); + LOG.fine(() -> "Explicit double validation - value: " + doubleValue + + ", toDouble() value: " + doubleValue.toDouble()); // This should fail (1000 is way outside int8 range of -128 to 127) assertThat(result.isValid()) @@ -1107,7 +1081,7 @@ public void testIntegerBoundaryValues() { @Test public void testIntegerValidationWithBigDecimalValues() { JsonValue schema = Json.parse("{\"type\": \"uint32\"}"); - JsonValue bigValue = JsonNumber.of(new java.math.BigDecimal("5000000000")); // > uint32 max + JsonValue bigValue = JsonNumber.of("5000000000"); // > uint32 max Jtd validator = new Jtd(); Jtd.Result result = validator.validate(schema, bigValue); diff --git a/json-java21/src/main/java/jdk/sandbox/internal/util/json/JsonArrayImpl.java b/json-java21/src/main/java/jdk/sandbox/internal/util/json/JsonArrayImpl.java index 66d4f92..43b8614 100644 --- a/json-java21/src/main/java/jdk/sandbox/internal/util/json/JsonArrayImpl.java +++ b/json-java21/src/main/java/jdk/sandbox/internal/util/json/JsonArrayImpl.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2025, Oracle and/or its affiliates. All rights reserved. + * Copyright (c) 2025, 2026, Oracle and/or its affiliates. All rights reserved. * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. * * This code is free software; you can redistribute it and/or modify it @@ -27,8 +27,10 @@ import java.util.Collections; import java.util.List; + import jdk.sandbox.java.util.json.JsonArray; import jdk.sandbox.java.util.json.JsonValue; + /** * JsonArray implementation class */ @@ -49,7 +51,7 @@ public JsonArrayImpl(List from, int o, char[] d) { } @Override - public List values() { + public List elements() { return Collections.unmodifiableList(theValues); } @@ -66,10 +68,10 @@ public int offset() { @Override public String toString() { var s = new StringBuilder("["); - for (JsonValue v: values()) { + for (JsonValue v: elements()) { s.append(v.toString()).append(","); } - if (!values().isEmpty()) { + if (!elements().isEmpty()) { s.setLength(s.length() - 1); // trim final comma } return s.append("]").toString(); @@ -78,11 +80,11 @@ public String toString() { @Override public boolean equals(Object o) { return o instanceof JsonArray oja && - values().equals(oja.values()); + elements().equals(oja.elements()); } @Override public int hashCode() { - return values().hashCode(); + return elements().hashCode(); } } diff --git a/json-java21/src/main/java/jdk/sandbox/internal/util/json/JsonBooleanImpl.java b/json-java21/src/main/java/jdk/sandbox/internal/util/json/JsonBooleanImpl.java index 476375c..cb9d82c 100644 --- a/json-java21/src/main/java/jdk/sandbox/internal/util/json/JsonBooleanImpl.java +++ b/json-java21/src/main/java/jdk/sandbox/internal/util/json/JsonBooleanImpl.java @@ -26,6 +26,7 @@ package jdk.sandbox.internal.util.json; import jdk.sandbox.java.util.json.JsonBoolean; + /** * JsonBoolean implementation class */ @@ -45,7 +46,7 @@ public JsonBooleanImpl(Boolean bool, char[] doc, int offset) { } @Override - public boolean value() { + public boolean bool() { return theBoolean; } @@ -61,16 +62,16 @@ public int offset() { @Override public String toString() { - return String.valueOf(value()); + return String.valueOf(bool()); } @Override public boolean equals(Object o) { - return o instanceof JsonBoolean ojb && value() == ojb.value(); + return o instanceof JsonBoolean ojb && bool() == ojb.bool(); } @Override public int hashCode() { - return Boolean.hashCode(value()); + return Boolean.hashCode(bool()); } } diff --git a/json-java21/src/main/java/jdk/sandbox/internal/util/json/JsonNullImpl.java b/json-java21/src/main/java/jdk/sandbox/internal/util/json/JsonNullImpl.java index f8852af..f3f308c 100644 --- a/json-java21/src/main/java/jdk/sandbox/internal/util/json/JsonNullImpl.java +++ b/json-java21/src/main/java/jdk/sandbox/internal/util/json/JsonNullImpl.java @@ -26,6 +26,7 @@ package jdk.sandbox.internal.util.json; import jdk.sandbox.java.util.json.JsonNull; + /** * JsonNull implementation class */ diff --git a/json-java21/src/main/java/jdk/sandbox/internal/util/json/JsonNumberImpl.java b/json-java21/src/main/java/jdk/sandbox/internal/util/json/JsonNumberImpl.java index 719cb27..d4e1b32 100644 --- a/json-java21/src/main/java/jdk/sandbox/internal/util/json/JsonNumberImpl.java +++ b/json-java21/src/main/java/jdk/sandbox/internal/util/json/JsonNumberImpl.java @@ -25,10 +25,12 @@ package jdk.sandbox.internal.util.json; -import java.math.BigDecimal; -import java.math.BigInteger; import java.util.Locale; + +import java.util.Optional; import jdk.sandbox.java.util.json.JsonNumber; + + /** * JsonNumber implementation class */ @@ -37,64 +39,31 @@ public final class JsonNumberImpl implements JsonNumber, JsonValueImpl { private final char[] doc; private final int startOffset; private final int endOffset; - private final boolean isFp; - private final StableValue theNumber = StableValue.of(); - private final StableValue numString = StableValue.of(); - private final StableValue cachedBD = StableValue.of(); + private final int decimalOffset; + private final int exponentOffset; - public JsonNumberImpl(Number num) { - // Called by factories. Input is Double, Long, BI, or BD. - if (num == null || - num instanceof Double d && (d.isNaN() || d.isInfinite())) { - throw new IllegalArgumentException("Not a valid JSON number"); - } - theNumber.setOrThrow(num); - numString.setOrThrow(num.toString()); - // unused - startOffset = -1; - endOffset = -1; - isFp = false; - doc = null; - } + private final StableValue numString = StableValue.of(); + private final StableValue> numLong = StableValue.of(); + private final StableValue> numDouble = StableValue.of(); - public JsonNumberImpl(char[] doc, int start, int end, boolean fp) { + public JsonNumberImpl(char[] doc, int start, int end, int dec, int exp) { this.doc = doc; startOffset = start; endOffset = end; - isFp = fp; + decimalOffset = dec; + exponentOffset = exp; } @Override - public Number toNumber() { - return theNumber.orElseSet(() -> { - var str = toString(); - if (isFp) { - var db = Double.parseDouble(str); - if (Double.isInfinite(db)) { - return toBigDecimal(); - } else { - return db; - } - } else { - try { - return Long.parseLong(str); - } catch(NumberFormatException e) { - return new BigInteger(str); - } - } - }); + public long toLong() { + return numLong.orElseSet(this::initNumLong).orElseThrow(() -> + Utils.composeError(this, this + " cannot be represented as a long.")); } @Override - public BigDecimal toBigDecimal() { - return cachedBD.orElseSet(() -> { - // If we already computed theNumber, check if it's BD - if (theNumber.orElse(null) instanceof BigDecimal bd) { - return bd; - } else { - return new BigDecimal(toString()); - } - }); + public double toDouble() { + return numDouble.orElseSet(this::initNumDouble).orElseThrow(() -> + Utils.composeError(this, this + " cannot be represented as a double.")); } @Override @@ -109,8 +78,7 @@ public int offset() { @Override public String toString() { - return numString.orElseSet( - () -> new String(doc, startOffset, endOffset - startOffset)); + return numString.orElseSet(this::initNumString); } @Override @@ -123,4 +91,79 @@ public boolean equals(Object o) { public int hashCode() { return toString().toLowerCase(Locale.ROOT).hashCode(); } + + // LazyConstants initializers + private String initNumString() { + return new String(doc, startOffset, endOffset - startOffset); + } + + private static long powExact10(int exp) { + long result = 1L; + for (int i = 0; i < exp; i++) { + result = Math.multiplyExact(result, 10L); + } + return result; + } + + // 4 cases: Fully integral, has decimal, has exponent, has decimal and exponent + private Optional initNumLong() { + try { + if (decimalOffset == -1 && exponentOffset == -1) { + // Parseable Long format + return Optional.of(Long.parseLong(numString.orElseSet(this::initNumString))); + } else { + // Decimal or exponent exists, can't parse w/ Long::parseLong + if (exponentOffset != -1) { + // Exponent exists + // Calculate exponent value + int exp = Math.abs(Integer.parseInt(new String(doc, + exponentOffset + 1, endOffset - exponentOffset - 1), 10)); + long sig; + long scale; + if (decimalOffset == -1) { + // Exponent with no decimal + sig = Long.parseLong(new String(doc, startOffset, exponentOffset - startOffset)); + } else { + // Exponent with decimal + for (int i = decimalOffset + exp + 1; i < exponentOffset; i++) { + if (doc[i] != '0') { + return Optional.empty(); + } + } + var shiftedFractionPart = new String(doc, decimalOffset + 1, Math.min(exp, exponentOffset - decimalOffset - 1)); + exp = exp - shiftedFractionPart.length(); + sig = Long.parseLong(new String(doc, startOffset, decimalOffset - startOffset) + shiftedFractionPart); + } + scale = powExact10(exp); + if (doc[exponentOffset + 1] != '-') { + return Optional.of(Math.multiplyExact(sig, scale)); + } else { + if (sig % scale == 0) { + return Optional.of(Math.divideExact(sig, scale)); + } else { + return Optional.empty(); + } + } + } else { + // Decimal with no exponent + for (int i = decimalOffset + 1; i < endOffset; i++) { + if (doc[i] != '0') { + return Optional.empty(); + } + } + return Optional.of(Long.parseLong(new String(doc, + startOffset, decimalOffset - startOffset), 10)); + } + } + } catch (NumberFormatException | ArithmeticException ex) {} + return Optional.empty(); + } + + private Optional initNumDouble() { + var db = Double.parseDouble(numString.orElseSet(this::initNumString)); + if (Double.isFinite(db)) { + return Optional.of(db); + } + return Optional.empty(); + } } diff --git a/json-java21/src/main/java/jdk/sandbox/internal/util/json/JsonObjectImpl.java b/json-java21/src/main/java/jdk/sandbox/internal/util/json/JsonObjectImpl.java index 5a232b6..8d6e8de 100644 --- a/json-java21/src/main/java/jdk/sandbox/internal/util/json/JsonObjectImpl.java +++ b/json-java21/src/main/java/jdk/sandbox/internal/util/json/JsonObjectImpl.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2025, Oracle and/or its affiliates. All rights reserved. + * Copyright (c) 2025, 2026, Oracle and/or its affiliates. All rights reserved. * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. * * This code is free software; you can redistribute it and/or modify it @@ -27,8 +27,10 @@ import java.util.Collections; import java.util.Map; + import jdk.sandbox.java.util.json.JsonObject; import jdk.sandbox.java.util.json.JsonValue; + /** * JsonObject implementation class */ diff --git a/json-java21/src/main/java/jdk/sandbox/internal/util/json/JsonParser.java b/json-java21/src/main/java/jdk/sandbox/internal/util/json/JsonParser.java index fb9a95c..f2a326a 100644 --- a/json-java21/src/main/java/jdk/sandbox/internal/util/json/JsonParser.java +++ b/json-java21/src/main/java/jdk/sandbox/internal/util/json/JsonParser.java @@ -30,6 +30,7 @@ import java.util.List; import java.util.Map; import java.util.function.Supplier; + import jdk.sandbox.java.util.json.JsonArray; import jdk.sandbox.java.util.json.JsonObject; import jdk.sandbox.java.util.json.JsonParseException; @@ -40,15 +41,14 @@ * Parses a JSON Document char[] into a tree of JsonValues. JsonObject and JsonArray * nodes create their data structures which maintain the connection to children. * JsonNumber and JsonString contain only a start and end offset, which - * are used to lazily procure their underlying value/string on demand. Singletons - * are used for JsonBoolean and JsonNull. + * are used to lazily procure their underlying value/string on demand. */ public final class JsonParser { // Access to the underlying JSON contents private final char[] doc; // Lazily initialized for member names with escape sequences - private final Supplier sb = StableValue.supplier(this::initSb); + private final StableValue sb = StableValue.of(); // Current offset during parsing private int offset; // For exception message on failure @@ -114,10 +114,7 @@ private JsonObject parseObject() { // Why not parse the name as a JsonString and then return its value()? // Would requires 2 passes; we should build the String as we parse. var name = parseName(); - - if (members.containsKey(name)) { - throw failure("The duplicate member name: \"%s\" was already parsed".formatted(name)); - } + var nameOffset = offset; // Move from name to ':' skipWhitespaces(); @@ -125,7 +122,11 @@ private JsonObject parseObject() { throw failure( "Expected a colon after the member name"); } - members.put(name, parseValue()); + + if (members.putIfAbsent(name, parseValue()) != null) { + throw failure(nameOffset, "The duplicate member name: \"%s\" was already parsed".formatted(name)); + } + // Ensure current char is either ',' or '}' if (charEquals('}')) { return new JsonObjectImpl(members, startO, doc); @@ -171,7 +172,7 @@ private String parseName() { } if (!useBldr) { // Append everything up to the first escape sequence - sb.get().append(doc, start, offset - escapeLength - 1 - start); + sb.orElseSet(this::initSb).append(doc, start, offset - escapeLength - 1 - start); useBldr = true; } escape = false; @@ -181,8 +182,8 @@ private String parseName() { } else if (c == '\"') { offset++; if (useBldr) { - var name = sb.toString(); - sb.get().setLength(0); + var name = sb.orElseSet(this::initSb).toString(); + sb.orElseSet(this::initSb).setLength(0); return name; } else { return new String(doc, start, offset - start - 1); @@ -191,7 +192,7 @@ private String parseName() { throw failure(UNESCAPED_CONTROL_CODE); } if (useBldr) { - sb.get().append(c); + sb.orElseSet(this::initSb).append(c); } } throw failure(UNCLOSED_STRING.formatted("JSON Object member name")); @@ -257,31 +258,27 @@ private JsonString parseString() { throw failure(UNCLOSED_STRING.formatted("JSON String")); } - /* - * Parsing true, false, and null return singletons. These JsonValues - * do not require offsets to lazily compute their values. - */ private JsonBooleanImpl parseTrue() { - offset++; + var start = offset++; if (charEquals('r') && charEquals('u') && charEquals('e')) { - return new JsonBooleanImpl(true, doc, offset); + return new JsonBooleanImpl(true, doc, start); } throw failure(UNEXPECTED_VAL); } private JsonBooleanImpl parseFalse() { - offset++; + var start = offset++; if (charEquals('a') && charEquals('l') && charEquals('s') && charEquals('e')) { - return new JsonBooleanImpl(false, doc, offset); + return new JsonBooleanImpl(false, doc, start); } throw failure(UNEXPECTED_VAL); } private JsonNullImpl parseNull() { - offset++; + var start = offset++; if (charEquals('u') && charEquals('l') && charEquals('l')) { - return new JsonNullImpl(doc, offset); + return new JsonNullImpl(doc, start); } throw failure(UNEXPECTED_VAL); } @@ -293,8 +290,8 @@ private JsonNullImpl parseNull() { * See https://datatracker.ietf.org/doc/html/rfc8259#section-6 */ private JsonNumberImpl parseNumber() { - boolean sawDecimal = false; - boolean sawExponent = false; + int decOff = -1; + int expOff = -1; boolean sawZero = false; boolean havePart = false; boolean sawSign = false; @@ -305,19 +302,19 @@ private JsonNumberImpl parseNumber() { var c = doc[offset]; switch (c) { case '-' -> { - if (offset != start && !sawExponent || sawSign) { + if (offset != start && expOff == -1 || sawSign) { throw failure(INVALID_POSITION_IN_NUMBER.formatted(c)); } sawSign = true; } case '+' -> { - if (!sawExponent || havePart || sawSign) { + if (expOff == -1 || havePart || sawSign) { throw failure(INVALID_POSITION_IN_NUMBER.formatted(c)); } sawSign = true; } case '0', '1', '2', '3', '4', '5', '6', '7', '8', '9' -> { - if (!sawDecimal && !sawExponent && sawZero) { + if (decOff == -1 && expOff == -1 && sawZero) { throw failure(INVALID_POSITION_IN_NUMBER.formatted('0')); } if (doc[offset] == '0' && !havePart) { @@ -326,24 +323,24 @@ private JsonNumberImpl parseNumber() { havePart = true; } case '.' -> { - if (sawDecimal) { + if (decOff != -1) { throw failure(INVALID_POSITION_IN_NUMBER.formatted(c)); } else { if (!havePart) { throw failure(INVALID_POSITION_IN_NUMBER.formatted(c)); } - sawDecimal = true; + decOff = offset; havePart = false; } } case 'e', 'E' -> { - if (sawExponent) { + if (expOff != -1) { throw failure(INVALID_POSITION_IN_NUMBER.formatted(c)); } else { if (!havePart) { throw failure(INVALID_POSITION_IN_NUMBER.formatted(c)); } - sawExponent = true; + expOff = offset; havePart = false; sawSign = false; } @@ -357,7 +354,7 @@ private JsonNumberImpl parseNumber() { if (!havePart) { throw failure("Input expected after '[.|e|E]'"); } - return new JsonNumberImpl(doc, start, offset, sawDecimal || sawExponent); + return new JsonNumberImpl(doc, start, offset, decOff, expOff); } // Utility functions @@ -408,7 +405,7 @@ private boolean notWhitespace() { return switch (doc[offset]) { case ' ', '\t','\r' -> false; case '\n' -> { - // Increments the row and col + // Increments the line and lineStart line++; lineStart = offset + 1; yield false; @@ -427,15 +424,15 @@ private boolean charEquals(char c) { return false; } - // Return the col position reflective of the current row - private int col() { - return offset - lineStart; + private JsonParseException failure(String message) { + return failure(offset, message); } - private JsonParseException failure(String message) { + private JsonParseException failure(int off, String message) { // Non-revealing message does not produce input source String - return new JsonParseException("%s. Location: row %d, col %d." - .formatted(message, line, col()), line, col()); + var pos = off - lineStart; + return new JsonParseException("%s. Location: line %d, position %d." + .formatted(message, line, pos), line, pos); } // Parsing error messages ---------------------- diff --git a/json-java21/src/main/java/jdk/sandbox/internal/util/json/JsonStringImpl.java b/json-java21/src/main/java/jdk/sandbox/internal/util/json/JsonStringImpl.java index f73b899..060919e 100644 --- a/json-java21/src/main/java/jdk/sandbox/internal/util/json/JsonStringImpl.java +++ b/json-java21/src/main/java/jdk/sandbox/internal/util/json/JsonStringImpl.java @@ -26,6 +26,7 @@ package jdk.sandbox.internal.util.json; import jdk.sandbox.java.util.json.JsonString; + /** * JsonString implementation class */ @@ -42,20 +43,12 @@ public final class JsonStringImpl implements JsonString, JsonValueImpl { // non-conformant characters are properly escaped. private final StableValue jsonStr = StableValue.of(); - // The String instance returned by `value()`. - // If created by parsing a JSON document, escaped characters are unescaped. - // If created via the factory method, the input String is used as-is. + // The String instance returned by `string()`. Escaped characters are unescaped. private final StableValue value = StableValue.of(); - // Called by JsonString.of() factory. The passed String represents the - // unescaped value. - public JsonStringImpl(String str) { - jsonStr.setOrThrow('"' + Utils.escape(value.orElseSet(() -> str)) + '"'); - // unused - doc = null; - startOffset = -1; - endOffset = -1; - hasEscape = false; + // LazyConstants initializers + private String initJsonStr() { + return new String(doc, startOffset, endOffset - startOffset); } public JsonStringImpl(char[] doc, int start, int end, boolean escape) { @@ -66,7 +59,7 @@ public JsonStringImpl(char[] doc, int start, int end, boolean escape) { } @Override - public String value() { + public String string() { return value.orElseSet(this::unescape); } @@ -82,8 +75,7 @@ public int offset() { @Override public String toString() { - return jsonStr.orElseSet( - () -> new String(doc, startOffset, endOffset - startOffset)); + return jsonStr.orElseSet(this::initJsonStr); } /* @@ -130,11 +122,11 @@ private String unescape() { @Override public boolean equals(Object o) { return o instanceof JsonString ojs && - value().equals(ojs.value()); + string().equals(ojs.string()); } @Override public int hashCode() { - return value().hashCode(); + return string().hashCode(); } } diff --git a/json-java21/src/main/java/jdk/sandbox/internal/util/json/Utils.java b/json-java21/src/main/java/jdk/sandbox/internal/util/json/Utils.java index 0157803..135273f 100644 --- a/json-java21/src/main/java/jdk/sandbox/internal/util/json/Utils.java +++ b/json-java21/src/main/java/jdk/sandbox/internal/util/json/Utils.java @@ -25,8 +25,6 @@ package jdk.sandbox.internal.util.json; -import java.util.List; -import java.util.Map; import jdk.sandbox.java.util.json.JsonArray; import jdk.sandbox.java.util.json.JsonAssertionException; import jdk.sandbox.java.util.json.JsonBoolean; @@ -44,16 +42,6 @@ public class Utils { // Non instantiable private Utils() {} - // Equivalent to JsonObject/Array.of() factories without the need for defensive copy - // and other input validation - public static JsonArray arrayOf(List list) { - return new JsonArrayImpl(list); - } - - public static JsonObject objectOf(Map map) { - return new JsonObjectImpl(map); - } - /* * Escapes a String to ensure it is a valid JSON String. * Backslash, double quote, and control chars are escaped. @@ -99,31 +87,33 @@ public static String escape(String str) { return sb == null ? str : sb.toString(); } + public static JsonAssertionException composeError(JsonValue jv, String message) { + return new JsonAssertionException(message + + (jv instanceof JsonValueImpl jvi && jvi.doc() != null ? JsonPath.getPath(jvi) : "")); + } + // Use to compose an exception when casting to an incorrect type public static JsonAssertionException composeTypeError(JsonValue jv, String expected) { var actual = switch (jv) { - case JsonObject obj -> "JsonObject"; - case JsonArray arr -> "JsonArray"; - case JsonBoolean b -> "JsonBoolean"; - case JsonNull n -> "JsonNull"; - case JsonNumber num -> "JsonNumber"; - case JsonString str -> "JsonString"; + case JsonObject ignored -> "JsonObject"; + case JsonArray ignored -> "JsonArray"; + case JsonBoolean ignored -> "JsonBoolean"; + case JsonNull ignored -> "JsonNull"; + case JsonNumber ignored -> "JsonNumber"; + case JsonString ignored -> "JsonString"; }; - return new JsonAssertionException("%s is not a %s.".formatted(actual, expected) - + ((jv instanceof JsonValueImpl jvi && jvi.doc() != null) ? JsonPath.getPath(jvi) : "")); - } - - static String getPath(JsonValueImpl jvi) { - return JsonPath.getPath(jvi); + return composeError(jv, "%s is not a %s.".formatted(actual, expected)); } + // This class is responsible for creating the path produced by JAE. + // Backtracks from the offset of the offending JSON element to the root. private static final class JsonPath { private final int offset; private final char[] doc; // Tracked and incremented during path creation - private int row; - private int col; + private int line; + private int pos; private JsonPath(JsonValueImpl jvi) { this.offset = jvi.offset(); @@ -138,17 +128,17 @@ private String parseToRoot() { var sb = new StringBuilder(); // Updates the sb toPath(offset, sb); - // If no new line encountered, col is the starting offset value - if (row == 0) { - col = offset; + // If no new line encountered, pos is the starting offset value + if (line == 0) { + pos = offset; } - return " Path: \"%s\". Location: row %d, col %d.".formatted(sb.toString(), row, col); + return " Path: \"%s\". Location: line %d, position %d.".formatted(sb.toString(), line, pos); } - private void addRow(int curr) { - row++; - if (row == 1) { - col = offset - curr - 1; + private void addLine(int curr) { + line++; + if (line == 1) { + pos = offset - curr - 1; } } @@ -175,7 +165,7 @@ private int walkWhitespace(int offset) { var ws = switch (doc[offset]) { case ' ', '\t','\r' -> true; case '\n' -> { - addRow(offset); + addLine(offset); yield true; } default -> false; @@ -215,7 +205,7 @@ private int arrayNode(int offset, StringBuilder sb) { } else if (c == '"') { inString = true; } else if (c == '\n') { - addRow(offset); + addLine(offset); } if (aDepth > 0) { break; @@ -271,7 +261,7 @@ private int objectNode(int offset, StringBuilder sb) { } else if (c == '"') { inString = true; } else if (c == '\n') { - addRow(offset); + addLine(offset); } if (depth > 0) { break; diff --git a/json-java21/src/main/java/jdk/sandbox/java/util/json/Json.java b/json-java21/src/main/java/jdk/sandbox/java/util/json/Json.java index 0594914..7fb58e0 100644 --- a/json-java21/src/main/java/jdk/sandbox/java/util/json/Json.java +++ b/json-java21/src/main/java/jdk/sandbox/java/util/json/Json.java @@ -24,18 +24,10 @@ */ package jdk.sandbox.java.util.json; -import java.math.BigDecimal; -import java.math.BigInteger; -import java.util.ArrayList; import java.util.Arrays; -import java.util.List; -import java.util.Map; import java.util.Objects; -import java.util.HashMap; -import java.util.LinkedHashMap; import jdk.sandbox.internal.util.json.JsonParser; -import jdk.sandbox.internal.util.json.Utils; /// This class provides static methods for producing and manipulating a {@link JsonValue}. /// @@ -45,21 +37,16 @@ /// {@link #toDisplayString(JsonValue, int)} is a formatter that produces a /// representation of the JSON value suitable for display. /// -/// {@link #fromUntyped(Object)} and {@link #toUntyped(JsonValue)} provide a conversion -/// between `JsonValue` and an untyped object. -/// /// ## Example Usage /// ```java /// // Parse JSON string /// JsonValue json = Json.parse("{\"name\":\"John\",\"age\":30}"); -/// -/// // Convert to standard Java types -/// Map data = (Map) Json.toUntyped(json); -/// -/// // Create JSON from Java objects -/// JsonValue fromJava = Json.fromUntyped(Map.of("active", true, "score", 95)); +/// if (json instanceof JsonObject obj) { +/// String name = ((JsonString) obj.members().get("name")).string(); +/// long age = ((JsonNumber) obj.members().get("age")).toLong(); +/// } /// ``` -/// +/// /// @spec https://datatracker.ietf.org/doc/html/rfc8259 RFC 8259: The JavaScript /// Object Notation (JSON) Data Interchange Format /// @since 99 @@ -80,8 +67,8 @@ public final class Json { /// ```java /// JsonValue value = Json.parse("{\"name\":\"Alice\",\"active\":true}"); /// if (value instanceof JsonObject obj) { - /// String name = ((JsonString) obj.members().get("name")).value(); - /// boolean active = ((JsonBoolean) obj.members().get("active")).value(); + /// String name = ((JsonString) obj.members().get("name")).string(); + /// boolean active = ((JsonBoolean) obj.members().get("active")).bool(); /// } /// ``` /// @@ -119,126 +106,6 @@ public static JsonValue parse(char[] in) { return new JsonParser(Arrays.copyOf(in, in.length)).parseRoot(); } - /// {@return a `JsonValue` created from the given `src` object} - /// The mapping from an untyped `src` object to a `JsonValue` - /// follows the table below. - /// - /// | Untyped Object | JsonValue | - /// |----------------|----------| - /// | `List` | `JsonArray` | - /// | `Boolean` | `JsonBoolean` | - /// | `null` | `JsonNull` | - /// | `Number*` | `JsonNumber` | - /// | `Map` | `JsonObject` | - /// | `String` | `JsonString` | - /// - /// *The supported `Number` subclasses are: `Byte`, - /// `Short`, `Integer`, `Long`, `Float`, - /// `Double`, `BigInteger`, and `BigDecimal`. - /// - /// If `src` is an instance of `JsonValue`, it is returned as is. - /// - /// ## Example - /// ```java - /// // Convert Java collections to JSON - /// JsonValue json = Json.fromUntyped(Map.of( - /// "user", Map.of( - /// "name", "Bob", - /// "age", 25 - /// ), - /// "scores", List.of(85, 90, 78) - /// )); - /// ``` - /// - /// @param src the data to produce the `JsonValue` from. May be null. - /// @throws IllegalArgumentException if `src` cannot be converted - /// to a `JsonValue`. - /// @see #toUntyped(JsonValue) - public static JsonValue fromUntyped(Object src) { - return switch (src) { - // Structural: JSON object - case Map map -> { - Map m = LinkedHashMap.newLinkedHashMap(map.size()); - for (Map.Entry entry : map.entrySet()) { - if (!(entry.getKey() instanceof String key)) { - throw new IllegalArgumentException( - "The key '%s' is not a String".formatted(entry.getKey())); - } else { - m.put(key, Json.fromUntyped(entry.getValue())); - } - } - // Bypasses defensive copy in JsonObject.of(m) - yield Utils.objectOf(m); - } - // Structural: JSON Array - case List list -> { - List l = new ArrayList<>(list.size()); - for (Object o : list) { - l.add(Json.fromUntyped(o)); - } - // Bypasses defensive copy in JsonArray.of(l) - yield Utils.arrayOf(l); - } - // JSON primitives - case String str -> JsonString.of(str); - case Boolean bool -> JsonBoolean.of(bool); - case Byte b -> JsonNumber.of(b); - case Integer i -> JsonNumber.of(i); - case Long l -> JsonNumber.of(l); - case Short s -> JsonNumber.of(s); - case Float f -> JsonNumber.of(f); - case Double d -> JsonNumber.of(d); - case BigInteger bi -> JsonNumber.of(bi); - case BigDecimal bd -> JsonNumber.of(bd); - case null -> JsonNull.of(); - // JsonValue - case JsonValue jv -> jv; - default -> throw new IllegalArgumentException(src.getClass().getSimpleName() + " is not a recognized type"); - }; - } - - /// {@return an `Object` created from the given `src` `JsonValue`} - /// The mapping from a `JsonValue` to an untyped `src` object follows the table below. - /// - /// | JsonValue | Untyped Object | - /// |-----------|----------------| - /// | `JsonArray` | `List` (unmodifiable) | - /// | `JsonBoolean` | `Boolean` | - /// | `JsonNull` | `null` | - /// | `JsonNumber` | `Number` | - /// | `JsonObject` | `Map` (unmodifiable) | - /// | `JsonString` | `String` | - /// - /// A `JsonObject` in `src` is converted to a `Map` whose - /// entries occur in the same order as the `JsonObject`'s members. - /// - /// ## Example - /// ```java - /// JsonValue json = Json.parse("{\"active\":true,\"count\":42}"); - /// Map data = (Map) Json.toUntyped(json); - /// // data contains: {"active"=true, "count"=42L} - /// ``` - /// - /// @param src the `JsonValue` to convert to untyped. Non-null. - /// @throws NullPointerException if `src` is `null` - /// @see #fromUntyped(Object) - public static Object toUntyped(JsonValue src) { - Objects.requireNonNull(src); - return switch (src) { - case JsonObject jo -> jo.members().entrySet().stream() - .collect(LinkedHashMap::new, // Avoid Collectors.toMap, to allow `null` value - (m, e) -> m.put(e.getKey(), Json.toUntyped(e.getValue())), - HashMap::putAll); - case JsonArray ja -> ja.values().stream() - .map(Json::toUntyped) - .toList(); - case JsonBoolean jb -> jb.value(); - case JsonNull ignored -> null; - case JsonNumber n -> n.toNumber(); - case JsonString js -> js.value(); - }; - } - /// {@return the String representation of the given `JsonValue` that conforms /// to the JSON syntax} As opposed to the compact output returned by {@link /// JsonValue#toString()}, this method returns a JSON string that is better @@ -309,11 +176,11 @@ private static String toDisplayString(JsonObject jo, int col, int indent, boolea private static String toDisplayString(JsonArray ja, int col, int indent, boolean isField) { var prefix = " ".repeat(col); var s = new StringBuilder(isField ? " " : prefix); - if (ja.values().isEmpty()) { + if (ja.elements().isEmpty()) { s.append("[]"); } else { s.append("[\n"); - for (JsonValue v: ja.values()) { + for (JsonValue v: ja.elements()) { if (v instanceof JsonValue jv) { s.append(Json.toDisplayString(jv, col + indent, indent, false)).append(",\n"); } else { diff --git a/json-java21/src/main/java/jdk/sandbox/java/util/json/JsonArray.java b/json-java21/src/main/java/jdk/sandbox/java/util/json/JsonArray.java index 7ed3d71..276deb9 100644 --- a/json-java21/src/main/java/jdk/sandbox/java/util/json/JsonArray.java +++ b/json-java21/src/main/java/jdk/sandbox/java/util/json/JsonArray.java @@ -47,11 +47,11 @@ /// )); /// /// // Access elements -/// for (JsonValue value : arr.values()) { +/// for (JsonValue value : arr.elements()) { /// switch (value) { -/// case JsonString s -> System.out.println("String: " + s.value()); -/// case JsonNumber n -> System.out.println("Number: " + n.toNumber()); -/// case JsonBoolean b -> System.out.println("Boolean: " + b.value()); +/// case JsonString s -> System.out.println("String: " + s.string()); +/// case JsonNumber n -> System.out.println("Number: " + n.toLong()); +/// case JsonBoolean b -> System.out.println("Boolean: " + b.bool()); /// default -> System.out.println("Other: " + value); /// } /// } @@ -62,7 +62,7 @@ public non-sealed interface JsonArray extends JsonValue { /// {@return an unmodifiable list of the `JsonValue` elements in /// this `JsonArray`} - List values(); + List elements(); /// {@return the `JsonArray` created from the given /// list of `JsonValue`s} @@ -83,20 +83,20 @@ static JsonArray of(List src) { /// {@return `true` if the given object is also a `JsonArray` /// and the two `JsonArray`s represent the same elements} Two /// `JsonArray`s `ja1` and `ja2` represent the same - /// elements if `ja1.values().equals(ja2.values())`. + /// elements if `ja1.elements().equals(ja2.elements())`. /// - /// @see #values() + /// @see #elements() @Override boolean equals(Object obj); /// {@return the hash code value for this `JsonArray`} The hash code value /// of a `JsonArray` is derived from the hash code of `JsonArray`'s - /// {@link #values()}. + /// {@link #elements()}. /// Thus, for two `JsonArray`s `ja1` and `ja2`, /// `ja1.equals(ja2)` implies that `ja1.hashCode() == ja2.hashCode()` /// as required by the general contract of {@link Object#hashCode}. /// - /// @see #values() + /// @see #elements() @Override int hashCode(); } diff --git a/json-java21/src/main/java/jdk/sandbox/java/util/json/JsonBoolean.java b/json-java21/src/main/java/jdk/sandbox/java/util/json/JsonBoolean.java index 98515be..c684c3a 100644 --- a/json-java21/src/main/java/jdk/sandbox/java/util/json/JsonBoolean.java +++ b/json-java21/src/main/java/jdk/sandbox/java/util/json/JsonBoolean.java @@ -38,7 +38,7 @@ public non-sealed interface JsonBoolean extends JsonValue { /// {@return the {@code boolean} value represented by this /// {@code JsonBoolean}} - boolean value(); + boolean bool(); /// {@return the {@code JsonBoolean} created from the given /// {@code boolean}} @@ -51,19 +51,19 @@ static JsonBoolean of(boolean src) { /// {@return {@code true} if the given object is also a {@code JsonBoolean} /// and the two {@code JsonBoolean}s represent the same boolean value} Two /// {@code JsonBoolean}s {@code jb1} and {@code jb2} represent the same - /// boolean values if {@code jb1.value().equals(jb2.value())}. + /// boolean values if {@code jb1.bool() == jb2.bool()}. /// - /// @see #value() + /// @see #bool() @Override boolean equals(Object obj); /// {@return the hash code value for this {@code JsonBoolean}} The hash code value /// of a {@code JsonBoolean} is derived from the hash code of {@code JsonBoolean}'s - /// {@link #value()}. Thus, for two {@code JsonBooleans}s {@code jb1} and {@code jb2}, + /// {@link #bool()}. Thus, for two {@code JsonBooleans}s {@code jb1} and {@code jb2}, /// {@code jb1.equals(jb2)} implies that {@code jb1.hashCode() == jb2.hashCode()} /// as required by the general contract of {@link Object#hashCode}. /// - /// @see #value() + /// @see #bool() @Override int hashCode(); } diff --git a/json-java21/src/main/java/jdk/sandbox/java/util/json/JsonNumber.java b/json-java21/src/main/java/jdk/sandbox/java/util/json/JsonNumber.java index 9e5fe8d..75da423 100644 --- a/json-java21/src/main/java/jdk/sandbox/java/util/json/JsonNumber.java +++ b/json-java21/src/main/java/jdk/sandbox/java/util/json/JsonNumber.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2025, Oracle and/or its affiliates. All rights reserved. + * Copyright (c) 2025, 2026, Oracle and/or its affiliates. All rights reserved. * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. * * This code is free software; you can redistribute it and/or modify it @@ -25,156 +25,108 @@ package jdk.sandbox.java.util.json; -import java.math.BigDecimal; -import java.math.BigInteger; - import jdk.sandbox.internal.util.json.JsonNumberImpl; /// The interface that represents JSON number, an arbitrary-precision /// number represented in base 10 using decimal digits. /// -/// A {@code JsonNumber} can be produced by {@link Json#parse(String)}. -/// Alternatively, {@link #of(double)} and its overloads can be used to obtain -/// a {@code JsonNumber} from a {@code Number}. -/// When a JSON number is parsed, a {@code JsonNumber} object is created -/// as long as the parsed value adheres to the JSON number -/// [syntax](https://datatracker.ietf.org/doc/html/rfc8259#section-6). -/// The value of the {@code JsonNumber} -/// can be retrieved from {@link #toString()} as the string representation -/// from which the JSON number is originally parsed, with -/// {@link #toNumber()} as a {@code Number} instance, or with -/// {@link #toBigDecimal()}. +/// A `JsonNumber` can be produced by {@link Json#parse(String)}. +/// Alternatively, {@link #of(double)}, {@link #of(long)}, or {@link #of(String)} +/// can be used to obtain a `JsonNumber`. +/// The value of the `JsonNumber` can be retrieved as a `long` +/// with {@link #toLong()} or as a `double` with {@link #toDouble()}. +/// `toString()` returns the string representation of the JSON number. +/// +/// @apiNote +/// To avoid precision loss when converting JSON numbers to Java types, or when +/// converting JSON numbers outside the range of `long` or `double`, use +/// `toString()` to create arbitrary-precision Java objects. /// /// @spec https://datatracker.ietf.org/doc/html/rfc8259#section-6 RFC 8259: /// The JavaScript Object Notation (JSON) Data Interchange Format - Numbers /// @since 99 public non-sealed interface JsonNumber extends JsonValue { - /// {@return the {@code Number} parsed or translated from the - /// {@link #toString string representation} of this {@code JsonNumber}} - /// - /// This method operates on the string representation and depending on that - /// representation computes and returns an instance of {@code Long}, {@code BigInteger}, - /// {@code Double}, or {@code BigDecimal}. - /// - /// If the string representation is the decimal string representation of - /// a {@code long} value, parsable by {@link Long#parseLong(String)}, - /// then that {@code long} value is returned in its boxed form as {@code Long}. - /// Otherwise, if the string representation is the decimal string representation of a - /// {@code BigInteger}, translatable by {@link BigInteger#BigInteger(String)}, - /// then that {@code BigInteger} is returned. - /// Otherwise, if the string representation is the decimal string representation of - /// a {@code double} value, parsable by {@link Double#parseDouble(String)}, - /// and the {@code double} value is not {@link Double#isInfinite() infinite}, then that - /// {@code double} value is returned in its boxed form as {@code Double}. - /// Otherwise, and in all other cases, the string representation is the decimal string - /// representation of a {@code BigDecimal}, translatable by - /// {@link BigDecimal#BigDecimal(String)}, and that {@code BigDecimal} is - /// returned. + /// {@return a `long` if it can be translated from the string + /// representation of this `JsonNumber`} The value must be a whole number + /// and within the range of {@link Long#MIN_VALUE} and {@link Long#MAX_VALUE}. /// - /// The computation may not preserve all information in the string representation. - /// In all of the above cases one or more leading zero digits are not preserved. - /// In the third case, returning {@code Double}, decimal to binary conversion may lose - /// decimal precision, and will not preserve one or more trailing zero digits in the fraction - /// part. - /// - /// @apiNote - /// Pattern matching can be used to match against {@code Long}, - /// {@code Double}, {@code BigInteger}, or {@code BigDecimal} reference - /// types. For example: - /// {@snippet lang=java: - /// switch(jsonNumber.toNumber()) { - /// case Long l -> { ... } - /// case Double d -> { ... } - /// case BigInteger bi -> { ... } - /// case BigDecimal bd -> { ... } - /// default -> { } // should not happen - /// } - ///} - /// @throws NumberFormatException if the {@code Number} cannot be parsed or translated from the string representation - /// @see #toBigDecimal() - /// @see #toString() - Number toNumber(); + /// @throws JsonAssertionException if this `JsonNumber` cannot + /// be represented as a `long`. + @Override + long toLong(); - /// {@return the {@code BigDecimal} translated from the - /// {@link #toString string representation} of this {@code JsonNumber}} + /// {@return a finite `double` if it can be translated from the string + /// representation of this `JsonNumber`} /// - /// The string representation is the decimal string representation of a - /// {@code BigDecimal}, translatable by {@link BigDecimal#BigDecimal(String)}, - /// and that {@code BigDecimal} is returned. - /// - /// The translation may not preserve all information in the string representation. - /// The sign is not preserved for the decimal string representation {@code -0.0}. One or more - /// leading zero digits are not preserved. - /// - /// @throws NumberFormatException if the {@code BigDecimal} cannot be translated from the string representation - BigDecimal toBigDecimal(); + /// @throws JsonAssertionException if this `JsonNumber` cannot + /// be represented as a finite `double`. + @Override + double toDouble(); - /// Creates a JSON number whose string representation is the - /// decimal string representation of the given {@code double} value, - /// produced by applying the value to {@link Double#toString(double)}. + /// Creates a JSON number from the given `double` value. + /// The string representation of the JSON number created is produced by + /// applying {@link Double#toString(double)} on `num`. /// - /// @param num the given {@code double} value. - /// @return a JSON number created from a {@code double} value - /// @throws IllegalArgumentException if the given {@code double} value - /// is {@link Double#isNaN() NaN} or is {@link Double#isInfinite() infinite}. + /// @param num the given `double` value. + /// @return a JSON number created from the `double` value + /// @throws IllegalArgumentException if the given `double` value + /// is not a finite floating-point value. static JsonNumber of(double num) { - // non-integral types - return new JsonNumberImpl(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); } - /// Creates a JSON number whose string representation is the - /// decimal string representation of the given {@code long} value, - /// produced by applying the value to {@link Long#toString(long)}. + /// Creates a JSON number from the given `long` value. + /// The string representation of the JSON number created is produced by + /// applying {@link Long#toString(long)} on `num`. /// - /// @param num the given {@code long} value. - /// @return a JSON number created from a {@code long} value + /// @param num the given `long` value. + /// @return a JSON number created from the `long` value static JsonNumber of(long num) { - // integral types - return new JsonNumberImpl(num); - } - - /// Creates a JSON number whose string representation is the - /// string representation of the given {@code BigInteger} value. - /// - /// @param num the given {@code BigInteger} value. - /// @return a JSON number created from a {@code BigInteger} value - static JsonNumber of(BigInteger num) { - return new JsonNumberImpl(num); + var str = Long.toString(num); + return new JsonNumberImpl(str.toCharArray(), 0, str.length(), -1, -1); } - /// Creates a JSON number whose string representation is the - /// string representation of the given {@code BigDecimal} value. + /// Creates a JSON number from the given `String` value. + /// The string representation of the JSON number created is equivalent to `num`. /// - /// @param num the given {@code BigDecimal} value. - /// @return a JSON number created from a {@code BigDecimal} value - static JsonNumber of(BigDecimal num) { - return new JsonNumberImpl(num); + /// @param num the given `String` value. + /// @return a JSON number created from the `String` value + /// @throws IllegalArgumentException if `num` is not a valid string + /// representation of a JSON number. + static JsonNumber of(String num) { + try { + if (Json.parse(num) instanceof JsonNumber jn) { + return jn; + } + } catch (JsonParseException ex) { + // fall through to error + } + throw new IllegalArgumentException("Not a JSON number"); } - /// {@return the decimal string representation of this {@code JsonNumber}} + /// {@return the string representation of this `JsonNumber`} /// - /// If this {@code JsonNumber} is created by parsing a JSON number in a JSON document, + /// If this `JsonNumber` is created by parsing a JSON number in a JSON document, /// it preserves the string representation in the document, regardless of its - /// precision or range. For example, a JSON number like - /// {@code 3.141592653589793238462643383279} in the JSON document will be - /// returned exactly as it appears. - /// If this {@code JsonNumber} is created via one of the factory methods, - /// such as {@link JsonNumber#of(double)}, then the string representation is - /// specified by the factory method. + /// precision or range. @Override String toString(); - /// {@return true if the given {@code obj} is equal to this {@code JsonNumber}} - /// The comparison is based on the string representation of this {@code JsonNumber}, + /// {@return true if the given `obj` is equal to this `JsonNumber`} + /// The comparison is based on the string representation of this `JsonNumber`, /// ignoring the case. /// /// @see #toString() @Override boolean equals(Object obj); - /// {@return the hash code value of this {@code JsonNumber}} The returned hash code - /// is derived from the string representation of this {@code JsonNumber}, + /// {@return the hash code value of this `JsonNumber`} The returned hash code + /// is derived from the string representation of this `JsonNumber`, /// ignoring the case. /// /// @see #toString() diff --git a/json-java21/src/main/java/jdk/sandbox/java/util/json/JsonObject.java b/json-java21/src/main/java/jdk/sandbox/java/util/json/JsonObject.java index b27385f..3c664b9 100644 --- a/json-java21/src/main/java/jdk/sandbox/java/util/json/JsonObject.java +++ b/json-java21/src/main/java/jdk/sandbox/java/util/json/JsonObject.java @@ -50,8 +50,8 @@ /// )); /// /// // Access members -/// JsonString name = (JsonString) obj.members().get("name"); -/// System.out.println(name.value()); // "Alice" + /// JsonString name = (JsonString) obj.members().get("name"); + /// System.out.println(name.string()); // "Alice" /// ``` /// /// @since 99 diff --git a/json-java21/src/main/java/jdk/sandbox/java/util/json/JsonParseException.java b/json-java21/src/main/java/jdk/sandbox/java/util/json/JsonParseException.java index 88c64a5..19aa57f 100644 --- a/json-java21/src/main/java/jdk/sandbox/java/util/json/JsonParseException.java +++ b/json-java21/src/main/java/jdk/sandbox/java/util/json/JsonParseException.java @@ -37,31 +37,31 @@ public class JsonParseException extends RuntimeException { @Serial private static final long serialVersionUID = 7022545379651073390L; - /// Position of the error row in the document + /// Position of the error line in the document /// @serial - private final int row; + private final int line; - /// Position of the error column in the document + /// Position of the error location in the document /// @serial - private final int col; + private final int position; /// Constructs a JsonParseException with the specified detail message. /// @param message the detail message - /// @param row the row of the error on parsing the document - /// @param col the column of the error on parsing the document - public JsonParseException(String message, int row, int col) { + /// @param line the line of the error on parsing the document + /// @param position the character position of the error on parsing the document + public JsonParseException(String message, int line, int position) { super(message); - this.row = row; - this.col = col; + this.line = line; + this.position = position; } - /// {@return the row of the error on parsing the document} - public int getErrorRow() { - return row; + /// {@return the line of the error on parsing the document} + public int getErrorLine() { + return line; } - /// {@return the column of the error on parsing the document} - public int getErrorColumn() { - return col; + /// {@return the position of the error on parsing the document} + public int getErrorPosition() { + return position; } } diff --git a/json-java21/src/main/java/jdk/sandbox/java/util/json/JsonString.java b/json-java21/src/main/java/jdk/sandbox/java/util/json/JsonString.java index 234efdc..efe489e 100644 --- a/json-java21/src/main/java/jdk/sandbox/java/util/json/JsonString.java +++ b/json-java21/src/main/java/jdk/sandbox/java/util/json/JsonString.java @@ -53,12 +53,12 @@ public non-sealed interface JsonString extends JsonValue { /// {@return the {@code JsonString} created from the given /// {@code String}} /// - /// @param value the given {@code String} used as the {@code value} of this - /// {@code JsonString}. Non-null. - /// @throws NullPointerException if {@code value} is {@code null} - static JsonString of(String value) { - Objects.requireNonNull(value); - return new JsonStringImpl(value); + /// @param src the given source {@code String}. Non-null. + /// @throws NullPointerException if {@code src} is {@code null} + static JsonString of(String src) { + var escaped = '"' + jdk.sandbox.internal.util.json.Utils.escape(Objects.requireNonNull(src)) + '"'; + return new JsonStringImpl(escaped.toCharArray(), 0, escaped.length(), + escaped.length() != src.length() + 2); } /// {@return the JSON {@code String} represented by this {@code JsonString}} @@ -66,7 +66,7 @@ static JsonString of(String value) { /// preserves the text representation of the corresponding JSON String. Otherwise, /// the {@code value} is escaped to produce the JSON {@code String}. /// - /// @see #value() + /// @see #string() String toString(); /// {@return the {@code String} value represented by this {@code JsonString}} @@ -75,21 +75,21 @@ static JsonString of(String value) { /// unescaped form. /// /// @see #toString() - String value(); + String string(); /// {@return true if the given {@code obj} is equal to this {@code JsonString}} /// Two {@code JsonString}s {@code js1} and {@code js2} represent the same value - /// if {@code js1.value().equals(js2.value())}. + /// if {@code js1.string().equals(js2.string())}. /// - /// @see #value() + /// @see #string() @Override boolean equals(Object obj); /// {@return the hash code value of this {@code JsonString}} The hash code of a /// {@code JsonString} is derived from the hash code of {@code JsonString}'s - /// {@link #value()}. + /// {@link #string()}. /// - /// @see #value() + /// @see #string() @Override int hashCode(); } diff --git a/json-java21/src/main/java/jdk/sandbox/java/util/json/JsonValue.java b/json-java21/src/main/java/jdk/sandbox/java/util/json/JsonValue.java index b7659f4..bf40a89 100644 --- a/json-java21/src/main/java/jdk/sandbox/java/util/json/JsonValue.java +++ b/json-java21/src/main/java/jdk/sandbox/java/util/json/JsonValue.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2025, Oracle and/or its affiliates. All rights reserved. + * Copyright (c) 2025, 2026, Oracle and/or its affiliates. All rights reserved. * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. * * This code is free software; you can redistribute it and/or modify it @@ -25,73 +25,108 @@ package jdk.sandbox.java.util.json; +import jdk.sandbox.internal.util.json.Utils; + +import java.util.List; +import java.util.Map; +import java.util.Objects; +import java.util.Optional; /// The interface that represents a JSON value. -/// -/// Instances of `JsonValue` are immutable and thread safe. -/// -/// A `JsonValue` can be produced by {@link Json#parse(String)} or {@link -/// Json#fromUntyped(Object)}. See {@link #toString()} for converting a `JsonValue` -/// to its corresponding JSON String. -/// -/// ## Example -/// ```java -/// List values = Arrays.asList("foo", true, 25); -/// JsonValue json = Json.fromUntyped(values); -/// json.toString(); // returns "[\"foo\",true,25]" -/// ``` -/// -/// ## Pattern Matching -/// ```java -/// JsonValue value = Json.parse(jsonString); -/// switch (value) { -/// case JsonObject obj -> processObject(obj); -/// case JsonArray arr -> processArray(arr); -/// case JsonString str -> processString(str); -/// case JsonNumber num -> processNumber(num); -/// case JsonBoolean bool -> processBoolean(bool); -/// case JsonNull n -> processNull(); -/// } -/// ``` /// -/// A class implementing a non-sealed `JsonValue` sub-interface must adhere -/// to the following: -/// - The class's implementations of `equals`, `hashCode`, -/// and `toString` compute their results solely from the values -/// of the class's instance fields (and the members of the objects they -/// reference), not from the instance's identity. -/// - The class's methods treat instances as *freely substitutable* -/// when equal, meaning that interchanging any two instances `x` and -/// `y` that are equal according to `equals()` produces no -/// visible change in the behavior of the class's methods. -/// - The class performs no synchronization using an instance's monitor. -/// - The class does not provide any instance creation mechanism that promises -/// a unique identity on each method call—in particular, any factory -/// method's contract must allow for the possibility that if two independently-produced -/// instances are equal according to `equals()`, they may also be -/// equal according to `==`. +/// Instances of `JsonValue` are immutable and thread safe. /// -/// Users of `JsonValue` instances should ensure the following: -/// - When two instances of `JsonValue` are equal (according to `equals()`), users -/// should not attempt to distinguish between their identities, whether directly via reference -/// equality or indirectly via an appeal to synchronization, identity hashing, -/// serialization, or any other identity-sensitive mechanism. -/// - Synchronization on instances of `JsonValue` is strongly discouraged, -/// because the programmer cannot guarantee exclusive ownership of the -/// associated monitor. +/// A `JsonValue` can be produced by {@link Json#parse(String)}. /// /// @since 99 public sealed interface JsonValue permits JsonString, JsonNumber, JsonObject, JsonArray, JsonBoolean, JsonNull { /// {@return the String representation of this `JsonValue` that conforms - /// to the JSON syntax} If this `JsonValue` is created by parsing a - /// JSON document, it preserves the text representation of the corresponding - /// JSON element, except that the returned string does not contain any white - /// spaces or newlines to produce a compact representation. - /// For a String representation suitable for display, use - /// {@link Json#toDisplayString(JsonValue, int)}. + /// to the JSON syntax} For a String representation suitable for display, + /// use {@link Json#toDisplayString(JsonValue, int)}. /// /// @see Json#toDisplayString(JsonValue, int) String toString(); + + /// {@return the `boolean` value represented by a `JsonBoolean`} + default boolean bool() { + throw Utils.composeTypeError(this, "JsonBoolean"); + } + + /// {@return this `JsonValue` as a `long`} + default long toLong() { + throw Utils.composeTypeError(this, "JsonNumber"); + } + + /// {@return this `JsonValue` as a `double`} + default double toDouble() { + throw Utils.composeTypeError(this, "JsonNumber"); + } + + /// {@return the `String` value represented by a `JsonString`} + default String string() { + throw Utils.composeTypeError(this, "JsonString"); + } + + /// {@return an `Optional` containing this `JsonValue` if it is not a + /// `JsonNull`, otherwise an empty `Optional`} + default Optional valueOrNull() { + return switch (this) { + case JsonNull ignored -> Optional.empty(); + case JsonValue ignored -> Optional.of(this); + }; + } + + /// {@return the {@link JsonArray#elements() elements} of a `JsonArray`} + default List elements() { + throw Utils.composeTypeError(this, "JsonArray"); + } + + /// {@return the {@link JsonObject#members() members} of a `JsonObject`} + default Map members() { + throw Utils.composeTypeError(this, "JsonObject"); + } + + /// {@return the `JsonValue` associated with the given member name of a `JsonObject`} + /// + /// @param name the member name + /// @throws NullPointerException if the member name is `null` + /// @throws JsonAssertionException if this `JsonValue` is not a `JsonObject` or + /// there is no association with the member name + default JsonValue get(String name) { + Objects.requireNonNull(name); + return switch (members().get(name)) { + case JsonValue jv -> jv; + case null -> throw Utils.composeError(this, + "JsonObject member \"%s\" does not exist.".formatted(name)); + }; + } + + /// {@return an `Optional` containing the `JsonValue` associated with the given member + /// name of a `JsonObject`, otherwise if there is no association an empty `Optional`} + /// + /// @param name the member name + /// @throws NullPointerException if the member name is `null` + /// @throws JsonAssertionException if this `JsonValue` is not a `JsonObject` + default Optional getOrAbsent(String name) { + Objects.requireNonNull(name); + return Optional.ofNullable(members().get(name)); + } + + /// {@return the `JsonValue` associated with the given index of a `JsonArray`} + /// + /// @param index the index of the array + /// @throws JsonAssertionException if this `JsonValue` is not a `JsonArray` + /// or the given index is outside the bounds + default JsonValue element(int index) { + List elements = elements(); + try { + return elements.get(index); + } catch (IndexOutOfBoundsException ex) { + throw Utils.composeError(this, + "JsonArray index %d out of bounds for length %d." + .formatted(index, elements.size())); + } + } } diff --git a/json-java21/src/main/java/jdk/sandbox/java/util/json/package-info.java b/json-java21/src/main/java/jdk/sandbox/java/util/json/package-info.java index 0cb20b5..aa0aa0f 100644 --- a/json-java21/src/main/java/jdk/sandbox/java/util/json/package-info.java +++ b/json-java21/src/main/java/jdk/sandbox/java/util/json/package-info.java @@ -23,168 +23,38 @@ * questions. */ -/// Provides APIs for parsing JSON text, creating `JsonValue`s, and -/// offering a mapping between a `JsonValue` and its corresponding Java Object. -/// -/// ## Design -/// This API is designed so that JSON values are composed as Algebraic -/// Data Types (ADTs) defined by interfaces. Each JSON value is represented as a -/// sealed `JsonValue` _sum_ type, which can be -/// pattern-matched into one of the following _product_ types: `JsonObject`, -/// `JsonArray`, `JsonString`, `JsonNumber`, `JsonBoolean`, -/// `JsonNull`. These product types are defined as non-sealed interfaces that -/// allow flexibility in the implementation of the type. For example, `JsonArray` -/// is defined as follows: -/// ```java -/// public non-sealed interface JsonArray extends JsonValue -/// ``` -/// -/// This API relies on pattern matching to allow for the extraction of a -/// JSON Value in a _single and class safe expression_ as follows: +/// Provides APIs for parsing JSON text, retrieving JSON values in the text, and +/// generating JSON text. +/// +/// ## Parsing JSON documents +/// Parsing produces a `JsonValue` from JSON text via `Json.parse(String)` or +/// `Json.parse(char[])`. A successful parse indicates that the JSON text adheres +/// to the RFC 8259 grammar. The parsing APIs provided do not accept JSON text +/// that contains JSON objects with duplicate names. +/// +/// ## Retrieving JSON values +/// Retrieving values from a JSON document involves two steps: first navigating +/// the document structure using access methods, and then converting the result +/// to the desired type using conversion methods. For example: /// ```java -/// JsonValue doc = Json.parse(text); -/// if (doc instanceof JsonObject o && o.members() instanceof Map members -/// && members.get("name") instanceof JsonString js && js.value() instanceof String name -/// && members.get("age") instanceof JsonNumber jn && jn.toNumber() instanceof long age) { -/// // can use both "name" and "age" from a single expression -/// } -/// ``` -/// -/// Both `JsonValue` instances and their underlying values are immutable. -/// -/// ## Parsing -/// -/// Parsing produces a `JsonValue` from JSON text and is done using either -/// {@link Json#parse(java.lang.String)} or {@link Json#parse(char[])}. A successful -/// parse indicates that the JSON text adheres to the -/// [JSON grammar](https://datatracker.ietf.org/doc/html/rfc8259). -/// The parsing APIs provided do not accept JSON text that contain JSON Objects -/// with duplicate names. -/// -/// For the reference JDK implementation, `JsonValue`s created via parsing -/// procure their underlying values _lazily_. -/// -/// ## Formatting -/// -/// Formatting of a `JsonValue` is performed with either {@link -/// JsonValue#toString()} or {@link Json#toDisplayString(JsonValue, int)}. -/// These methods produce formatted String representations of a `JsonValue`. -/// The returned text adheres to the JSON grammar defined in RFC 8259. -/// `JsonValue.toString()` produces the most compact representation which does not -/// include extra whitespaces or line-breaks, preferable for network transaction -/// or storage. `Json.toDisplayString(JsonValue, int)` produces a text representation that -/// is human friendly, preferable for debugging or logging. -/// -/// --- -/// -/// ## Usage Notes from Unofficial Backport -/// -/// ### Major Classes -/// -/// - {@link Json} - Main entry point for parsing and converting JSON -/// - {@link JsonValue} - Base sealed interface for all JSON values -/// - {@link JsonObject} - Represents JSON objects (key-value pairs) -/// - {@link JsonArray} - Represents JSON arrays -/// - {@link JsonString} - Represents JSON strings -/// - {@link JsonNumber} - Represents JSON numbers -/// - {@link JsonBoolean} - Represents JSON booleans (true/false) -/// - {@link JsonNull} - Represents JSON null -/// - {@link JsonParseException} - Thrown when parsing invalid JSON -/// -/// ### Simple Parsing Example -/// -/// ```java -/// // Parse a JSON string -/// String jsonText = """ -/// { -/// "name": "Alice", -/// "age": 30, -/// "active": true -/// } -/// """; -/// -/// JsonValue value = Json.parse(jsonText); -/// JsonObject obj = (JsonObject) value; -/// -/// // Access values -/// String name = ((JsonString) obj.members().get("name")).value(); -/// int age = ((JsonNumber) obj.members().get("age")).toNumber().intValue(); -/// boolean active = ((JsonBoolean) obj.members().get("active")).value(); -/// ``` -/// -/// ### Record Mapping Example -/// -/// The API works seamlessly with Java records for domain modeling: -/// -/// ```java -/// // Define your domain model -/// record User(String name, String email, boolean active) {} -/// record Team(String teamName, List members) {} -/// -/// // Create domain objects -/// Team team = new Team("Engineering", List.of( -/// new User("Alice", "alice@example.com", true), -/// new User("Bob", "bob@example.com", false) -/// )); -/// -/// // Convert to JSON using Java collections -/// 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() -/// )); -/// -/// // Parse 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() -/// .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() -/// ); -/// }) -/// .toList() -/// ); -/// ``` -/// -/// ### REST API Response Example -/// -/// Build complex JSON structures programmatically: -/// -/// ```java -/// // Build a typical REST API response -/// JsonObject response = JsonObject.of(Map.of( -/// "status", JsonString.of("success"), -/// "data", JsonObject.of(Map.of( -/// "user", JsonObject.of(Map.of( -/// "id", JsonNumber.of(12345), -/// "name", JsonString.of("John Doe"), -/// "roles", JsonArray.of(List.of( -/// JsonString.of("admin"), -/// JsonString.of("user") -/// )) -/// )), -/// "timestamp", JsonNumber.of(System.currentTimeMillis()) -/// )), -/// "errors", JsonArray.of(List.of()) -/// )); -/// -/// // Pretty print the response -/// String formatted = Json.toDisplayString(response, 2); +/// var name = doc.get("foo").get("bar").element(0).string(); /// ``` +/// By chaining access methods, the "foo" member is retrieved from the root object, +/// then the "bar" member from "foo", followed by the element at index 0 from "bar". +/// The navigation process leads to a leaf JSON string element. The final call to +/// `string()` returns the corresponding `String` value. For more details on these +/// methods, see `JsonValue`. +/// +/// ## Generating JSON documents +/// Generating JSON text is performed with either `JsonValue.toString()` or +/// `Json.toDisplayString(JsonValue, int)`. These methods produce formatted +/// string representations of a `JsonValue` that adhere to the JSON grammar +/// defined in RFC 8259. `JsonValue.toString()` produces the most compact +/// representation, while `Json.toDisplayString(JsonValue, int)` produces a +/// human-friendly, indented representation suitable for logging or debugging. /// /// @spec https://datatracker.ietf.org/doc/html/rfc8259 RFC 8259: The JavaScript /// Object Notation (JSON) Data Interchange Format /// @since 99 package jdk.sandbox.java.util.json; - diff --git a/json-java21/src/test/java/jdk/sandbox/internal/util/json/JsonParserTests.java b/json-java21/src/test/java/jdk/sandbox/internal/util/json/JsonParserTests.java index 860358f..c1c7d6b 100644 --- a/json-java21/src/test/java/jdk/sandbox/internal/util/json/JsonParserTests.java +++ b/json-java21/src/test/java/jdk/sandbox/internal/util/json/JsonParserTests.java @@ -14,24 +14,24 @@ public class JsonParserTests { void testParseComplexJson() { JsonObject jsonObject = complexJsonObject(); - assertThat(((JsonString) jsonObject.members().get("name")).value()).isEqualTo("John Doe"); - assertThat(((JsonNumber) jsonObject.members().get("age")).toNumber().longValue()).isEqualTo(30L); - assertThat(((JsonBoolean) jsonObject.members().get("isStudent")).value()).isFalse(); + assertThat(((JsonString) jsonObject.members().get("name")).string()).isEqualTo("John Doe"); + assertThat(((JsonNumber) jsonObject.members().get("age")).toLong()).isEqualTo(30L); + assertThat(((JsonBoolean) jsonObject.members().get("isStudent")).bool()).isFalse(); JsonArray courses = (JsonArray) jsonObject.members().get("courses"); - assertThat(courses.values()).hasSize(2); + assertThat(courses.elements()).hasSize(2); - JsonObject course1 = (JsonObject) courses.values().getFirst(); - assertThat(((JsonString) course1.members().get("title")).value()).isEqualTo("History"); - assertThat(((JsonNumber) course1.members().get("credits")).toNumber().longValue()).isEqualTo(3L); + JsonObject course1 = (JsonObject) courses.elements().getFirst(); + assertThat(((JsonString) course1.members().get("title")).string()).isEqualTo("History"); + assertThat(((JsonNumber) course1.members().get("credits")).toLong()).isEqualTo(3L); - JsonObject course2 = (JsonObject) courses.values().get(1); - assertThat(((JsonString) course2.members().get("title")).value()).isEqualTo("Math"); - assertThat(((JsonNumber) course2.members().get("credits")).toNumber().longValue()).isEqualTo(4L); + JsonObject course2 = (JsonObject) courses.elements().get(1); + assertThat(((JsonString) course2.members().get("title")).string()).isEqualTo("Math"); + assertThat(((JsonNumber) course2.members().get("credits")).toLong()).isEqualTo(4L); JsonObject address = (JsonObject) jsonObject.members().get("address"); - assertThat(((JsonString) address.members().get("street")).value()).isEqualTo("123 Main St"); - assertThat(((JsonString) address.members().get("city")).value()).isEqualTo("Anytown"); + assertThat(((JsonString) address.members().get("street")).string()).isEqualTo("123 Main St"); + assertThat(((JsonString) address.members().get("city")).string()).isEqualTo("Anytown"); } private static JsonObject complexJsonObject() { diff --git a/json-java21/src/test/java/jdk/sandbox/internal/util/json/JsonPatternMatchingTests.java b/json-java21/src/test/java/jdk/sandbox/internal/util/json/JsonPatternMatchingTests.java index ca88b80..73d5fe3 100644 --- a/json-java21/src/test/java/jdk/sandbox/internal/util/json/JsonPatternMatchingTests.java +++ b/json-java21/src/test/java/jdk/sandbox/internal/util/json/JsonPatternMatchingTests.java @@ -16,10 +16,10 @@ public class JsonPatternMatchingTests { private String identifyJsonValue(JsonValue jsonValue) { return switch (jsonValue) { case JsonObject o -> "Object with " + o.members().size() + " members"; - case JsonArray a -> "Array with " + a.values().size() + " elements"; - case JsonString s -> "String with value: " + s.value(); - case JsonNumber n -> "Number with value: " + n.toNumber(); - case JsonBoolean b -> "Boolean with value: " + b.value(); + case JsonArray a -> "Array with " + a.elements().size() + " elements"; + case JsonString s -> "String with value: " + s.string(); + case JsonNumber n -> "Number with value: " + n.toDouble(); + case JsonBoolean b -> "Boolean with value: " + b.bool(); case JsonNull ignored -> "Null"; }; } diff --git a/json-java21/src/test/java/jdk/sandbox/internal/util/json/JsonRecordMappingTests.java b/json-java21/src/test/java/jdk/sandbox/internal/util/json/JsonRecordMappingTests.java index e7202d1..1585f49 100644 --- a/json-java21/src/test/java/jdk/sandbox/internal/util/json/JsonRecordMappingTests.java +++ b/json-java21/src/test/java/jdk/sandbox/internal/util/json/JsonRecordMappingTests.java @@ -77,31 +77,31 @@ private Ecommerce toDomain(JsonValue jsonValue) { } Map members = jsonObject.members(); - String type = ((JsonString) members.get("type")).value(); + String type = ((JsonString) members.get("type")).string(); return switch (type) { case "order" -> { - String orderId = ((JsonString) members.get("orderId")).value(); + String orderId = ((JsonString) members.get("orderId")).string(); Customer customer = (Customer) toDomain(members.get("customer")); - List items = ((JsonArray) members.get("items")).values().stream() + List items = ((JsonArray) members.get("items")).elements().stream() .map(item -> (LineItem) toDomain(item)) .collect(Collectors.toList()); yield new Order(orderId, customer, items); } case "customer" -> { - String name = ((JsonString) members.get("name")).value(); - String email = ((JsonString) members.get("email")).value(); + String name = ((JsonString) members.get("name")).string(); + String email = ((JsonString) members.get("email")).string(); yield new Customer(name, email); } case "lineItem" -> { Product product = (Product) toDomain(members.get("product")); - int quantity = ((JsonNumber) members.get("quantity")).toNumber().intValue(); + int quantity = Math.toIntExact(((JsonNumber) members.get("quantity")).toLong()); yield new LineItem(product, quantity); } case "product" -> { - String sku = ((JsonString) members.get("sku")).value(); - String name = ((JsonString) members.get("name")).value(); - double price = ((JsonNumber) members.get("price")).toNumber().doubleValue(); + String sku = ((JsonString) members.get("sku")).string(); + String name = ((JsonString) members.get("name")).string(); + double price = ((JsonNumber) members.get("price")).toDouble(); yield new Product(sku, name, price); } default -> throw new IllegalStateException("Unexpected value: " + type); diff --git a/json-java21/src/test/java/jdk/sandbox/java/util/json/EscapedKeyBugTest.java b/json-java21/src/test/java/jdk/sandbox/java/util/json/EscapedKeyBugTest.java index 49dec75..b18d261 100644 --- a/json-java21/src/test/java/jdk/sandbox/java/util/json/EscapedKeyBugTest.java +++ b/json-java21/src/test/java/jdk/sandbox/java/util/json/EscapedKeyBugTest.java @@ -51,8 +51,8 @@ public void testEscapedCharactersInKeys() { JsonObject obj = (JsonObject) result; // Verify both keys are parsed correctly - assertEquals(1L, ((JsonNumber) obj.members().get("foo\nbar")).toNumber()); - assertEquals(2L, ((JsonNumber) obj.members().get("foo\tbar")).toNumber()); + assertEquals(1L, ((JsonNumber) obj.members().get("foo\nbar")).toLong()); + assertEquals(2L, ((JsonNumber) obj.members().get("foo\tbar")).toLong()); } @Test @@ -67,7 +67,7 @@ public void testEscapedQuoteInKey() { JsonObject obj = (JsonObject) result; // Verify key with escaped quote is parsed correctly - assertEquals(1L, ((JsonNumber) obj.members().get("foo\"bar")).toNumber()); + assertEquals(1L, ((JsonNumber) obj.members().get("foo\"bar")).toLong()); } @Test @@ -82,7 +82,7 @@ public void testEscapedBackslashInKey() { JsonObject obj = (JsonObject) result; // Verify key with escaped backslash is parsed correctly - assertEquals(1L, ((JsonNumber) obj.members().get("foo\\bar")).toNumber()); + assertEquals(1L, ((JsonNumber) obj.members().get("foo\\bar")).toLong()); } @Test @@ -97,6 +97,6 @@ public void testMultipleEscapedCharactersInKey() { JsonObject obj = (JsonObject) result; // Verify key with multiple escaped characters is parsed correctly - assertEquals(1L, ((JsonNumber) obj.members().get("foo\n\t\"bar")).toNumber()); + assertEquals(1L, ((JsonNumber) obj.members().get("foo\n\t\"bar")).toLong()); } } diff --git a/json-java21/src/test/java/jdk/sandbox/java/util/json/JsonTypedUntypedTests.java b/json-java21/src/test/java/jdk/sandbox/java/util/json/JsonTypedUntypedTests.java deleted file mode 100644 index a981ce9..0000000 --- a/json-java21/src/test/java/jdk/sandbox/java/util/json/JsonTypedUntypedTests.java +++ /dev/null @@ -1,236 +0,0 @@ -package jdk.sandbox.java.util.json; - -import org.junit.jupiter.api.Test; - -import java.math.BigDecimal; -import java.math.BigInteger; -import java.util.List; -import java.util.Map; - -import static org.assertj.core.api.Assertions.assertThat; -import static org.assertj.core.api.Assertions.assertThatThrownBy; - -public class JsonTypedUntypedTests { - - @Test - void testFromUntypedWithSimpleTypes() { - // Test string - JsonValue jsonString = Json.fromUntyped("hello"); - assertThat(jsonString).isInstanceOf(JsonString.class); - assertThat(((JsonString) jsonString).value()).isEqualTo("hello"); - - // Test integer - JsonValue jsonInt = Json.fromUntyped(42); - assertThat(jsonInt).isInstanceOf(JsonNumber.class); - assertThat(((JsonNumber) jsonInt).toNumber()).isEqualTo(42L); - - // Test long - JsonValue jsonLong = Json.fromUntyped(42L); - assertThat(jsonLong).isInstanceOf(JsonNumber.class); - assertThat(((JsonNumber) jsonLong).toNumber()).isEqualTo(42L); - - // Test double - JsonValue jsonDouble = Json.fromUntyped(3.14); - assertThat(jsonDouble).isInstanceOf(JsonNumber.class); - assertThat(((JsonNumber) jsonDouble).toNumber()).isEqualTo(3.14); - - // Test boolean - JsonValue jsonBool = Json.fromUntyped(true); - assertThat(jsonBool).isInstanceOf(JsonBoolean.class); - assertThat(((JsonBoolean) jsonBool).value()).isTrue(); - - // Test null - JsonValue jsonNull = Json.fromUntyped(null); - assertThat(jsonNull).isInstanceOf(JsonNull.class); - } - - @Test - void testFromUntypedWithBigNumbers() { - // Test BigInteger - BigInteger bigInt = new BigInteger("123456789012345678901234567890"); - JsonValue jsonBigInt = Json.fromUntyped(bigInt); - assertThat(jsonBigInt).isInstanceOf(JsonNumber.class); - assertThat(((JsonNumber) jsonBigInt).toNumber()).isEqualTo(bigInt); - - // Test BigDecimal - BigDecimal bigDec = new BigDecimal("123456789012345678901234567890.123456789"); - JsonValue jsonBigDec = Json.fromUntyped(bigDec); - assertThat(jsonBigDec).isInstanceOf(JsonNumber.class); - assertThat(((JsonNumber) jsonBigDec).toNumber()).isEqualTo(bigDec); - } - - @Test - void testFromUntypedWithCollections() { - // Test List - List list = List.of("item1", 42, true); - JsonValue jsonArray = Json.fromUntyped(list); - assertThat(jsonArray).isInstanceOf(JsonArray.class); - JsonArray array = (JsonArray) jsonArray; - assertThat(array.values()).hasSize(3); - assertThat(((JsonString) array.values().get(0)).value()).isEqualTo("item1"); - assertThat(((JsonNumber) array.values().get(1)).toNumber()).isEqualTo(42L); - assertThat(((JsonBoolean) array.values().get(2)).value()).isTrue(); - - // Test Map - Map map = Map.of("name", "John", "age", 30, "active", true); - JsonValue jsonObject = Json.fromUntyped(map); - assertThat(jsonObject).isInstanceOf(JsonObject.class); - JsonObject obj = (JsonObject) jsonObject; - assertThat(((JsonString) obj.members().get("name")).value()).isEqualTo("John"); - assertThat(((JsonNumber) obj.members().get("age")).toNumber()).isEqualTo(30L); - assertThat(((JsonBoolean) obj.members().get("active")).value()).isTrue(); - } - - @Test - void testFromUntypedWithNestedStructures() { - Map nested = Map.of( - "user", Map.of("name", "John", "age", 30), - "scores", List.of(85, 92, 78), - "active", true - ); - - JsonValue json = Json.fromUntyped(nested); - assertThat(json).isInstanceOf(JsonObject.class); - - JsonObject root = (JsonObject) json; - JsonObject user = (JsonObject) root.members().get("user"); - assertThat(((JsonString) user.members().get("name")).value()).isEqualTo("John"); - - JsonArray scores = (JsonArray) root.members().get("scores"); - assertThat(scores.values()).hasSize(3); - assertThat(((JsonNumber) scores.values().getFirst()).toNumber()).isEqualTo(85L); - } - - @Test - void testFromUntypedWithJsonValue() { - // If input is already a JsonValue, return as-is - JsonString original = JsonString.of("test"); - JsonValue result = Json.fromUntyped(original); - assertThat(result).isSameAs(original); - } - - @Test - void testFromUntypedWithInvalidTypes() { - // Test with unsupported type - assertThatThrownBy(() -> Json.fromUntyped(new StringBuilder("test"))) - .isInstanceOf(IllegalArgumentException.class) - .hasMessageContaining("StringBuilder is not a recognized type"); - } - - @Test - void testFromUntypedWithNonStringMapKey() { - // Test map with non-string key - Map invalidMap = Map.of(123, "value"); - assertThatThrownBy(() -> Json.fromUntyped(invalidMap)) - .isInstanceOf(IllegalArgumentException.class) - .hasMessageContaining("The key '123' is not a String"); - } - - @Test - void testToUntypedWithSimpleTypes() { - // Test string - Object str = Json.toUntyped(JsonString.of("hello")); - assertThat(str).isEqualTo("hello"); - - // Test number - Object num = Json.toUntyped(JsonNumber.of(42)); - assertThat(num).isEqualTo(42L); - - // Test boolean - Object bool = Json.toUntyped(JsonBoolean.of(true)); - assertThat(bool).isEqualTo(true); - - // Test null - Object nullVal = Json.toUntyped(JsonNull.of()); - assertThat(nullVal).isNull(); - } - - @Test - void testToUntypedWithCollections() { - // Test array - JsonArray array = JsonArray.of(List.of( - JsonString.of("item1"), - JsonNumber.of(42), - JsonBoolean.of(true) - )); - Object result = Json.toUntyped(array); - assertThat(result).isInstanceOf(List.class); - @SuppressWarnings("unchecked") - List list = (List) result; - assertThat(list).containsExactly("item1", 42L, true); - - // Test object - JsonObject obj = JsonObject.of(Map.of( - "name", JsonString.of("John"), - "age", JsonNumber.of(30), - "active", JsonBoolean.of(true) - )); - Object objResult = Json.toUntyped(obj); - assertThat(objResult).isInstanceOf(Map.class); - Map map = (Map) objResult; - assert map != null; - assertThat(map.get("name")).isEqualTo("John"); - assertThat(map.get("age")).isEqualTo(30L); - assertThat(map.get("active")).isEqualTo(true); - } - - @Test - void testRoundTripConversion() { - // Create complex nested structure - Map original = Map.of( - "user", Map.of( - "name", "John Doe", - "age", 30, - "email", "john@example.com" - ), - "scores", List.of(85.5, 92.0, 78.3), - "active", true, - "metadata", Map.of( - "created", "2024-01-01", - "tags", List.of("vip", "premium") - ) - ); - - // Convert to JsonValue and back - JsonValue json = Json.fromUntyped(original); - Object reconstructed = Json.toUntyped(json); - - // Verify structure is preserved - assertThat(reconstructed).isInstanceOf(Map.class); - Map resultMap = (Map) reconstructed; - - assert resultMap != null; - Map user = (Map) resultMap.get("user"); - assertThat(user.get("name")).isEqualTo("John Doe"); - assertThat(user.get("age")).isEqualTo(30L); - - @SuppressWarnings("unchecked") - List scores = (List) resultMap.get("scores"); - assertThat(scores).containsExactly(85.5, 92.0, 78.3); - - Map metadata = (Map) resultMap.get("metadata"); - @SuppressWarnings("unchecked") - List tags = (List) metadata.get("tags"); - assertThat(tags).containsExactly("vip", "premium"); - } - - @Test - void testToUntypedPreservesOrder() { - // JsonObject should preserve insertion order - JsonObject obj = JsonObject.of(Map.of( - "z", JsonString.of("last"), - "a", JsonString.of("first"), - "m", JsonString.of("middle") - )); - - Object result = Json.toUntyped(obj); - assertThat(result).isInstanceOf(Map.class); - - // The order might not be preserved with Map.of(), so let's just verify contents - @SuppressWarnings("unchecked") - Map map = (Map) result; - assertThat(map).containsEntry("z", "last") - .containsEntry("a", "first") - .containsEntry("m", "middle"); - } -} \ No newline at end of file diff --git a/json-java21/src/test/java/jdk/sandbox/java/util/json/ReadmeDemoTests.java b/json-java21/src/test/java/jdk/sandbox/java/util/json/ReadmeDemoTests.java index ada8173..0708c4a 100644 --- a/json-java21/src/test/java/jdk/sandbox/java/util/json/ReadmeDemoTests.java +++ b/json-java21/src/test/java/jdk/sandbox/java/util/json/ReadmeDemoTests.java @@ -18,8 +18,8 @@ void quickStartExample() { assertThat(value).isInstanceOf(JsonObject.class); JsonObject obj = (JsonObject) value; - assertThat(((JsonString) obj.members().get("name")).value()).isEqualTo("Alice"); - assertThat(((JsonNumber) obj.members().get("age")).toNumber()).isEqualTo(30L); + assertThat(((JsonString) obj.members().get("name")).string()).isEqualTo("Alice"); + assertThat(((JsonNumber) obj.members().get("age")).toLong()).isEqualTo(30L); String roundTrip = value.toString(); assertThat(roundTrip).isEqualTo(jsonString); @@ -40,37 +40,37 @@ void recordMappingExample() { new User("Bob", "bob@example.com", false) )); - // Convert records to JSON using untyped conversion - 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() + // Convert records to JSON + 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()) )); // Verify the JSON structure assertThat(teamJson).isInstanceOf(JsonObject.class); JsonObject teamObj = (JsonObject) teamJson; - assertThat(((JsonString) teamObj.members().get("teamName")).value()).isEqualTo("Engineering"); + assertThat(((JsonString) teamObj.members().get("teamName")).string()).isEqualTo("Engineering"); JsonArray members = (JsonArray) teamObj.members().get("members"); - assertThat(members.values()).hasSize(2); + assertThat(members.elements()).hasSize(2); // 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() @@ -78,12 +78,6 @@ void recordMappingExample() { // Verify reconstruction assertThat(reconstructed).isEqualTo(team); - assertThat(reconstructed.teamName()).isEqualTo("Engineering"); - assertThat(reconstructed.members()).hasSize(2); - assertThat(reconstructed.members().get(0).name()).isEqualTo("Alice"); - assertThat(reconstructed.members().get(0).active()).isTrue(); - assertThat(reconstructed.members().get(1).name()).isEqualTo("Bob"); - assertThat(reconstructed.members().get(1).active()).isFalse(); } @Test @@ -106,19 +100,19 @@ void builderPatternExample() { )); // Verify structure - assertThat(((JsonString) response.members().get("status")).value()).isEqualTo("success"); + assertThat(((JsonString) response.members().get("status")).string()).isEqualTo("success"); JsonObject data = (JsonObject) response.members().get("data"); JsonObject user = (JsonObject) data.members().get("user"); - assertThat(((JsonNumber) user.members().get("id")).toNumber()).isEqualTo(12345L); - assertThat(((JsonString) user.members().get("name")).value()).isEqualTo("John Doe"); + assertThat(((JsonNumber) user.members().get("id")).toLong()).isEqualTo(12345L); + assertThat(((JsonString) user.members().get("name")).string()).isEqualTo("John Doe"); JsonArray roles = (JsonArray) user.members().get("roles"); - assertThat(roles.values()).hasSize(2); - assertThat(((JsonString) roles.values().getFirst()).value()).isEqualTo("admin"); + assertThat(roles.elements()).hasSize(2); + assertThat(((JsonString) roles.elements().getFirst()).string()).isEqualTo("admin"); JsonArray errors = (JsonArray) response.members().get("errors"); - assertThat(errors.values()).isEmpty(); + assertThat(errors.elements()).isEmpty(); } @Test @@ -136,10 +130,10 @@ void streamingProcessingExample() { // Process a large array of records JsonArray items = (JsonArray) Json.parse(largeJsonArray); - List activeUserEmails = items.values().stream() + List activeUserEmails = items.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(); // Verify we got only active users @@ -164,40 +158,6 @@ void errorHandlingExample() { .hasMessageContaining("Expecting a JSON Object member name"); } - @Test - void typeConversionExample() { - // Using fromUntyped and toUntyped for complex structures - Map config = Map.of( - "server", Map.of( - "host", "localhost", - "port", 8080, - "ssl", true - ), - "features", List.of("auth", "logging", "metrics"), - "maxConnections", 1000 - ); - - // Convert to JSON - JsonValue json = Json.fromUntyped(config); - - // Convert back to Java types - @SuppressWarnings("unchecked") - Map restored = (Map) Json.toUntyped(json); - - // Verify round-trip conversion - assert restored != null; - @SuppressWarnings("unchecked") - Map server = (Map) restored.get("server"); - assertThat(server.get("host")).isEqualTo("localhost"); - assertThat(server.get("port")).isEqualTo(8080L); // Note: integers become Long - assertThat(server.get("ssl")).isEqualTo(true); - - @SuppressWarnings("unchecked") - List features = (List) restored.get("features"); - assertThat(features).containsExactly("auth", "logging", "metrics"); - - assertThat(restored.get("maxConnections")).isEqualTo(1000L); - } @Test void displayFormattingExample() { @@ -224,4 +184,28 @@ void displayFormattingExample() { assertThat(formatted).contains(" ]"); assertThat(formatted).contains("}"); } + + @Test + void complexConfigurationExample() { + // Creating a complex structure programmatically using the new API + JsonObject config = JsonObject.of(Map.of( + "server", JsonObject.of(Map.of( + "host", JsonString.of("localhost"), + "port", JsonNumber.of(8080), + "ssl", JsonBoolean.of(true) + )), + "features", JsonArray.of(List.of( + JsonString.of("auth"), + JsonString.of("logging"), + JsonString.of("metrics") + )), + "maxConnections", JsonNumber.of(1000) + )); + + // Accessing the values using the new navigation methods + assertThat(config.get("server").get("host").string()).isEqualTo("localhost"); + assertThat(config.get("server").get("port").toLong()).isEqualTo(8080L); + assertThat(config.get("features").elements()).hasSize(3); + assertThat(config.get("maxConnections").toLong()).isEqualTo(1000L); + } }