From 33b937db76545c80e5a3b9321fefec594b64686b Mon Sep 17 00:00:00 2001 From: Simon Massey Date: Fri, 25 Jul 2025 08:55:41 +0100 Subject: [PATCH 1/4] Document type conversion utilities (fromUntyped/toUntyped) Added comprehensive documentation for the Json.fromUntyped() and Json.toUntyped() methods that provide bidirectional conversion between JsonValue objects and standard Java collections/types. Includes usage examples and type mappings. --- README.md | 38 ++++++++++++++++++- .../{ => internal/util/json}/StableValue.java | 0 2 files changed, 37 insertions(+), 1 deletion(-) rename src/main/java/jdk/sandbox/{ => internal/util/json}/StableValue.java (100%) diff --git a/README.md b/README.md index 1208815..67d80f8 100644 --- a/README.md +++ b/README.md @@ -43,4 +43,40 @@ The API provides immutable JSON value types: Parsing is done via the `Json` class: ```java JsonValue value = Json.parse(jsonString); -``` \ No newline at end of file +``` + +## Type Conversion Utilities + +The `Json` class provides bidirectional conversion between `JsonValue` objects and standard Java types: + +### 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 \ No newline at end of file diff --git a/src/main/java/jdk/sandbox/StableValue.java b/src/main/java/jdk/sandbox/internal/util/json/StableValue.java similarity index 100% rename from src/main/java/jdk/sandbox/StableValue.java rename to src/main/java/jdk/sandbox/internal/util/json/StableValue.java From e9e0b18449b35d24d77b849d93b187f0ba414143 Mon Sep 17 00:00:00 2001 From: Simon Massey Date: Fri, 25 Jul 2025 08:58:42 +0100 Subject: [PATCH 2/4] Add comprehensive tests for typed/untyped conversion utilities - Created JsonTypedUntypedTests with full coverage of Json.fromUntyped() and Json.toUntyped() - Tests simple types, collections, nested structures, edge cases, and round-trip conversion - Added .mvn directory to fix mvnd warning about root directory --- src/main/java/jdk/sandbox/demo/JsonDemo.java | 4 +- .../internal/util/json/JsonNumberImpl.java | 6 +- .../internal/util/json/JsonParser.java | 2 +- .../internal/util/json/JsonStringImpl.java | 4 +- .../internal/util/json/StableValue.java | 60 ++--- .../java/jdk/sandbox/java/util/json/Json.java | 4 +- .../java/util/json/JsonTypedUntypedTests.java | 230 ++++++++++++++++++ 7 files changed, 263 insertions(+), 47 deletions(-) create mode 100644 src/test/java/jdk/sandbox/java/util/json/JsonTypedUntypedTests.java diff --git a/src/main/java/jdk/sandbox/demo/JsonDemo.java b/src/main/java/jdk/sandbox/demo/JsonDemo.java index c843f13..1e501ad 100644 --- a/src/main/java/jdk/sandbox/demo/JsonDemo.java +++ b/src/main/java/jdk/sandbox/demo/JsonDemo.java @@ -14,8 +14,8 @@ public static void main(String[] args) { "age", JsonNumber.of(30) )); - System.out.println(jsonObject.toString()); - + System.out.println(jsonObject); + // Parse JSON string String jsonStr = "{\"name\":\"Jane\",\"age\":25}"; var parsed = Json.parse(jsonStr); diff --git a/src/main/java/jdk/sandbox/internal/util/json/JsonNumberImpl.java b/src/main/java/jdk/sandbox/internal/util/json/JsonNumberImpl.java index 49e31e8..14a3cc2 100644 --- a/src/main/java/jdk/sandbox/internal/util/json/JsonNumberImpl.java +++ b/src/main/java/jdk/sandbox/internal/util/json/JsonNumberImpl.java @@ -40,9 +40,9 @@ public final class JsonNumberImpl implements JsonNumber { private final int startOffset; private final int endOffset; private final boolean isFp; - private final jdk.sandbox.StableValue theNumber = jdk.sandbox.StableValue.of(); - private final jdk.sandbox.StableValue numString = jdk.sandbox.StableValue.of(); - private final jdk.sandbox.StableValue cachedBD = jdk.sandbox.StableValue.of(); + private final StableValue theNumber = StableValue.of(); + private final StableValue numString = StableValue.of(); + private final StableValue cachedBD = StableValue.of(); public JsonNumberImpl(Number num) { // Called by factories. Input is Double, Long, BI, or BD. diff --git a/src/main/java/jdk/sandbox/internal/util/json/JsonParser.java b/src/main/java/jdk/sandbox/internal/util/json/JsonParser.java index 002954d..0eb2c42 100644 --- a/src/main/java/jdk/sandbox/internal/util/json/JsonParser.java +++ b/src/main/java/jdk/sandbox/internal/util/json/JsonParser.java @@ -48,7 +48,7 @@ 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 = jdk.sandbox.StableValue.supplier(this::initSb); + private final Supplier sb = StableValue.supplier(this::initSb); // Current offset during parsing private int offset; // For exception message on failure diff --git a/src/main/java/jdk/sandbox/internal/util/json/JsonStringImpl.java b/src/main/java/jdk/sandbox/internal/util/json/JsonStringImpl.java index 22a92db..c7a601e 100644 --- a/src/main/java/jdk/sandbox/internal/util/json/JsonStringImpl.java +++ b/src/main/java/jdk/sandbox/internal/util/json/JsonStringImpl.java @@ -42,12 +42,12 @@ public final class JsonStringImpl implements JsonString { // It always conforms to JSON syntax. If created by parsing a JSON document, // it matches the original text exactly. If created via the factory method, // non-conformant characters are properly escaped. - private final jdk.sandbox.StableValue jsonStr = jdk.sandbox.StableValue.of(); + 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. - private final jdk.sandbox.StableValue value = jdk.sandbox.StableValue.of(); + private final StableValue value = StableValue.of(); // Called by JsonString.of() factory. The passed String represents the // unescaped value. diff --git a/src/main/java/jdk/sandbox/internal/util/json/StableValue.java b/src/main/java/jdk/sandbox/internal/util/json/StableValue.java index c288eb3..626b65f 100644 --- a/src/main/java/jdk/sandbox/internal/util/json/StableValue.java +++ b/src/main/java/jdk/sandbox/internal/util/json/StableValue.java @@ -1,4 +1,4 @@ -package jdk.sandbox; +package jdk.sandbox.internal.util.json; import java.util.function.Supplier; @@ -6,35 +6,21 @@ * Mimics JDK's StableValue using double-checked locking pattern * for thread-safe lazy initialization. */ -public class StableValue { +class StableValue { private volatile T value; private final Object lock = new Object(); - + private StableValue() {} - + public static StableValue of() { return new StableValue<>(); } - - public T get() { - return value; - } - - public void set(T value) { - if (this.value == null) { - synchronized (lock) { - if (this.value == null) { - this.value = value; - } - } - } - } - - public T orElse(T defaultValue) { + + public T orElse(T defaultValue) { T result = value; return result != null ? result : defaultValue; } - + public T orElseSet(Supplier supplier) { T result = value; if (result == null) { @@ -47,7 +33,7 @@ public T orElseSet(Supplier supplier) { } return result; } - + public void setOrThrow(T newValue) { if (value != null) { throw new IllegalStateException("Value already set"); @@ -59,25 +45,25 @@ public void setOrThrow(T newValue) { value = newValue; } } - + public static Supplier supplier(Supplier supplier) { - return new Supplier() { - private volatile T cached; - private final Object supplierLock = new Object(); - - @Override - public T get() { - T result = cached; + return new Supplier<>() { + private volatile T cached; + private final Object supplierLock = new Object(); + + @Override + public T get() { + T result = cached; + if (result == null) { + synchronized (supplierLock) { + result = cached; if (result == null) { - synchronized (supplierLock) { - result = cached; - if (result == null) { - cached = result = supplier.get(); - } - } + cached = result = supplier.get(); } - return result; + } } + return result; + } }; } } \ No newline at end of file diff --git a/src/main/java/jdk/sandbox/java/util/json/Json.java b/src/main/java/jdk/sandbox/java/util/json/Json.java index c1d6575..56c47fb 100644 --- a/src/main/java/jdk/sandbox/java/util/json/Json.java +++ b/src/main/java/jdk/sandbox/java/util/json/Json.java @@ -48,8 +48,8 @@ *

* {@link #fromUntyped(Object)} and {@link #toUntyped(JsonValue)} provide a conversion * between {@code JsonValue} and an untyped object. - * - * @spec https://datatracker.ietf.org/doc/html/rfc8259 RFC 8259: The JavaScript + *

+ * {@code @spec} ... RFC 8259: The JavaScript * Object Notation (JSON) Data Interchange Format * @since 99 */ diff --git a/src/test/java/jdk/sandbox/java/util/json/JsonTypedUntypedTests.java b/src/test/java/jdk/sandbox/java/util/json/JsonTypedUntypedTests.java new file mode 100644 index 0000000..6beb7a2 --- /dev/null +++ b/src/test/java/jdk/sandbox/java/util/json/JsonTypedUntypedTests.java @@ -0,0 +1,230 @@ +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(42); + + // 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(42); + 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(30); + 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().get(0)).toNumber()).isEqualTo(85); + } + + @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); + 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; + 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; + + Map user = (Map) resultMap.get("user"); + assertThat(user.get("name")).isEqualTo("John Doe"); + assertThat(user.get("age")).isEqualTo(30L); + + List scores = (List) resultMap.get("scores"); + assertThat(scores).containsExactly(85.5, 92.0, 78.3); + + Map metadata = (Map) resultMap.get("metadata"); + 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 + Map map = (Map) result; + assertThat(map).containsEntry("z", "last") + .containsEntry("a", "first") + .containsEntry("m", "middle"); + } +} \ No newline at end of file From e05a6892a1f23ae73f5c228e7d3bb8295100f1a4 Mon Sep 17 00:00:00 2001 From: Simon Massey Date: Fri, 25 Jul 2025 09:08:16 +0100 Subject: [PATCH 3/4] Fix JsonTypedUntypedTests compilation and test failures - Fixed generic type issues with List by using @SuppressWarnings and proper casting - Corrected numeric type expectations (Integer boxed to Long in JsonNumber) - All 14 tests now pass successfully --- .../java/util/json/JsonTypedUntypedTests.java | 20 +++++++++++-------- 1 file changed, 12 insertions(+), 8 deletions(-) diff --git a/src/test/java/jdk/sandbox/java/util/json/JsonTypedUntypedTests.java b/src/test/java/jdk/sandbox/java/util/json/JsonTypedUntypedTests.java index 6beb7a2..6ec623b 100644 --- a/src/test/java/jdk/sandbox/java/util/json/JsonTypedUntypedTests.java +++ b/src/test/java/jdk/sandbox/java/util/json/JsonTypedUntypedTests.java @@ -22,7 +22,7 @@ void testFromUntypedWithSimpleTypes() { // Test integer JsonValue jsonInt = Json.fromUntyped(42); assertThat(jsonInt).isInstanceOf(JsonNumber.class); - assertThat(((JsonNumber) jsonInt).toNumber()).isEqualTo(42); + assertThat(((JsonNumber) jsonInt).toNumber()).isEqualTo(42L); // Test long JsonValue jsonLong = Json.fromUntyped(42L); @@ -68,7 +68,7 @@ void testFromUntypedWithCollections() { 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(42); + assertThat(((JsonNumber) array.values().get(1)).toNumber()).isEqualTo(42L); assertThat(((JsonBoolean) array.values().get(2)).value()).isTrue(); // Test Map @@ -77,7 +77,7 @@ void testFromUntypedWithCollections() { 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(30); + assertThat(((JsonNumber) obj.members().get("age")).toNumber()).isEqualTo(30L); assertThat(((JsonBoolean) obj.members().get("active")).value()).isTrue(); } @@ -98,7 +98,7 @@ void testFromUntypedWithNestedStructures() { JsonArray scores = (JsonArray) root.members().get("scores"); assertThat(scores.values()).hasSize(3); - assertThat(((JsonNumber) scores.values().get(0)).toNumber()).isEqualTo(85); + assertThat(((JsonNumber) scores.values().get(0)).toNumber()).isEqualTo(85L); } @Test @@ -155,7 +155,8 @@ void testToUntypedWithCollections() { )); Object result = Json.toUntyped(array); assertThat(result).isInstanceOf(List.class); - List list = (List) result; + @SuppressWarnings("unchecked") + List list = (List) result; assertThat(list).containsExactly("item1", 42L, true); // Test object @@ -201,11 +202,13 @@ void testRoundTripConversion() { assertThat(user.get("name")).isEqualTo("John Doe"); assertThat(user.get("age")).isEqualTo(30L); - List scores = (List) resultMap.get("scores"); + @SuppressWarnings("unchecked") + List scores = (List) resultMap.get("scores"); assertThat(scores).containsExactly(85.5, 92.0, 78.3); Map metadata = (Map) resultMap.get("metadata"); - List tags = (List) metadata.get("tags"); + @SuppressWarnings("unchecked") + List tags = (List) metadata.get("tags"); assertThat(tags).containsExactly("vip", "premium"); } @@ -222,7 +225,8 @@ void testToUntypedPreservesOrder() { assertThat(result).isInstanceOf(Map.class); // The order might not be preserved with Map.of(), so let's just verify contents - Map map = (Map) result; + @SuppressWarnings("unchecked") + Map map = (Map) result; assertThat(map).containsEntry("z", "last") .containsEntry("a", "first") .containsEntry("m", "middle"); From ac642016e1359399fec4df741f2bc78aeb056b05 Mon Sep 17 00:00:00 2001 From: Simon Massey Date: Fri, 25 Jul 2025 09:11:11 +0100 Subject: [PATCH 4/4] Update GitHub Actions to trigger on PR events - Added explicit PR event types: opened, synchronize, reopened - Ensures checks run when PRs are created or updated --- .github/workflows/maven.yml | 1 + 1 file changed, 1 insertion(+) diff --git a/.github/workflows/maven.yml b/.github/workflows/maven.yml index 791a4d6..671b5ba 100644 --- a/.github/workflows/maven.yml +++ b/.github/workflows/maven.yml @@ -5,6 +5,7 @@ on: branches: [ "main" ] pull_request: branches: [ "main" ] + types: [opened, synchronize, reopened] jobs: build: