diff --git a/README.md b/README.md index 67d80f8..bd01750 100644 --- a/README.md +++ b/README.md @@ -1,13 +1,30 @@ -# JSON Experimental - JDK 21+ Backport +# java.util.json Backport for JDK 21+ -This repository contains a backport of the experimental JSON API from the [jdk-sandbox project](https://github.com/openjdk/jdk-sandbox) to JDK 21 and later. +Early access to the future `java.util.json` API - tracking OpenJDK sandbox development. -## Origin +## Project Vision -This code is derived from the official OpenJDK sandbox repository at commit [d22dc2ba89789041c3908cdaafadc1dcf8882ebf](https://github.com/openjdk/jdk-sandbox/commit/d22dc2ba89789041c3908cdaafadc1dcf8882ebf) ("Improve hash code spec wording"). +This project provides Java developers with early access to the future `java.util.json` API patterns today, allowing code written against this API to migrate seamlessly when the official API is released. Rather than adopting third-party JSON libraries that will never align with future JDK standards, developers can start using tomorrow's API patterns today. + +## Current Status + +This code is derived from the official OpenJDK sandbox repository at commit [d22dc2ba89789041c3908cdaafadc1dcf8882ebf](https://github.com/openjdk/jdk-sandbox/commit/d22dc2ba89789041c3908cdaafadc1dcf8882ebf) (3 days ago - "Improve hash code spec wording"). 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) +## Project Goals + +- **Enable early adoption**: Let developers use future Java JSON patterns today on JDK 21+ +- **Smooth migration path**: Code written against this API should require minimal changes when migrating to the official release +- **API compatibility over performance**: Focus on matching the API design rather than competing with existing JSON libraries on speed + +## Non-Goals + +- **Performance competition**: This is not intended to be the fastest JSON library +- **Feature additions**: No features beyond what's in the official sandbox/preview +- **Production optimization**: The official implementation will have JVM-level optimizations unavailable to a backport +- **API stability**: This backport may evolve as the official specification develops (if folks find it useful) + ## Modifications This is a simplified backport with the following changes from the original: @@ -79,4 +96,128 @@ The conversion mappings are: 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 +- Working with frameworks that use reflection on standard types + +## Usage Examples + +### Record Mapping + +The most powerful feature is mapping between Java records and JSON: + +```java +// Domain model using records +record User(String name, String email, boolean active) {} +record Team(String teamName, List members) {} + +// Create a team with users +Team team = new Team("Engineering", List.of( + new User("Alice", "alice@example.com", true), + new User("Bob", "bob@example.com", false) +)); + +// 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() +)); + +// 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() + .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() +); +``` + +### Building Complex JSON + +Create structured JSON programmatically: + +```java +// Building a 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()) +)); +``` + +### Stream Processing + +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() + .map(v -> (JsonObject) v) + .filter(obj -> ((JsonBoolean) obj.members().get("active")).value()) + .map(obj -> ((JsonString) obj.members().get("email")).value()) + .toList(); +``` + +### Error Handling + +Handle parsing errors gracefully: + +```java +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()); +} +``` + +### Pretty Printing + +Format JSON for display: + +```java +JsonObject data = JsonObject.of(Map.of( + "name", JsonString.of("Alice"), + "scores", JsonArray.of(List.of( + JsonNumber.of(85), + JsonNumber.of(90), + JsonNumber.of(95) + )) +)); + +String formatted = Json.toDisplayString(data, 2); +// Output: +// { +// "name": "Alice", +// "scores": [ +// 85, +// 90, +// 95 +// ] +// } +``` \ No newline at end of file diff --git a/pom.xml b/pom.xml index 2e7e23b..882be44 100644 --- a/pom.xml +++ b/pom.xml @@ -7,8 +7,8 @@ 1.0-SNAPSHOT jar - JSON Experimental - Experimental JSON API and internal dependencies + java.util.json Backport for JDK 21+ + Early access to future java.util.json API - tracking OpenJDK sandbox development 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 56c47fb..0594914 100644 --- a/src/main/java/jdk/sandbox/java/util/json/Json.java +++ b/src/main/java/jdk/sandbox/java/util/json/Json.java @@ -37,124 +37,123 @@ 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}. - *

- * {@link #parse(String)} and {@link #parse(char[])} produce a {@code JsonValue} - * by parsing data adhering to the JSON syntax defined in RFC 8259. - *

- * {@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 {@code JsonValue} and an untyped object. - *

- * {@code @spec} ... RFC 8259: The JavaScript - * Object Notation (JSON) Data Interchange Format - * @since 99 - */ +/// This class provides static methods for producing and manipulating a {@link JsonValue}. +/// +/// {@link #parse(String)} and {@link #parse(char[])} produce a `JsonValue` +/// by parsing data adhering to the JSON syntax defined in RFC 8259. +/// +/// {@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)); +/// ``` +/// +/// @spec https://datatracker.ietf.org/doc/html/rfc8259 RFC 8259: The JavaScript +/// Object Notation (JSON) Data Interchange Format +/// @since 99 public final class Json { - /** - * Parses and creates a {@code JsonValue} from the given JSON document. - * If parsing succeeds, it guarantees that the input document conforms to - * the JSON syntax. If the document contains any JSON Object that has - * duplicate names, a {@code JsonParseException} is thrown. - *

- * {@code JsonValue}s created by this method produce their String and underlying - * value representation lazily. - *

- * {@code JsonObject}s preserve the order of their members declared in and parsed from - * the JSON document. - * - * @param in the input JSON document as {@code String}. Non-null. - * @throws JsonParseException if the input JSON document does not conform - * to the JSON document format or a JSON object containing - * duplicate names is encountered. - * @throws NullPointerException if {@code in} is {@code null} - * @return the parsed {@code JsonValue} - */ + /// Parses and creates a `JsonValue` from the given JSON document. + /// If parsing succeeds, it guarantees that the input document conforms to + /// the JSON syntax. If the document contains any JSON Object that has + /// duplicate names, a `JsonParseException` is thrown. + /// + /// `JsonValue`s created by this method produce their String and underlying + /// value representation lazily. + /// + /// `JsonObject`s preserve the order of their members declared in and parsed from + /// the JSON document. + /// + /// ## Example + /// ```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(); + /// } + /// ``` + /// + /// @param in the input JSON document as `String`. Non-null. + /// @throws JsonParseException if the input JSON document does not conform + /// to the JSON document format or a JSON object containing + /// duplicate names is encountered. + /// @throws NullPointerException if `in` is `null` + /// @return the parsed `JsonValue` public static JsonValue parse(String in) { Objects.requireNonNull(in); return new JsonParser(in.toCharArray()).parseRoot(); } - /** - * Parses and creates a {@code JsonValue} from the given JSON document. - * If parsing succeeds, it guarantees that the input document conforms to - * the JSON syntax. If the document contains any JSON Object that has - * duplicate names, a {@code JsonParseException} is thrown. - *

- * {@code JsonValue}s created by this method produce their String and underlying - * value representation lazily. - *

- * {@code JsonObject}s preserve the order of their members declared in and parsed from - * the JSON document. - * - * @param in the input JSON document as {@code char[]}. Non-null. - * @throws JsonParseException if the input JSON document does not conform - * to the JSON document format or a JSON object containing - * duplicate names is encountered. - * @throws NullPointerException if {@code in} is {@code null} - * @return the parsed {@code JsonValue} - */ + /// Parses and creates a `JsonValue` from the given JSON document. + /// If parsing succeeds, it guarantees that the input document conforms to + /// the JSON syntax. If the document contains any JSON Object that has + /// duplicate names, a `JsonParseException` is thrown. + /// + /// `JsonValue`s created by this method produce their String and underlying + /// value representation lazily. + /// + /// `JsonObject`s preserve the order of their members declared in and parsed from + /// the JSON document. + /// + /// @param in the input JSON document as `char[]`. Non-null. + /// @throws JsonParseException if the input JSON document does not conform + /// to the JSON document format or a JSON object containing + /// duplicate names is encountered. + /// @throws NullPointerException if `in` is `null` + /// @return the parsed `JsonValue` public static JsonValue parse(char[] in) { Objects.requireNonNull(in); // Defensive copy on input. Ensure source is immutable. return new JsonParser(Arrays.copyOf(in, in.length)).parseRoot(); } - /** - * {@return a {@code JsonValue} created from the given {@code src} object} - * The mapping from an untyped {@code src} object to a {@code JsonValue} - * follows the table below. - * - * - * - * - * - * - * - * - * - * - * - * - * - * - * - * - * - * - * - * - * - * - * - * - * - * - * - * - * - * - * - * - * - * - *
Untyped to JsonValue mapping
Untyped ObjectJsonValue
{@code List}{@code JsonArray}
{@code Boolean}{@code JsonBoolean}
{@code `null`}{@code JsonNull}
{@code Number*}{@code JsonNumber}
{@code Map}{@code JsonObject}
{@code String}{@code JsonString}
- * - * *The supported {@code Number} subclasses are: {@code Byte}, - * {@code Short}, {@code Integer}, {@code Long}, {@code Float}, - * {@code Double}, {@code BigInteger}, and {@code BigDecimal}. - * - *

If {@code src} is an instance of {@code JsonValue}, it is returned as is. - * - * @param src the data to produce the {@code JsonValue} from. May be null. - * @throws IllegalArgumentException if {@code src} cannot be converted - * to a {@code JsonValue}. - * @see #toUntyped(JsonValue) - */ + /// {@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 @@ -198,54 +197,31 @@ public static JsonValue fromUntyped(Object src) { }; } - /** - * {@return an {@code Object} created from the given {@code src} - * {@code JsonValue}} The mapping from a {@code JsonValue} to an - * untyped {@code src} object follows the table below. - * - * - * - * - * - * - * - * - * - * - * - * - * - * - * - * - * - * - * - * - * - * - * - * - * - * - * - * - * - * - * - * - * - * - *
JsonValue to Untyped mapping
JsonValueUntyped Object
{@code JsonArray}{@code List}(unmodifiable)
{@code JsonBoolean}{@code Boolean}
{@code JsonNull}{@code `null`}
{@code JsonNumber}{@code Number}
{@code JsonObject}{@code Map}(unmodifiable)
{@code JsonString}{@code String}
- * - *

- * A {@code JsonObject} in {@code src} is converted to a {@code Map} whose - * entries occur in the same order as the {@code JsonObject}'s members. - * - * @param src the {@code JsonValue} to convert to untyped. Non-null. - * @throws NullPointerException if {@code src} is {@code null} - * @see #fromUntyped(Object) - */ + /// {@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) { @@ -263,18 +239,31 @@ public static Object toUntyped(JsonValue src) { }; } - /** - * {@return the String representation of the given {@code 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 - * suited for display. - * - * @param value the {@code JsonValue} to create the display string from. Non-null. - * @param indent the number of spaces used for the indentation. Zero or positive. - * @throws NullPointerException if {@code value} is {@code null} - * @throws IllegalArgumentException if {@code indent} is a negative number - * @see JsonValue#toString() - */ + /// {@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 + /// suited for display. + /// + /// ## Example + /// ```java + /// JsonValue json = Json.parse("{\"name\":\"Alice\",\"scores\":[85,90,95]}"); + /// System.out.println(Json.toDisplayString(json, 2)); + /// // Output: + /// // { + /// // "name": "Alice", + /// // "scores": [ + /// // 85, + /// // 90, + /// // 95 + /// // ] + /// // } + /// ``` + /// + /// @param value the `JsonValue` to create the display string from. Non-null. + /// @param indent the number of spaces used for the indentation. Zero or positive. + /// @throws NullPointerException if `value` is `null` + /// @throws IllegalArgumentException if `indent` is a negative number + /// @see JsonValue#toString() public static String toDisplayString(JsonValue value, int indent) { Objects.requireNonNull(value); if (indent < 0) { diff --git a/src/main/java/jdk/sandbox/java/util/json/JsonArray.java b/src/main/java/jdk/sandbox/java/util/json/JsonArray.java index 120e61a..7ed3d71 100644 --- a/src/main/java/jdk/sandbox/java/util/json/JsonArray.java +++ b/src/main/java/jdk/sandbox/java/util/json/JsonArray.java @@ -32,30 +32,44 @@ import jdk.sandbox.internal.util.json.JsonArrayImpl; -/** - * The interface that represents JSON array. - *

- * A {@code JsonArray} can be produced by {@link Json#parse(String)}. - *

Alternatively, {@link #of(List)} can be used to obtain a {@code JsonArray}. - * - * @since 99 - */ +/// The interface that represents JSON array. +/// +/// A `JsonArray` can be produced by {@link Json#parse(String)}. +/// Alternatively, {@link #of(List)} can be used to obtain a `JsonArray`. +/// +/// ## Example Usage +/// ```java +/// // Create from a List +/// JsonArray arr = JsonArray.of(List.of( +/// JsonString.of("first"), +/// JsonNumber.of(42), +/// JsonBoolean.of(true) +/// )); +/// +/// // Access elements +/// for (JsonValue value : arr.values()) { +/// 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()); +/// default -> System.out.println("Other: " + value); +/// } +/// } +/// ``` +/// +/// @since 99 public non-sealed interface JsonArray extends JsonValue { - /** - * {@return an unmodifiable list of the {@code JsonValue} elements in - * this {@code JsonArray}} - */ + /// {@return an unmodifiable list of the `JsonValue` elements in + /// this `JsonArray`} List values(); - /** - * {@return the {@code JsonArray} created from the given - * list of {@code JsonValue}s} - * - * @param src the list of {@code JsonValue}s. Non-null. - * @throws NullPointerException if {@code src} is {@code null}, or contains - * any values that are {@code null} - */ + /// {@return the `JsonArray` created from the given + /// list of `JsonValue`s} + /// + /// @param src the list of `JsonValue`s. Non-null. + /// @throws NullPointerException if `src` is `null`, or contains + /// any values that are `null` static JsonArray of(List src) { // Careful not to use List::contains on src for null checking which // throws NPE for immutable lists @@ -66,27 +80,23 @@ static JsonArray of(List src) { ); } - /** - * {@return {@code true} if the given object is also a {@code JsonArray} - * and the two {@code JsonArray}s represent the same elements} Two - * {@code JsonArray}s {@code ja1} and {@code ja2} represent the same - * elements if {@code ja1.values().equals(ja2.values())}. - * - * @see #values() - */ + /// {@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())`. + /// + /// @see #values() @Override boolean equals(Object obj); - /** - * {@return the hash code value for this {@code JsonArray}} The hash code value - * of a {@code JsonArray} is derived from the hash code of {@code JsonArray}'s - * {@link #values()}. - * Thus, for two {@code JsonArray}s {@code ja1} and {@code ja2}, - * {@code ja1.equals(ja2)} implies that {@code ja1.hashCode() == ja2.hashCode()} - * as required by the general contract of {@link Object#hashCode}. - * - * @see #values() - */ + /// {@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()}. + /// 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() @Override int hashCode(); } diff --git a/src/main/java/jdk/sandbox/java/util/json/JsonObject.java b/src/main/java/jdk/sandbox/java/util/json/JsonObject.java index 9d7d1ce..b27385f 100644 --- a/src/main/java/jdk/sandbox/java/util/json/JsonObject.java +++ b/src/main/java/jdk/sandbox/java/util/json/JsonObject.java @@ -32,36 +32,44 @@ import jdk.sandbox.internal.util.json.JsonObjectImpl; -/** - * The interface that represents JSON object. - *

- * A {@code JsonObject} can be produced by a {@link Json#parse(String)}. - *

Alternatively, {@link #of(Map)} can be used to obtain a {@code JsonObject}. - * Implementations of {@code JsonObject} cannot be created from sources that - * contain duplicate member names. If duplicate names appear during - * a {@link Json#parse(String)}, a {@code JsonParseException} is thrown. - * - * @since 99 - */ +/// The interface that represents JSON object. +/// +/// A `JsonObject` can be produced by a {@link Json#parse(String)}. +/// Alternatively, {@link #of(Map)} can be used to obtain a `JsonObject`. +/// Implementations of `JsonObject` cannot be created from sources that +/// contain duplicate member names. If duplicate names appear during +/// a {@link Json#parse(String)}, a `JsonParseException` is thrown. +/// +/// ## Example Usage +/// ```java +/// // Create from a Map +/// JsonObject obj = JsonObject.of(Map.of( +/// "name", JsonString.of("Alice"), +/// "age", JsonNumber.of(30), +/// "active", JsonBoolean.of(true) +/// )); +/// +/// // Access members +/// JsonString name = (JsonString) obj.members().get("name"); +/// System.out.println(name.value()); // "Alice" +/// ``` +/// +/// @since 99 public non-sealed interface JsonObject extends JsonValue { - /** - * {@return an unmodifiable map of the {@code String} to {@code JsonValue} - * members in this {@code JsonObject}} - */ + /// {@return an unmodifiable map of the `String` to `JsonValue` + /// members in this `JsonObject`} Map members(); - /** - * {@return the {@code JsonObject} created from the given - * map of {@code String} to {@code JsonValue}s} - * - * The {@code JsonObject}'s members occur in the same order as the given - * map's entries. - * - * @param map the map of {@code JsonValue}s. Non-null. - * @throws NullPointerException if {@code map} is {@code null}, contains - * any keys that are {@code null}, or contains any values that are {@code null}. - */ + /// {@return the `JsonObject` created from the given + /// map of `String` to `JsonValue`s} + /// + /// The `JsonObject`'s members occur in the same order as the given + /// map's entries. + /// + /// @param map the map of `JsonValue`s. Non-null. + /// @throws NullPointerException if `map` is `null`, contains + /// any keys that are `null`, or contains any values that are `null`. static JsonObject of(Map map) { return new JsonObjectImpl(map.entrySet() // Implicit NPE on map .stream() @@ -70,26 +78,22 @@ static JsonObject of(Map map) { (ignored, v) -> v, LinkedHashMap::new))); } - /** - * {@return {@code true} if the given object is also a {@code JsonObject} - * and the two {@code JsonObject}s represent the same mappings} Two - * {@code JsonObject}s {@code jo1} and {@code jo2} represent the same - * mappings if {@code jo1.members().equals(jo2.members())}. - * - * @see #members() - */ + /// {@return `true` if the given object is also a `JsonObject` + /// and the two `JsonObject`s represent the same mappings} Two + /// `JsonObject`s `jo1` and `jo2` represent the same + /// mappings if `jo1.members().equals(jo2.members())`. + /// + /// @see #members() @Override boolean equals(Object obj); - /** - * {@return the hash code value for this {@code JsonObject}} The hash code value - * of a {@code JsonObject} is derived from the hash code of {@code JsonObject}'s - * {@link #members()}. Thus, for two {@code JsonObject}s {@code jo1} and {@code jo2}, - * {@code jo1.equals(jo2)} implies that {@code jo1.hashCode() == jo2.hashCode()} - * as required by the general contract of {@link Object#hashCode}. - * - * @see #members() - */ + /// {@return the hash code value for this `JsonObject`} The hash code value + /// of a `JsonObject` is derived from the hash code of `JsonObject`'s + /// {@link #members()}. Thus, for two `JsonObject`s `jo1` and `jo2`, + /// `jo1.equals(jo2)` implies that `jo1.hashCode() == jo2.hashCode()` + /// as required by the general contract of {@link Object#hashCode}. + /// + /// @see #members() @Override int hashCode(); } diff --git a/src/main/java/jdk/sandbox/java/util/json/JsonValue.java b/src/main/java/jdk/sandbox/java/util/json/JsonValue.java index e02c60e..b7659f4 100644 --- a/src/main/java/jdk/sandbox/java/util/json/JsonValue.java +++ b/src/main/java/jdk/sandbox/java/util/json/JsonValue.java @@ -26,65 +26,72 @@ package jdk.sandbox.java.util.json; -/** - * The interface that represents a JSON value. - *

- * Instances of {@code JsonValue} are immutable and thread safe. - *

- * A {@code JsonValue} can be produced by {@link Json#parse(String)} or {@link - * Json#fromUntyped(Object)}. See {@link #toString()} for converting a {@code - * JsonValue} to its corresponding JSON String. For example, - * {@snippet lang=java: - * List values = Arrays.asList("foo", true, 25); - * JsonValue json = Json.fromUntyped(values); - * json.toString(); // returns "[\"foo\",true,25]" - * } - * - * A class implementing a non-sealed {@code JsonValue} sub-interface must adhere - * to the following: - *
    - *
  • The class's implementations of {@code equals}, {@code hashCode}, - * and {@code 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 {@code x} and - * {@code y} that are equal according to {@code 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 {@code equals()}, they may also be - * equal according to {@code ==}.
  • - *
- *

- * Users of {@code JsonValue} instances should ensure the following: - *

    - *
  • When two instances of {@code JsonValue} are equal (according to {@code 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 {@code JsonValue} is strongly discouraged, - * because the programmer cannot guarantee exclusive ownership of the - * associated monitor.
  • - *
- * - * @since 99 - */ +/// 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 `==`. +/// +/// 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. +/// +/// @since 99 public sealed interface JsonValue permits JsonString, JsonNumber, JsonObject, JsonArray, JsonBoolean, JsonNull { - /** - * {@return the String representation of this {@code JsonValue} that conforms - * to the JSON syntax} If this {@code 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)}. - * - * @see Json#toDisplayString(JsonValue, int) - */ + /// {@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)}. + /// + /// @see Json#toDisplayString(JsonValue, int) String toString(); } diff --git a/src/main/java/jdk/sandbox/java/util/json/package-info.java b/src/main/java/jdk/sandbox/java/util/json/package-info.java index 6b86383..0cb20b5 100644 --- a/src/main/java/jdk/sandbox/java/util/json/package-info.java +++ b/src/main/java/jdk/sandbox/java/util/json/package-info.java @@ -23,61 +23,168 @@ * questions. */ -/** - * Provides APIs for parsing JSON text, creating {@code JsonValue}s, and - * offering a mapping between a {@code 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 {@code JsonValue} sum type, which can be - * pattern-matched into one of the following product types: {@code JsonObject}, - * {@code JsonArray}, {@code JsonString}, {@code JsonNumber}, {@code JsonBoolean}, - * {@code JsonNull}. These product types are defined as non-sealed interfaces that - * allow flexibility in the implementation of the type. For example, {@code JsonArray} - * is defined as follows: - *
{@code 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: - * {@snippet lang=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 {@code JsonValue} instances and their underlying values are immutable. - * - *

Parsing

- * - * Parsing produces a {@code 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. - * The parsing APIs provided do not accept JSON text that contain JSON Objects - * with duplicate names. - * - *

For the reference JDK implementation, {@code JsonValue}s created via parsing - * procure their underlying values lazily. - * - *

Formatting

- * - * Formatting of a {@code JsonValue} is performed with either {@link - * JsonValue#toString()} or {@link Json#toDisplayString(JsonValue, int)}. - * These methods produce formatted String representations of a {@code JsonValue}. - * The returned text adheres to the JSON grammar defined in RFC 8259. - * {@code JsonValue.toString()} produces the most compact representation which does not - * include extra whitespaces or line-breaks, preferable for network transaction - * or storage. {@code Json.toDisplayString(JsonValue, int)} produces a text representation that - * is human friendly, preferable for debugging or logging. - * - * @spec https://datatracker.ietf.org/doc/html/rfc8259 RFC 8259: The JavaScript - * Object Notation (JSON) Data Interchange Format - * @since 99 - */ +/// 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: +/// ```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); +/// ``` +/// +/// @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/src/test/java/jdk/sandbox/java/util/json/ReadmeDemoTests.java b/src/test/java/jdk/sandbox/java/util/json/ReadmeDemoTests.java new file mode 100644 index 0000000..6764758 --- /dev/null +++ b/src/test/java/jdk/sandbox/java/util/json/ReadmeDemoTests.java @@ -0,0 +1,221 @@ +package jdk.sandbox.java.util.json; + +import org.junit.jupiter.api.Test; + +import java.util.List; +import java.util.Map; +import java.util.stream.Collectors; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatThrownBy; + +public class ReadmeDemoTests { + + @Test + void quickStartExample() { + // Basic parsing example + String jsonString = "{\"name\":\"Alice\",\"age\":30}"; + JsonValue value = Json.parse(jsonString); + + 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); + } + + // Domain model using records + record User(String name, String email, boolean active) {} + record Team(String teamName, List members) {} + + @Test + void recordMappingExample() { + // Create a team with users + Team team = new Team("Engineering", List.of( + new User("Alice", "alice@example.com", true), + 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() + )); + + // Verify the JSON structure + assertThat(teamJson).isInstanceOf(JsonObject.class); + JsonObject teamObj = (JsonObject) teamJson; + assertThat(((JsonString) teamObj.members().get("teamName")).value()).isEqualTo("Engineering"); + + JsonArray members = (JsonArray) teamObj.members().get("members"); + assertThat(members.values()).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() + .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() + ); + + // 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 + void builderPatternExample() { + // Building a 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(1234567890L) + )), + "errors", JsonArray.of(List.of()) + )); + + // Verify structure + assertThat(((JsonString) response.members().get("status")).value()).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"); + + JsonArray roles = (JsonArray) user.members().get("roles"); + assertThat(roles.values()).hasSize(2); + assertThat(((JsonString) roles.values().get(0)).value()).isEqualTo("admin"); + + JsonArray errors = (JsonArray) response.members().get("errors"); + assertThat(errors.values()).isEmpty(); + } + + @Test + void streamingProcessingExample() { + // Create a large array of user records + String largeJsonArray = """ + [ + {"name": "Alice", "email": "alice@example.com", "active": true}, + {"name": "Bob", "email": "bob@example.com", "active": false}, + {"name": "Charlie", "email": "charlie@example.com", "active": true}, + {"name": "David", "email": "david@example.com", "active": false}, + {"name": "Eve", "email": "eve@example.com", "active": true} + ] + """; + + // Process a large array of records + JsonArray items = (JsonArray) Json.parse(largeJsonArray); + List activeUserEmails = items.values().stream() + .map(v -> (JsonObject) v) + .filter(obj -> ((JsonBoolean) obj.members().get("active")).value()) + .map(obj -> ((JsonString) obj.members().get("email")).value()) + .toList(); + + // Verify we got only active users + assertThat(activeUserEmails).containsExactly( + "alice@example.com", + "charlie@example.com", + "eve@example.com" + ); + } + + @Test + void errorHandlingExample() { + // Valid JSON parsing + String validJson = "{\"valid\": true}"; + JsonValue value = Json.parse(validJson); + assertThat(value).isInstanceOf(JsonObject.class); + + // Invalid JSON parsing + String invalidJson = "{invalid json}"; + assertThatThrownBy(() -> Json.parse(invalidJson)) + .isInstanceOf(JsonParseException.class) + .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 + @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() { + // Create a structured JSON + JsonObject data = JsonObject.of(Map.of( + "name", JsonString.of("Alice"), + "scores", JsonArray.of(List.of( + JsonNumber.of(85), + JsonNumber.of(90), + JsonNumber.of(95) + )) + )); + + // Format for display + String formatted = Json.toDisplayString(data, 2); + + // Verify it contains proper formatting (checking key parts) + assertThat(formatted).contains("{\n"); + assertThat(formatted).contains(" \"name\": \"Alice\""); + assertThat(formatted).contains(" \"scores\": ["); + assertThat(formatted).contains(" 85,"); + assertThat(formatted).contains(" 90,"); + assertThat(formatted).contains(" 95"); + assertThat(formatted).contains(" ]"); + assertThat(formatted).contains("}"); + } +} \ No newline at end of file