diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 7d28e48..2c0ff07 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -39,7 +39,7 @@ jobs: for k in totals: totals[k]+=int(r.get(k,'0')) except Exception: pass - exp_tests=466 + exp_tests=475 exp_skipped=0 if totals['tests']!=exp_tests or totals['skipped']!=exp_skipped: print(f"Unexpected test totals: {totals} != expected tests={exp_tests}, skipped={exp_skipped}") diff --git a/AGENTS.md b/AGENTS.md index 93b258f..702a828 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -14,6 +14,26 @@ - Never commit unverified mass changes—compile or test first. - Do not use Perl or sed for multi-line structural edits; rely on Python 3.2-friendly heredocs. +## Markdown-Driven-Development (MDD) +We practice **Markdown-Driven-Development** where documentation precedes implementation: + +1. **Create GitHub issue** with clear problem statement and goals +2. **Update user documentation** (README.md) with new behavior/semantics +3. **Update agentic documentation** (AGENTS.md) with implementation guidance +4. **Update specialist documentation** (**/*.md, e.g., ARCHITECTURE.md) as needed +5. **Create implementation plan** (PLAN_${issue_id}.md) documenting exact changes +6. **Implement code changes** to match documented behavior +7. **Update tests** to validate the documented behavior +8. **Verify all documentation** remains accurate after implementation + +This ensures: +- Users understand behavior changes before code is written +- Developers have clear implementation guidance +- Documentation stays synchronized with code +- Breaking changes are clearly communicated + +When making changes, always update documentation files before modifying code. + ## Testing & Logging Discipline @@ -222,11 +242,21 @@ The property test logs at FINEST level: ### Issue Management - Use the native tooling for the remote (for example `gh` for GitHub). - Create issues in the repository tied to the `origin` remote unless instructed otherwise; if another remote is required, ask for its name. -- Tickets and issues must state only “what” and “why,” leaving “how” for later discussion. +- Tickets and issues must state only "what" and "why," leaving "how" for later discussion. - Comments may discuss implementation details. - Label tickets as `Ready` once actionable; if a ticket lacks that label, request confirmation before proceeding. - Limit tidy-up issues to an absolute minimum (no more than two per PR). +### Creating GitHub Issues +- **Title requirements**: No issue numbers, no special characters, no quotes, no shell metacharacters +- **Body requirements**: Write issue body to a file first, then use --body-file flag +- **Example workflow**: + ```bash + echo "Issue description here" > /tmp/issue_body.md + gh issue create --title "Brief description of bug" --body-file /tmp/issue_body.md + ``` +- **Never use --body flag** with complex content - always use --body-file to avoid shell escaping issues + ### Commit Requirements - Commit messages start with `Issue # `. - Include a link to the referenced issue when possible. @@ -488,6 +518,16 @@ IMPORTANT: Never disable tests written for logic that we are yet to write we do * Virtual threads for concurrent processing * **Use try-with-resources for all AutoCloseable resources** (HttpClient, streams, etc.) +## RFC 8927 Compliance Guidelines + +* **{} must compile to the Empty form and accept any JSON value** (RFC 8927 §2.2) +* **Do not introduce compatibility modes that reinterpret {} with object semantics** +* **Specs from json-typedef-spec are authoritative for behavior and tests** +* **If a test, doc, or code disagrees with RFC 8927 about {}, the test/doc/code is wrong** +* **We log at INFO when {} is compiled to help users who come from non-JTD validators** + +Per RFC 8927 §3.3.1: "If a schema is of the 'empty' form, then it accepts all instances. A schema of the 'empty' form will never produce any error indicators." + ## Package Structure * Use default (package-private) access as the standard. Do not use 'private' or 'public' by default. diff --git a/README.md b/README.md index b9a5477..4f86841 100644 --- a/README.md +++ b/README.md @@ -295,6 +295,19 @@ This repo contains an incubating JTD validator that has the core JSON API as its A complete JSON Type Definition validator is included (module: json-java21-jtd). +### Empty Schema `{}` Semantics (RFC 8927) + +Per **RFC 8927 (JSON Typedef)**, the empty schema `{}` is the **empty form** and +**accepts all JSON instances** (null, boolean, numbers, strings, arrays, objects). + +> RFC 8927 §2.2 "Forms": +> `schema = empty / ref / type / enum / elements / properties / values / discriminator / definitions` +> `empty = {}` +> **Empty form:** A schema in the empty form accepts all JSON values and produces no errors. + +⚠️ Note: Some tools or in-house validators mistakenly interpret `{}` as "object with no +properties allowed." **That is not JTD.** This implementation follows RFC 8927 strictly. + ```java import json.java21.jtd.Jtd; import jdk.sandbox.java.util.json.*; diff --git a/json-java21-jtd/ARCHITECTURE.md b/json-java21-jtd/ARCHITECTURE.md index 4d17ad0..7b99718 100644 --- a/json-java21-jtd/ARCHITECTURE.md +++ b/json-java21-jtd/ARCHITECTURE.md @@ -288,6 +288,14 @@ $(command -v mvnd || command -v mvn || command -v ./mvnw) test -pl json-java21-j - **Definitions**: Validate all definitions exist at compile time - **Type Checking**: Strict RFC 8927 compliance for all primitive types +## Empty Schema `{}` + +- **Form**: `empty = {}` +- **Behavior**: **accepts all instances**; produces no validation errors. +- **RFC 8927 §3.3.1**: "If a schema is of the 'empty' form, then it accepts all instances. A schema of the 'empty' form will never produce any error indicators." +- **Common pitfall**: confusing JTD with non-JTD validators that treat `{}` as an empty-object schema. +- **Implementation**: compile `{}` to `EmptySchema` and validate everything as OK. + ## RFC 8927 Compliance This implementation strictly follows RFC 8927: diff --git a/json-java21-jtd/src/main/java/json/java21/jtd/Crumbs.java b/json-java21-jtd/src/main/java/json/java21/jtd/Crumbs.java new file mode 100644 index 0000000..eac13b4 --- /dev/null +++ b/json-java21-jtd/src/main/java/json/java21/jtd/Crumbs.java @@ -0,0 +1,16 @@ +package json.java21.jtd; + +/// Lightweight breadcrumb trail for human-readable error paths +record Crumbs(String value) { + static Crumbs root() { + return new Crumbs("#"); + } + + Crumbs withObjectField(String name) { + return new Crumbs(value + "→field:" + name); + } + + Crumbs withArrayIndex(int idx) { + return new Crumbs(value + "→item:" + idx); + } +} diff --git a/json-java21-jtd/src/main/java/json/java21/jtd/Frame.java b/json-java21-jtd/src/main/java/json/java21/jtd/Frame.java new file mode 100644 index 0000000..a99442c --- /dev/null +++ b/json-java21-jtd/src/main/java/json/java21/jtd/Frame.java @@ -0,0 +1,19 @@ +package json.java21.jtd; + +import jdk.sandbox.java.util.json.JsonValue; + +/// Stack frame for iterative validation with path and offset tracking +record Frame(JtdSchema schema, JsonValue instance, String ptr, Crumbs crumbs, String discriminatorKey) { + /// Constructor for normal validation without discriminator context + Frame(JtdSchema schema, JsonValue instance, String ptr, Crumbs crumbs) { + this(schema, instance, ptr, crumbs, null); + } + + @Override + public String toString() { + final var kind = schema.getClass().getSimpleName(); + final var tag = (schema instanceof JtdSchema.RefSchema r) ? "(ref=" + r.ref() + ")" : ""; + return "Frame[schema=" + kind + tag + ", instance=" + instance + ", ptr=" + ptr + + ", crumbs=" + crumbs + ", discriminatorKey=" + discriminatorKey + "]"; + } +} 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 e60bf5a..3286f7e 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 @@ -17,38 +17,7 @@ public class Jtd { /// Top-level definitions map for ref resolution private final Map definitions = new java.util.HashMap<>(); - - /// Stack frame for iterative validation with path and offset tracking - record Frame(JtdSchema schema, JsonValue instance, String ptr, Crumbs crumbs, String discriminatorKey) { - /// Constructor for normal validation without discriminator context - Frame(JtdSchema schema, JsonValue instance, String ptr, Crumbs crumbs) { - this(schema, instance, ptr, crumbs, null); - } - - @Override - public String toString() { - final var kind = schema.getClass().getSimpleName(); - final var tag = (schema instanceof JtdSchema.RefSchema r) ? "(ref=" + r.ref() + ")" : ""; - return "Frame[schema=" + kind + tag + ", instance=" + instance + ", ptr=" + ptr + - ", crumbs=" + crumbs + ", discriminatorKey=" + discriminatorKey + "]"; - } - } - - /// Lightweight breadcrumb trail for human-readable error paths - record Crumbs(String value) { - static Crumbs root() { - return new Crumbs("#"); - } - - Crumbs withObjectField(String name) { - return new Crumbs(value + "→field:" + name); - } - - Crumbs withArrayIndex(int idx) { - return new Crumbs(value + "→item:" + idx); - } - } - + /// Extracts offset from JsonValue implementation classes static int offsetOf(JsonValue v) { return switch (v) { @@ -65,8 +34,8 @@ static int offsetOf(JsonValue v) { /// Creates an enriched error message with offset and path information static String enrichedError(String baseMessage, Frame frame, JsonValue contextValue) { int off = offsetOf(contextValue); - String ptr = frame.ptr; - String via = frame.crumbs.value(); + String ptr = frame.ptr(); + String via = frame.crumbs().value(); return "[off=" + off + " ptr=" + ptr + " via=" + via + "] " + baseMessage; } @@ -106,25 +75,25 @@ Result validateWithStack(JtdSchema schema, JsonValue instance) { stack.push(rootFrame); LOG.fine(() -> "Starting stack validation - schema=" + - rootFrame.schema.getClass().getSimpleName() + - (rootFrame.schema instanceof JtdSchema.RefSchema r ? "(ref=" + r.ref() + ")" : "") + + rootFrame.schema().getClass().getSimpleName() + + (rootFrame.schema() instanceof JtdSchema.RefSchema r ? "(ref=" + r.ref() + ")" : "") + ", ptr=#"); // Process frames iteratively while (!stack.isEmpty()) { Frame frame = stack.pop(); - LOG.fine(() -> "Processing frame - schema: " + frame.schema.getClass().getSimpleName() + - (frame.schema instanceof JtdSchema.RefSchema r ? "(ref=" + r.ref() + ")" : "") + - ", ptr: " + frame.ptr + ", off: " + offsetOf(frame.instance)); + LOG.fine(() -> "Processing frame - schema: " + frame.schema().getClass().getSimpleName() + + (frame.schema() instanceof JtdSchema.RefSchema r ? "(ref=" + r.ref() + ")" : "") + + ", ptr: " + frame.ptr() + ", off: " + offsetOf(frame.instance())); // Validate current frame - if (!frame.schema.validateWithFrame(frame, errors, false)) { - LOG.fine(() -> "Validation failed for frame at " + frame.ptr + " with " + errors.size() + " errors"); + if (!frame.schema().validateWithFrame(frame, errors, false)) { + LOG.fine(() -> "Validation failed for frame at " + frame.ptr() + " with " + errors.size() + " errors"); continue; // Continue processing other frames even if this one failed } // Handle special validations for PropertiesSchema - if (frame.schema instanceof JtdSchema.PropertiesSchema propsSchema) { + if (frame.schema() instanceof JtdSchema.PropertiesSchema propsSchema) { validatePropertiesSchema(frame, propsSchema, errors); } @@ -163,7 +132,7 @@ void validatePropertiesSchema(Frame frame, JtdSchema.PropertiesSchema propsSchem for (String key : obj.members().keySet()) { if (!propsSchema.properties().containsKey(key) && !propsSchema.optionalProperties().containsKey(key)) { // Only exempt the discriminator field itself, not all additional properties - if (discriminatorKey != null && key.equals(discriminatorKey)) { + if (key.equals(discriminatorKey)) { continue; // Skip the discriminator field - it's exempt } JsonValue value = obj.members().get(key); @@ -179,8 +148,8 @@ void validatePropertiesSchema(Frame frame, JtdSchema.PropertiesSchema propsSchem /// Pushes child frames for complex schema types void pushChildFrames(Frame frame, java.util.Deque stack) { - JtdSchema schema = frame.schema; - JsonValue instance = frame.instance; + JtdSchema schema = frame.schema(); + JsonValue instance = frame.instance(); LOG.finer(() -> "Pushing child frames for schema type: " + schema.getClass().getSimpleName()); @@ -189,8 +158,8 @@ void pushChildFrames(Frame frame, java.util.Deque stack) { if (instance instanceof JsonArray arr) { int index = 0; for (JsonValue element : arr.values()) { - String childPtr = frame.ptr + "/" + index; - Crumbs childCrumbs = frame.crumbs.withArrayIndex(index); + String childPtr = frame.ptr() + "/" + index; + Crumbs childCrumbs = frame.crumbs().withArrayIndex(index); Frame childFrame = new Frame(elementsSchema.elements(), element, childPtr, childCrumbs); stack.push(childFrame); LOG.finer(() -> "Pushed array element frame at " + childPtr); @@ -200,34 +169,49 @@ void pushChildFrames(Frame frame, java.util.Deque stack) { } case JtdSchema.PropertiesSchema propsSchema -> { if (instance instanceof JsonObject obj) { - // Push required properties that are present + String discriminatorKey = frame.discriminatorKey(); + for (var entry : propsSchema.properties().entrySet()) { String key = entry.getKey(); + + // Skip the discriminator field - it was already validated by discriminator logic + if (key.equals(discriminatorKey)) { + LOG.finer(() -> "Skipping discriminator field validation for: " + key); + continue; + } + JsonValue value = obj.members().get(key); - + if (value != null) { - String childPtr = frame.ptr + "/" + key; - Crumbs childCrumbs = frame.crumbs.withObjectField(key); + String childPtr = frame.ptr() + "/" + key; + Crumbs childCrumbs = frame.crumbs().withObjectField(key); Frame childFrame = new Frame(entry.getValue(), value, childPtr, childCrumbs); stack.push(childFrame); LOG.finer(() -> "Pushed required property frame at " + childPtr); } } - - // Push optional properties that are present + for (var entry : propsSchema.optionalProperties().entrySet()) { String key = entry.getKey(); + + // Skip the discriminator field - it was already validated by discriminator logic + if (key.equals(discriminatorKey)) { + LOG.finer(() -> "Skipping discriminator field validation for optional: " + key); + continue; + } + JtdSchema childSchema = entry.getValue(); JsonValue value = obj.members().get(key); - + if (value != null) { - String childPtr = frame.ptr + "/" + key; - Crumbs childCrumbs = frame.crumbs.withObjectField(key); + String childPtr = frame.ptr() + "/" + key; + Crumbs childCrumbs = frame.crumbs().withObjectField(key); Frame childFrame = new Frame(childSchema, value, childPtr, childCrumbs); stack.push(childFrame); LOG.finer(() -> "Pushed optional property frame at " + childPtr); } } + } } case JtdSchema.ValuesSchema valuesSchema -> { @@ -235,8 +219,8 @@ void pushChildFrames(Frame frame, java.util.Deque stack) { for (var entry : obj.members().entrySet()) { String key = entry.getKey(); JsonValue value = entry.getValue(); - String childPtr = frame.ptr + "/" + key; - Crumbs childCrumbs = frame.crumbs.withObjectField(key); + String childPtr = frame.ptr() + "/" + key; + Crumbs childCrumbs = frame.crumbs().withObjectField(key); Frame childFrame = new Frame(valuesSchema.values(), value, childPtr, childCrumbs); stack.push(childFrame); LOG.finer(() -> "Pushed values schema frame at " + childPtr); @@ -250,15 +234,10 @@ void pushChildFrames(Frame frame, java.util.Deque stack) { String discriminatorValueStr = discStr.value(); JtdSchema variantSchema = discSchema.mapping().get(discriminatorValueStr); if (variantSchema != null) { - // Special-case: skip pushing variant schema if object contains only discriminator key - if (obj.members().size() == 1 && obj.members().containsKey(discSchema.discriminator())) { - LOG.finer(() -> "Skipping variant schema push for discriminator-only object"); - } else { - // Push variant schema for validation with discriminator key context - Frame variantFrame = new Frame(variantSchema, instance, frame.ptr, frame.crumbs, discSchema.discriminator()); - stack.push(variantFrame); - LOG.finer(() -> "Pushed discriminator variant frame for " + discriminatorValueStr + " with discriminator key: " + discSchema.discriminator()); - } + + Frame variantFrame = new Frame(variantSchema, instance, frame.ptr(), frame.crumbs(), discSchema.discriminator()); + stack.push(variantFrame); + LOG.finer(() -> "Pushed discriminator variant frame for " + discriminatorValueStr + " with discriminator key: " + discSchema.discriminator()); } } } @@ -266,8 +245,8 @@ void pushChildFrames(Frame frame, java.util.Deque stack) { case JtdSchema.RefSchema refSchema -> { try { JtdSchema resolved = refSchema.target(); - Frame resolvedFrame = new Frame(resolved, instance, frame.ptr, - frame.crumbs, frame.discriminatorKey()); + Frame resolvedFrame = new Frame(resolved, instance, frame.ptr(), + frame.crumbs(), frame.discriminatorKey()); pushChildFrames(resolvedFrame, stack); LOG.finer(() -> "Pushed ref schema resolved to " + resolved.getClass().getSimpleName() + " for ref: " + refSchema.ref()); @@ -299,7 +278,9 @@ JtdSchema compileSchema(JsonValue schema) { JsonObject defsObj = (JsonObject) obj.members().get("definitions"); for (String key : defsObj.members().keySet()) { if (definitions.get(key) == null) { - JtdSchema compiled = compileSchema(defsObj.members().get(key)); + JsonValue rawDef = defsObj.members().get(key); + // Compile definitions normally (RFC 8927 strict) + JtdSchema compiled = compileSchema(rawDef); definitions.put(key, compiled); } } @@ -308,7 +289,7 @@ JtdSchema compileSchema(JsonValue schema) { return compileObjectSchema(obj); } - /// Compiles an object schema according to RFC 8927 + /// Compiles an object schema according to RFC 8927 with strict semantics JtdSchema compileObjectSchema(JsonObject obj) { // Check for mutually-exclusive schema forms List forms = new ArrayList<>(); @@ -336,9 +317,29 @@ JtdSchema compileObjectSchema(JsonObject obj) { // Parse the specific schema form JtdSchema schema; - if (forms.isEmpty()) { - // Empty schema - accepts any value - schema = new JtdSchema.EmptySchema(); + // RFC 8927: {} is the empty form and accepts all instances + if (forms.isEmpty() && obj.members().isEmpty()) { + LOG.finer(() -> "Empty schema {} encountered. Per RFC 8927 this means 'accept anything'. " + + "Some non-JTD validators interpret {} with object semantics; this implementation follows RFC 8927."); + return new JtdSchema.EmptySchema(); + } else if (forms.isEmpty()) { + // Check if this is effectively an empty schema (ignoring metadata keys) + boolean hasNonMetadataKeys = members.keySet().stream() + .anyMatch(key -> !key.equals("nullable") && !key.equals("metadata") && !key.equals("definitions")); + + if (!hasNonMetadataKeys) { + // This is an empty schema (possibly with metadata) + LOG.finer(() -> "Empty schema encountered (with metadata: " + members.keySet() + "). " + + "Per RFC 8927 this means 'accept anything'. " + + "Some non-JTD validators interpret {} with object semantics; this implementation follows RFC 8927."); + return new JtdSchema.EmptySchema(); + } else { + // This should not happen in RFC 8927 - unknown keys present + throw new IllegalArgumentException("Schema contains unknown keys: " + + members.keySet().stream() + .filter(key -> !key.equals("nullable") && !key.equals("metadata") && !key.equals("definitions")) + .toList()); + } } else { String form = forms.getFirst(); schema = switch (form) { @@ -446,11 +447,8 @@ JtdSchema compilePropertiesSchema(JsonObject obj) { throw new IllegalArgumentException("additionalProperties must be a boolean"); } additionalProperties = bool.value(); - } else if (properties.isEmpty() && optionalProperties.isEmpty()) { - // Empty schema with no properties defined rejects additional properties by default - additionalProperties = false; - } - + } // Empty schema with no properties defined rejects additional properties by default + return new JtdSchema.PropertiesSchema(properties, optionalProperties, additionalProperties); } @@ -483,6 +481,8 @@ JtdSchema compileDiscriminatorSchema(JsonObject obj) { return new JtdSchema.DiscriminatorSchema(discStr.value(), mapping); } + // Removed: RFC 8927 strict mode - no context-aware ref resolution needed + /// Extracts and stores top-level definitions for ref resolution private Map parsePropertySchemas(JsonObject propsObj) { Map schemas = new java.util.HashMap<>(); 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 c39c282..e42b555 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 @@ -8,7 +8,7 @@ /// JTD Schema interface - validates JSON instances against JTD schemas /// Following RFC 8927 specification with eight mutually-exclusive schema forms -public sealed interface JtdSchema { +sealed interface JtdSchema { /// Validates a JSON instance against this schema /// @param instance The JSON value to validate @@ -20,7 +20,7 @@ public sealed interface JtdSchema { /// @param errors List to accumulate error messages /// @param verboseErrors Whether to include full JSON values in error messages /// @return true if validation passes, false if validation fails - default boolean validateWithFrame(Jtd.Frame frame, java.util.List errors, boolean verboseErrors) { + default boolean validateWithFrame(Frame frame, java.util.List errors, boolean verboseErrors) { // Default implementation delegates to existing validate method for backward compatibility Jtd.Result result = validate(frame.instance(), verboseErrors); if (!result.isValid()) { @@ -50,8 +50,9 @@ public Jtd.Result validate(JsonValue instance) { return wrapped.validate(instance); } + @SuppressWarnings("ClassEscapesDefinedScope") @Override - public boolean validateWithFrame(Jtd.Frame frame, java.util.List errors, boolean verboseErrors) { + public boolean validateWithFrame(Frame frame, java.util.List errors, boolean verboseErrors) { if (frame.instance() instanceof JsonNull) { return true; } @@ -67,8 +68,9 @@ public Jtd.Result validate(JsonValue instance) { return Jtd.Result.success(); } + @SuppressWarnings("ClassEscapesDefinedScope") @Override - public boolean validateWithFrame(Jtd.Frame frame, java.util.List errors, boolean verboseErrors) { + public boolean validateWithFrame(Frame frame, java.util.List errors, boolean verboseErrors) { // Empty schema accepts any JSON value return true; } @@ -89,10 +91,11 @@ public Jtd.Result validate(JsonValue instance) { return target().validate(instance); } + @SuppressWarnings("ClassEscapesDefinedScope") @Override - public boolean validateWithFrame(Jtd.Frame frame, java.util.List errors, boolean verboseErrors) { + public boolean validateWithFrame(Frame frame, java.util.List errors, boolean verboseErrors) { JtdSchema resolved = target(); - Jtd.Frame resolvedFrame = new Jtd.Frame(resolved, frame.instance(), frame.ptr(), + Frame resolvedFrame = new Frame(resolved, frame.instance(), frame.ptr(), frame.crumbs(), frame.discriminatorKey()); return resolved.validateWithFrame(resolvedFrame, errors, verboseErrors); } @@ -127,8 +130,9 @@ public Jtd.Result validate(JsonValue instance, boolean verboseErrors) { }; } + @SuppressWarnings("ClassEscapesDefinedScope") @Override - public boolean validateWithFrame(Jtd.Frame frame, java.util.List errors, boolean verboseErrors) { + public boolean validateWithFrame(Frame frame, java.util.List errors, boolean verboseErrors) { Jtd.Result result = validate(frame.instance(), verboseErrors); if (!result.isValid()) { // Enrich errors with offset and path information @@ -178,63 +182,7 @@ Jtd.Result validateTimestamp(JsonValue instance, boolean verboseErrors) { : Jtd.Error.EXPECTED_TIMESTAMP.message(instance.getClass().getSimpleName()); return Jtd.Result.failure(error); } - - /// Package-protected static validation for RFC 3339 timestamp format with leap second support - /// RFC 3339 grammar: date-time = full-date "T" full-time - /// Supports leap seconds (seconds = 60 when minutes = 59) - static boolean isValidRfc3339Timestamp(String timestamp) { - // RFC 3339 regex pattern with leap second support - String rfc3339Pattern = "^(\\d{4})-(\\d{2})-(\\d{2})T(\\d{2}):(\\d{2}):(\\d{2})(\\.\\d+)?(Z|[+-]\\d{2}:\\d{2})$"; - java.util.regex.Pattern pattern = java.util.regex.Pattern.compile(rfc3339Pattern); - java.util.regex.Matcher matcher = pattern.matcher(timestamp); - - if (!matcher.matches()) { - return false; - } - - try { - int year = Integer.parseInt(matcher.group(1)); - int month = Integer.parseInt(matcher.group(2)); - int day = Integer.parseInt(matcher.group(3)); - int hour = Integer.parseInt(matcher.group(4)); - int minute = Integer.parseInt(matcher.group(5)); - int second = Integer.parseInt(matcher.group(6)); - - // Validate basic date/time components - if (year < 1 || month < 1 || month > 12 || day < 1 || day > 31) { - return false; - } - if (hour < 0 || hour > 23 || minute < 0 || minute > 59) { - return false; - } - - // Handle leap seconds: seconds = 60 is valid only if minutes = 59 - if (second == 60) { - if (minute != 59) { - return false; - } - // For leap seconds, we accept the format but don't validate the specific date - // This matches RFC 8927 behavior - format validation only - return true; - } - - if (second < 0 || second > 59) { - return false; - } - - // For normal timestamps, delegate to OffsetDateTime.parse for full validation - try { - OffsetDateTime.parse(timestamp, DateTimeFormatter.ISO_OFFSET_DATE_TIME); - return true; - } catch (Exception e) { - return false; - } - - } catch (NumberFormatException e) { - return false; - } - } - + Jtd.Result validateInteger(JsonValue instance, String type, boolean verboseErrors) { if (instance instanceof JsonNumber num) { Number value = num.toNumber(); @@ -317,8 +265,9 @@ public Jtd.Result validate(JsonValue instance, boolean verboseErrors) { return Jtd.Result.failure(error); } + @SuppressWarnings("ClassEscapesDefinedScope") @Override - public boolean validateWithFrame(Jtd.Frame frame, java.util.List errors, boolean verboseErrors) { + public boolean validateWithFrame(Frame frame, java.util.List errors, boolean verboseErrors) { Jtd.Result result = validate(frame.instance(), verboseErrors); if (!result.isValid()) { // Enrich errors with offset and path information @@ -360,11 +309,12 @@ public Jtd.Result validate(JsonValue instance, boolean verboseErrors) { return Jtd.Result.failure(error); } + @SuppressWarnings("ClassEscapesDefinedScope") @Override - public boolean validateWithFrame(Jtd.Frame frame, java.util.List errors, boolean verboseErrors) { + public boolean validateWithFrame(Frame frame, java.util.List errors, boolean verboseErrors) { JsonValue instance = frame.instance(); - if (!(instance instanceof JsonArray arr)) { + if (!(instance instanceof JsonArray)) { String error = verboseErrors ? Jtd.Error.EXPECTED_ARRAY.message(instance, instance.getClass().getSimpleName()) : Jtd.Error.EXPECTED_ARRAY.message(instance.getClass().getSimpleName()); @@ -447,11 +397,12 @@ public Jtd.Result validate(JsonValue instance, boolean verboseErrors) { return Jtd.Result.success(); } + @SuppressWarnings("ClassEscapesDefinedScope") @Override - public boolean validateWithFrame(Jtd.Frame frame, java.util.List errors, boolean verboseErrors) { + public boolean validateWithFrame(Frame frame, java.util.List errors, boolean verboseErrors) { JsonValue instance = frame.instance(); - if (!(instance instanceof JsonObject obj)) { + if (!(instance instanceof JsonObject)) { String error = verboseErrors ? Jtd.Error.EXPECTED_OBJECT.message(instance, instance.getClass().getSimpleName()) : Jtd.Error.EXPECTED_OBJECT.message(instance.getClass().getSimpleName()); @@ -497,11 +448,12 @@ public Jtd.Result validate(JsonValue instance, boolean verboseErrors) { return Jtd.Result.success(); } + @SuppressWarnings("ClassEscapesDefinedScope") @Override - public boolean validateWithFrame(Jtd.Frame frame, java.util.List errors, boolean verboseErrors) { + public boolean validateWithFrame(Frame frame, java.util.List errors, boolean verboseErrors) { JsonValue instance = frame.instance(); - if (!(instance instanceof JsonObject obj)) { + if (!(instance instanceof JsonObject)) { String error = verboseErrors ? Jtd.Error.EXPECTED_OBJECT.message(instance, instance.getClass().getSimpleName()) : Jtd.Error.EXPECTED_OBJECT.message(instance.getClass().getSimpleName()); @@ -567,8 +519,9 @@ public Jtd.Result validate(JsonValue instance, boolean verboseErrors) { return variantSchema.validate(instance, verboseErrors); } + @SuppressWarnings("ClassEscapesDefinedScope") @Override - public boolean validateWithFrame(Jtd.Frame frame, java.util.List errors, boolean verboseErrors) { + public boolean validateWithFrame(Frame frame, java.util.List errors, boolean verboseErrors) { JsonValue instance = frame.instance(); if (!(instance instanceof JsonObject obj)) { diff --git a/json-java21-jtd/src/test/java/json/java21/jdt/demo/VisibilityTest.java b/json-java21-jtd/src/test/java/json/java21/jdt/demo/VisibilityTest.java new file mode 100644 index 0000000..57b0cff --- /dev/null +++ b/json-java21-jtd/src/test/java/json/java21/jdt/demo/VisibilityTest.java @@ -0,0 +1,32 @@ +package json.java21.jdt.demo; + +import jdk.sandbox.java.util.json.Json; +import jdk.sandbox.java.util.json.JsonValue; +import json.java21.jtd.Jtd; +import json.java21.jtd.JtdTestBase; +import org.junit.jupiter.api.Test; + +import java.util.logging.Logger; + +import static org.assertj.core.api.Assertions.assertThat; + + +public class VisibilityTest extends JtdTestBase { + + static final Logger LOG = Logger.getLogger("json.java21.jtd"); + + /// Test ref schema resolution with valid definitions + /// RFC 8927 Section 3.3.2: Ref schemas must resolve against definitions + @Test + public void testRefSchemaValid() { + JsonValue schema = Json.parse("{\"ref\": \"address\", \"definitions\": {\"address\": {\"type\": \"string\"}}}"); + JsonValue validData = Json.parse("\"123 Main St\""); + + Jtd validator = new Jtd(); + Jtd.Result result = validator.validate(schema, validData); + + assertThat(result.isValid()).isTrue(); + assertThat(result.errors()).isEmpty(); + LOG.fine(() -> "Ref schema valid test - schema: " + schema + ", data: " + validData); + } +} diff --git a/json-java21-jtd/src/test/java/json/java21/jtd/DocumentationAJvTests.java b/json-java21-jtd/src/test/java/json/java21/jtd/DocumentationAJvTests.java index 5e38f40..162c1fc 100644 --- a/json-java21-jtd/src/test/java/json/java21/jtd/DocumentationAJvTests.java +++ b/json-java21-jtd/src/test/java/json/java21/jtd/DocumentationAJvTests.java @@ -13,7 +13,7 @@ public class DocumentationAJvTests extends JtdTestBase { /// Type form: primitive values - string type /// Example from docs: { type: "string" } @Test - public void testTypeFormString() throws Exception { + public void testTypeFormString() { JsonValue schema = Json.parse("{ \"type\": \"string\" }"); // Test valid string @@ -26,7 +26,7 @@ public void testTypeFormString() throws Exception { /// Counter-test: Type form string validation should fail for non-strings /// Same schema as testTypeFormString but tests invalid data @Test - public void testTypeFormStringInvalid() throws Exception { + public void testTypeFormStringInvalid() { JsonValue schema = Json.parse("{ \"type\": \"string\" }"); // Test validation failure - should fail for non-string @@ -41,7 +41,7 @@ public void testTypeFormStringInvalid() throws Exception { /// Enum form: string enumeration /// Example from docs: { enum: ["foo", "bar"] } @Test - public void testEnumForm() throws Exception { + public void testEnumForm() { JsonValue schema = Json.parse("{ \"enum\": [\"foo\", \"bar\"] }"); // Test valid enum values @@ -57,7 +57,7 @@ public void testEnumForm() throws Exception { /// Counter-test: Enum form validation should fail for values not in enum /// Same schema as testEnumForm but tests invalid data @Test - public void testEnumFormInvalid() throws Exception { + public void testEnumFormInvalid() { JsonValue schema = Json.parse("{ \"enum\": [\"foo\", \"bar\"] }"); // Test validation failure - should fail for value not in enum @@ -72,7 +72,7 @@ public void testEnumFormInvalid() throws Exception { /// Counter-test: Elements form validation should fail for heterogeneous arrays /// Same schema as testElementsForm but tests invalid data @Test - public void testElementsFormInvalid() throws Exception { + public void testElementsFormInvalid() { JsonValue schema = Json.parse("{ \"elements\": { \"type\": \"string\" } }"); // Test validation failure - should fail for array with non-string elements @@ -87,7 +87,7 @@ public void testElementsFormInvalid() throws Exception { /// Elements form: homogeneous arrays /// Schema example: { elements: { type: "string" } } @Test - public void testElementsForm() throws Exception { + public void testElementsForm() { JsonValue schema = Json.parse("{ \"elements\": { \"type\": \"string\" } }"); // Test valid arrays @@ -103,7 +103,7 @@ public void testElementsForm() throws Exception { /// Properties form: objects with required properties /// Example 1: { properties: { foo: { type: "string" } } } @Test - public void testPropertiesFormRequiredOnly() throws Exception { + public void testPropertiesFormRequiredOnly() { JsonValue schema = Json.parse("{ \"properties\": { \"foo\": { \"type\": \"string\" } } }"); // Test valid object @@ -117,7 +117,7 @@ public void testPropertiesFormRequiredOnly() throws Exception { /// Counter-test: Properties form validation should fail for missing required properties /// Same schema as testPropertiesFormRequiredOnly but tests invalid data @Test - public void testPropertiesFormRequiredOnlyInvalid() throws Exception { + public void testPropertiesFormRequiredOnlyInvalid() { JsonValue schema = Json.parse("{ \"properties\": { \"foo\": { \"type\": \"string\" } } }"); // Test validation failure - should fail for missing required property @@ -132,7 +132,7 @@ public void testPropertiesFormRequiredOnlyInvalid() throws Exception { /// Properties form: objects with required and optional properties /// Example 2: { properties: { foo: {type: "string"} }, optionalProperties: { bar: {enum: ["1", "2"]} }, additionalProperties: true } @Test - public void testPropertiesFormWithOptional() throws Exception { + public void testPropertiesFormWithOptional() { JsonValue schema = Json.parse("{ \"properties\": { \"foo\": {\"type\": \"string\"} }, \"optionalProperties\": { \"bar\": {\"enum\": [\"1\", \"2\"]} }, \"additionalProperties\": true }"); // Test valid objects @@ -150,7 +150,7 @@ public void testPropertiesFormWithOptional() throws Exception { /// Discriminator form: tagged union /// Example 1: { discriminator: "version", mapping: { "1": { properties: { foo: {type: "string"} } }, "2": { properties: { foo: {type: "uint8"} } } } } @Test - public void testDiscriminatorForm() throws Exception { + public void testDiscriminatorForm() { JsonValue schema = Json.parse("{ \"discriminator\": \"version\", \"mapping\": { \"1\": { \"properties\": { \"foo\": {\"type\": \"string\"} } }, \"2\": { \"properties\": { \"foo\": {\"type\": \"uint8\"} } } } }"); // Test valid discriminated objects @@ -166,7 +166,7 @@ public void testDiscriminatorForm() throws Exception { /// Counter-test: Discriminator form validation should fail for invalid discriminator values /// Same schema as testDiscriminatorForm but tests invalid data @Test - public void testDiscriminatorFormInvalid() throws Exception { + public void testDiscriminatorFormInvalid() { JsonValue schema = Json.parse("{ \"discriminator\": \"version\", \"mapping\": { \"1\": { \"properties\": { \"foo\": {\"type\": \"string\"} } }, \"2\": { \"properties\": { \"foo\": {\"type\": \"uint8\"} } } } }"); // Test validation failure - should fail for discriminator value not in mapping @@ -181,7 +181,7 @@ public void testDiscriminatorFormInvalid() throws Exception { /// Values form: dictionary with homogeneous values /// Example: { values: { type: "uint8" } } @Test - public void testValuesForm() throws Exception { + public void testValuesForm() { JsonValue schema = Json.parse("{ \"values\": { \"type\": \"uint8\" } }"); // Test valid dictionaries @@ -197,7 +197,7 @@ public void testValuesForm() throws Exception { /// Counter-test: Values form validation should fail for heterogeneous value types /// Same schema as testValuesForm but tests invalid data @Test - public void testValuesFormInvalid() throws Exception { + public void testValuesFormInvalid() { JsonValue schema = Json.parse("{ \"values\": { \"type\": \"uint8\" } }"); // Test validation failure - should fail for object with mixed value types @@ -212,7 +212,7 @@ public void testValuesFormInvalid() throws Exception { /// Ref form: reference to definitions /// Example 1: { properties: { propFoo: {ref: "foo", nullable: true} }, definitions: { foo: {type: "string"} } } @Test - public void testRefForm() throws Exception { + public void testRefForm() { JsonValue schema = Json.parse("{ \"properties\": { \"propFoo\": {\"ref\": \"foo\", \"nullable\": true} }, \"definitions\": { \"foo\": {\"type\": \"string\"} } }"); assertThat(schema).isNotNull(); @@ -222,7 +222,7 @@ public void testRefForm() throws Exception { /// Self-referencing schema for binary tree /// Example 2: { ref: "tree", definitions: { tree: { properties: { value: {type: "int32"} }, optionalProperties: { left: {ref: "tree"}, right: {ref: "tree"} } } } } @Test - public void testSelfReferencingSchema() throws Exception { + public void testSelfReferencingSchema() { JsonValue schema = Json.parse("{ \"ref\": \"tree\", \"definitions\": { \"tree\": { \"properties\": { \"value\": {\"type\": \"int32\"} }, \"optionalProperties\": { \"left\": {\"ref\": \"tree\"}, \"right\": {\"ref\": \"tree\"} } } } }"); // Test tree structure @@ -233,47 +233,43 @@ public void testSelfReferencingSchema() throws Exception { LOG.fine(() -> "Self-referencing schema test - schema: " + schema + ", tree: " + tree); } - /// Empty form: any data + /// Empty form: RFC 8927 - {} accepts all JSON instances @Test - public void testEmptyForm() throws Exception { + public void testEmptyFormRfc8927() { JsonValue schema = Json.parse("{}"); + Jtd validator = new Jtd(); - // Test various data types - JsonValue stringData = Json.parse("\"hello\""); - JsonValue numberData = Json.parse("42"); - JsonValue objectData = Json.parse("{\"key\": \"value\"}"); - JsonValue arrayData = Json.parse("[1, 2, 3]"); - JsonValue nullData = Json.parse("null"); - JsonValue boolData = Json.parse("true"); - - assertThat(schema).isNotNull(); - assertThat(stringData).isNotNull(); - assertThat(numberData).isNotNull(); - assertThat(objectData).isNotNull(); - assertThat(arrayData).isNotNull(); - assertThat(nullData).isNotNull(); - assertThat(boolData).isNotNull(); - LOG.fine(() -> "Empty form test - schema: " + schema + ", accepts any data"); + // RFC 8927 §3.3.1: "If a schema is of the 'empty' form, then it accepts all instances" + assertThat(validator.validate(schema, Json.parse("null")).isValid()).isTrue(); + assertThat(validator.validate(schema, Json.parse("true")).isValid()).isTrue(); + assertThat(validator.validate(schema, Json.parse("123")).isValid()).isTrue(); + assertThat(validator.validate(schema, Json.parse("3.14")).isValid()).isTrue(); + assertThat(validator.validate(schema, Json.parse("\"hello\"")).isValid()).isTrue(); + assertThat(validator.validate(schema, Json.parse("[]")).isValid()).isTrue(); + assertThat(validator.validate(schema, Json.parse("{}")).isValid()).isTrue(); + assertThat(validator.validate(schema, Json.parse("{\"key\": \"value\"}")).isValid()).isTrue(); + + LOG.fine(() -> "Empty form RFC 8927 test - schema: " + schema + ", accepts all JSON instances"); } - /// Counter-test: Empty form validation should pass for any data (no invalid data) - /// Same schema as testEmptyForm but tests that no data is invalid + /// Demonstration: Empty form has no invalid data per RFC 8927 + /// Same schema as testEmptyFormRfc8927 but shows everything passes @Test - public void testEmptyFormInvalid() throws Exception { + public void testEmptyFormNoInvalidData() { JsonValue schema = Json.parse("{}"); + Jtd validator = new Jtd(); - // Test that empty schema accepts any data - should pass for "invalid" data + // RFC 8927: {} accepts everything, so even "invalid-looking" data passes JsonValue anyData = Json.parse("{\"anything\": \"goes\"}"); - Jtd validator = new Jtd(); Jtd.Result result = validator.validate(schema, anyData); assertThat(result.isValid()).isTrue(); assertThat(result.errors()).isEmpty(); - LOG.fine(() -> "Empty form invalid test - schema: " + schema + ", any data should pass: " + anyData); + LOG.fine(() -> "Empty form no invalid data test - schema: " + schema + ", any data passes: " + anyData); } /// Type form: numeric types @Test - public void testNumericTypes() throws Exception { + public void testNumericTypes() { // Test various numeric types String[] numericSchemas = {"{ \"type\": \"int8\" }", "{ \"type\": \"uint8\" }", "{ \"type\": \"int16\" }", "{ \"type\": \"uint16\" }", "{ \"type\": \"int32\" }", "{ \"type\": \"uint32\" }", "{ \"type\": \"float32\" }", "{ \"type\": \"float64\" }"}; @@ -287,7 +283,7 @@ public void testNumericTypes() throws Exception { /// Counter-test: Numeric type validation should fail for non-numeric data /// Tests that numeric types reject string data @Test - public void testNumericTypesInvalid() throws Exception { + public void testNumericTypesInvalid() { JsonValue schema = Json.parse("{ \"type\": \"int32\" }"); // Test validation failure - should fail for string data @@ -301,7 +297,7 @@ public void testNumericTypesInvalid() throws Exception { /// Nullable types @Test - public void testNullableTypes() throws Exception { + public void testNullableTypes() { String[] nullableSchemas = {"{ \"type\": \"string\", \"nullable\": true }", "{ \"enum\": [\"foo\", \"bar\"], \"nullable\": true }", "{ \"elements\": { \"type\": \"string\" }, \"nullable\": true }"}; for (String schemaJson : nullableSchemas) { @@ -314,7 +310,7 @@ public void testNullableTypes() throws Exception { /// Counter-test: Nullable types should still fail for non-matching non-null data /// Tests that nullable doesn't bypass type validation for non-null values @Test - public void testNullableTypesInvalid() throws Exception { + public void testNullableTypesInvalid() { JsonValue schema = Json.parse("{ \"type\": \"string\", \"nullable\": true }"); // Test validation failure - should fail for non-string, non-null data diff --git a/json-java21-jtd/src/test/java/json/java21/jtd/DocumentationAJvTests.java.backup b/json-java21-jtd/src/test/java/json/java21/jtd/DocumentationAJvTests.java.backup deleted file mode 100644 index b9c3bc9..0000000 --- a/json-java21-jtd/src/test/java/json/java21/jtd/DocumentationAJvTests.java.backup +++ /dev/null @@ -1,430 +0,0 @@ -package json.java21.jtd; - -import com.fasterxml.jackson.databind.ObjectMapper; -import jdk.sandbox.java.util.json.Json; -import jdk.sandbox.java.util.json.JsonValue; -import org.junit.jupiter.api.Test; - -import static org.assertj.core.api.Assertions.assertThat; - -/// Tests based on AJV documentation examples for JTD schema forms. -/// Each test method corresponds to an example from the AJV JTD documentation. -public class DocumentationAJvTests extends JtdTestBase { - - /// Type form: primitive values - string type - /// Example from docs: { type: "string" } - @Test - public void testTypeFormString() throws Exception { - String schemaJson = "{ \"type\": \"string\" }"; - JsonValue result1; - result1 = Json.parse(schemaJson); - JsonValue schema = result; - - // Test valid string - JsonValue result; - result = Json.parse("\"hello\""); - JsonValue validData = result; - assertThat(schema).isNotNull(); - assertThat(validData).isNotNull(); - LOG.fine(() -> "Type form string test - schema: " + schema + ", data: " + validData); - } - - /// Counter-test: Type form string validation should fail for non-strings - /// Same schema as testTypeFormString but tests invalid data - @Test - public void testTypeFormStringInvalid() throws Exception { - String schemaJson = "{ \"type\": \"string\" }"; - JsonValue result1; - result1 = Json.parse(schemaJson); - JsonValue schema = result; - - // Test validation failure - should fail for non-string - JsonValue invalidData = Json.parse("123"); - JtdValidator validator = new JtdValidator(); - ValidationResult invalidResult = validator.validate(schema, invalidData); - assertThat(invalidResult.isValid()).isFalse(); - assertThat(invalidResult.errors()).isNotEmpty(); - LOG.fine(() -> "Type form string invalid test - schema: " + schema + ", invalid data: " + invalidData + ", errors: " + invalidResult.errors()); - } - - /// Enum form: string enumeration - /// Example from docs: { enum: ["foo", "bar"] } - @Test - public void testEnumForm() throws Exception { - String schemaJson = "{ \"enum\": [\"foo\", \"bar\"] }"; - JsonValue result2; - result2 = Json.parse(schemaJson); - JsonValue schema = result; - - // Test valid enum values - JsonValue result1; - result1 = Json.parse("\"foo\""); - JsonValue validFoo = result1; - JsonValue result; - result = Json.parse("\"bar\""); - JsonValue validBar = result; - - assertThat(schema).isNotNull(); - assertThat(validFoo).isNotNull(); - assertThat(validBar).isNotNull(); - LOG.fine(() -> "Enum form test - schema: " + schema + ", valid values: foo, bar"); - } - - /// Counter-test: Enum form validation should fail for values not in enum - /// Same schema as testEnumForm but tests invalid data - @Test - public void testEnumFormInvalid() throws Exception { - String schemaJson = "{ \"enum\": [\"foo\", \"bar\"] }"; - JsonValue result2; - result2 = Json.parse(schemaJson); - JsonValue schema = result; - - // Test validation failure - should fail for value not in enum - JsonValue invalidData = Json.parse("\"baz\""); - JtdValidator validator = new JtdValidator(); - ValidationResult invalidResult = validator.validate(schema, invalidData); - assertThat(invalidResult.isValid()).isFalse(); - assertThat(invalidResult.errors()).isNotEmpty(); - LOG.fine(() -> "Enum form invalid test - schema: " + schema + ", invalid data: " + invalidData + ", errors: " + invalidResult.errors()); - } - - /// Counter-test: Elements form validation should fail for heterogeneous arrays - /// Same schema as testElementsForm but tests invalid data - @Test - public void testElementsFormInvalid() throws Exception { - String schemaJson = "{ \"elements\": { \"type\": \"string\" } }"; - JsonValue result2; - result2 = Json.parse(schemaJson); - JsonValue schema = result; - - // Test validation failure - should fail for array with non-string elements - JsonValue invalidData = Json.parse("[\"foo\", 123]"); - JtdValidator validator = new JtdValidator(); - ValidationResult invalidResult = validator.validate(schema, invalidData); - assertThat(invalidResult.isValid()).isFalse(); - assertThat(invalidResult.errors()).isNotEmpty(); - LOG.fine(() -> "Elements form invalid test - schema: " + schema + ", invalid data: " + invalidData + ", errors: " + invalidResult.errors()); - } - - /// Elements form: homogeneous arrays - /// Schema example: { elements: { type: "string" } } - @Test - public void testElementsForm() throws Exception { - String schemaJson = "{ \"elements\": { \"type\": \"string\" } }"; - JsonValue result2; - result2 = Json.parse(schemaJson); - JsonValue schema = result; - - // Test valid arrays - JsonValue result1; - result1 = Json.parse("[]"); - JsonValue emptyArray = result1; - JsonValue result; - result = Json.parse("[\"foo\", \"bar\"]"); - JsonValue stringArray = result; - - assertThat(schema).isNotNull(); - assertThat(emptyArray).isNotNull(); - assertThat(stringArray).isNotNull(); - LOG.fine(() -> "Elements form test - schema: " + schema + ", valid arrays: [], [\"foo\", \"bar\"]"); - } - - /// Properties form: objects with required properties - /// Example 1: { properties: { foo: { type: "string" } } } - @Test - public void testPropertiesFormRequiredOnly() throws Exception { - String schemaJson = "{ \"properties\": { \"foo\": { \"type\": \"string\" } } }"; - JsonValue result1; - result1 = Json.parse(schemaJson); - JsonValue schema = result; - - // Test valid object - JsonValue result; - result = Json.parse("{\"foo\": \"bar\"}"); - JsonValue validObject = result; - - assertThat(schema).isNotNull(); - assertThat(validObject).isNotNull(); - LOG.fine(() -> "Properties form (required only) test - schema: " + schema + ", valid: {\"foo\": \"bar\"}"); - } - - /// Counter-test: Properties form validation should fail for missing required properties - /// Same schema as testPropertiesFormRequiredOnly but tests invalid data - @Test - public void testPropertiesFormRequiredOnlyInvalid() throws Exception { - String schemaJson = "{ \"properties\": { \"foo\": { \"type\": \"string\" } } }"; - JsonValue result1; - result1 = Json.parse(schemaJson); - JsonValue schema = result; - - // Test validation failure - should fail for missing required property - JsonValue invalidData = Json.parse("{}"); - JtdValidator validator = new JtdValidator(); - ValidationResult invalidResult = validator.validate(schema, invalidData); - assertThat(invalidResult.isValid()).isFalse(); - assertThat(invalidResult.errors()).isNotEmpty(); - LOG.fine(() -> "Properties form (required only) invalid test - schema: " + schema + ", invalid data: " + invalidData + ", errors: " + invalidResult.errors()); - } - - /// Properties form: objects with required and optional properties - /// Example 2: { properties: { foo: {type: "string"} }, optionalProperties: { bar: {enum: ["1", "2"]} }, additionalProperties: true } - @Test - public void testPropertiesFormWithOptional() throws Exception { - String schemaJson = "{ \"properties\": { \"foo\": {\"type\": \"string\"} }, \"optionalProperties\": { \"bar\": {\"enum\": [\"1\", \"2\"]} }, \"additionalProperties\": true }"; - JsonValue result3; - result3 = Json.parse(schemaJson); - JsonValue schema = result; - - // Test valid objects - JsonValue result2; - result2 = Json.parse("{\"foo\": \"bar\"}"); - JsonValue withRequired = result2; - JsonValue result1; - result1 = Json.parse("{\"foo\": \"bar\", \"bar\": \"1\"}"); - JsonValue withOptional = result1; - JsonValue result; - result = Json.parse("{\"foo\": \"bar\", \"additional\": 1}"); - JsonValue withAdditional = result; - - assertThat(schema).isNotNull(); - assertThat(withRequired).isNotNull(); - assertThat(withOptional).isNotNull(); - assertThat(withAdditional).isNotNull(); - LOG.fine(() -> "Properties form (with optional) test - schema: " + schema); - } - - /// Discriminator form: tagged union - /// Example 1: { discriminator: "version", mapping: { "1": { properties: { foo: {type: "string"} } }, "2": { properties: { foo: {type: "uint8"} } } } } - @Test - public void testDiscriminatorForm() throws Exception { - String schemaJson = "{ \"discriminator\": \"version\", \"mapping\": { \"1\": { \"properties\": { \"foo\": {\"type\": \"string\"} } }, \"2\": { \"properties\": { \"foo\": {\"type\": \"uint8\"} } } } }"; - JsonValue result2; - result2 = Json.parse(schemaJson); - JsonValue schema = result; - - // Test valid discriminated objects - JsonValue result1; - result1 = Json.parse("{\"version\": \"1\", \"foo\": \"1\"}"); - JsonValue version1 = result1; - JsonValue result; - result = Json.parse("{\"version\": \"2\", \"foo\": 1}"); - JsonValue version2 = result; - - assertThat(schema).isNotNull(); - assertThat(version1).isNotNull(); - assertThat(version2).isNotNull(); - LOG.fine(() -> "Discriminator form test - schema: " + schema); - } - - /// Counter-test: Discriminator form validation should fail for invalid discriminator values - /// Same schema as testDiscriminatorForm but tests invalid data - @Test - public void testDiscriminatorFormInvalid() throws Exception { - String schemaJson = "{ \"discriminator\": \"version\", \"mapping\": { \"1\": { \"properties\": { \"foo\": {\"type\": \"string\"} } }, \"2\": { \"properties\": { \"foo\": {\"type\": \"uint8\"} } } } }"; - JsonValue result2; - result2 = Json.parse(schemaJson); - JsonValue schema = result; - - // Test validation failure - should fail for discriminator value not in mapping - JsonValue invalidData = Json.parse("{\"version\": \"3\", \"foo\": \"1\"}"); - JtdValidator validator = new JtdValidator(); - ValidationResult invalidResult = validator.validate(schema, invalidData); - assertThat(invalidResult.isValid()).isFalse(); - assertThat(invalidResult.errors()).isNotEmpty(); - LOG.fine(() -> "Discriminator form invalid test - schema: " + schema + ", invalid data: " + invalidData + ", errors: " + invalidResult.errors()); - } - - /// Values form: dictionary with homogeneous values - /// Example: { values: { type: "uint8" } } - @Test - public void testValuesForm() throws Exception { - String schemaJson = "{ \"values\": { \"type\": \"uint8\" } }"; - JsonValue result2; - result2 = Json.parse(schemaJson); - JsonValue schema = result; - - // Test valid dictionaries - JsonValue result1; - result1 = Json.parse("{}"); - JsonValue emptyObj = result1; - JsonValue result; - result = Json.parse("{\"foo\": 1, \"bar\": 2}"); - JsonValue numberValues = result; - - assertThat(schema).isNotNull(); - assertThat(emptyObj).isNotNull(); - assertThat(numberValues).isNotNull(); - LOG.fine(() -> "Values form test - schema: " + schema + ", valid: {}, {\"foo\": 1, \"bar\": 2}"); - } - - /// Counter-test: Values form validation should fail for heterogeneous value types - /// Same schema as testValuesForm but tests invalid data - @Test - public void testValuesFormInvalid() throws Exception { - String schemaJson = "{ \"values\": { \"type\": \"uint8\" } }"; - JsonValue result2; - result2 = Json.parse(schemaJson); - JsonValue schema = result; - - // Test validation failure - should fail for object with mixed value types - JsonValue invalidData = Json.parse("{\"foo\": 1, \"bar\": \"not-a-number\"}"); - JtdValidator validator = new JtdValidator(); - ValidationResult invalidResult = validator.validate(schema, invalidData); - assertThat(invalidResult.isValid()).isFalse(); - assertThat(invalidResult.errors()).isNotEmpty(); - LOG.fine(() -> "Values form invalid test - schema: " + schema + ", invalid data: " + invalidData + ", errors: " + invalidResult.errors()); - } - - /// Ref form: reference to definitions - /// Example 1: { properties: { propFoo: {ref: "foo", nullable: true} }, definitions: { foo: {type: "string"} } } - @Test - public void testRefForm() throws Exception { - String schemaJson = "{ \"properties\": { \"propFoo\": {\"ref\": \"foo\", \"nullable\": true} }, \"definitions\": { \"foo\": {\"type\": \"string\"} } }"; - JsonValue result; - result = Json.parse(schemaJson); - JsonValue schema = result; - - assertThat(schema).isNotNull(); - LOG.fine(() -> "Ref form test - schema: " + schema); - } - - /// Self-referencing schema for binary tree - /// Example 2: { ref: "tree", definitions: { tree: { properties: { value: {type: "int32"} }, optionalProperties: { left: {ref: "tree"}, right: {ref: "tree"} } } } } - @Test - public void testSelfReferencingSchema() throws Exception { - String schemaJson = "{ \"ref\": \"tree\", \"definitions\": { \"tree\": { \"properties\": { \"value\": {\"type\": \"int32\"} }, \"optionalProperties\": { \"left\": {\"ref\": \"tree\"}, \"right\": {\"ref\": \"tree\"} } } } }"; - JsonValue result1; - result1 = Json.parse(schemaJson); - JsonValue schema = result; - - // Test tree structure - JsonValue result; - result = Json.parse("{\"value\": 1, \"left\": {\"value\": 2}, \"right\": {\"value\": 3}}"); - JsonValue tree = result; - - assertThat(schema).isNotNull(); - assertThat(tree).isNotNull(); - LOG.fine(() -> "Self-referencing schema test - schema: " + schema + ", tree: " + tree); - } - - /// Empty form: any data - @Test - public void testEmptyForm() throws Exception { - String schemaJson = "{}"; - JsonValue result6; - result6 = Json.parse(schemaJson); - JsonValue schema = result; - - // Test various data types - JsonValue result5; - result5 = Json.parse("\"hello\""); - JsonValue stringData = result5; - JsonValue result4; - result4 = Json.parse("42"); - JsonValue numberData = result4; - JsonValue result3; - result3 = Json.parse("{\"key\": \"value\"}"); - JsonValue objectData = result3; - JsonValue result2; - result2 = Json.parse("[1, 2, 3]"); - JsonValue arrayData = result2; - JsonValue result1; - result1 = Json.parse("null"); - JsonValue nullData = result1; - JsonValue result; - result = Json.parse("true"); - JsonValue boolData = result; - - assertThat(schema).isNotNull(); - assertThat(stringData).isNotNull(); - assertThat(numberData).isNotNull(); - assertThat(objectData).isNotNull(); - assertThat(arrayData).isNotNull(); - assertThat(nullData).isNotNull(); - assertThat(boolData).isNotNull(); - LOG.fine(() -> "Empty form test - schema: " + schema + ", accepts any data"); - } - - /// Counter-test: Empty form validation should pass for any data (no invalid data) - /// Same schema as testEmptyForm but tests that no data is invalid - @Test - public void testEmptyFormInvalid() throws Exception { - String schemaJson = "{}"; - JsonValue result1; - result1 = Json.parse(schemaJson); - JsonValue schema = result; - - // Test that empty schema accepts any data - should pass for "invalid" data - JsonValue anyData = Json.parse("{\"anything\": \"goes\"}"); - JtdValidator validator = new JtdValidator(); - ValidationResult result = validator.validate(schema, anyData); - assertThat(result.isValid()).isTrue(); - assertThat(result.errors()).isEmpty(); - LOG.fine(() -> "Empty form invalid test - schema: " + schema + ", any data should pass: " + anyData); - } - - /// Type form: numeric types - @Test - public void testNumericTypes() throws Exception { - // Test various numeric types - String[] numericSchemas = {"{ \"type\": \"int8\" }", "{ \"type\": \"uint8\" }", "{ \"type\": \"int16\" }", "{ \"type\": \"uint16\" }", "{ \"type\": \"int32\" }", "{ \"type\": \"uint32\" }", "{ \"type\": \"float32\" }", "{ \"type\": \"float64\" }"}; - - for (String schemaJson : numericSchemas) { - JsonValue result; - result = Json.parse(schemaJson); - JsonValue schema = result; - assertThat(schema).isNotNull(); - LOG.fine(() -> "Numeric type test - schema: " + schema); - } - } - - /// Counter-test: Numeric type validation should fail for non-numeric data - /// Tests that numeric types reject string data - @Test - public void testNumericTypesInvalid() throws Exception { - String schemaJson = "{ \"type\": \"int32\" }"; - JsonValue result; - result = Json.parse(schemaJson); - JsonValue schema = result; - - // Test validation failure - should fail for string data - JsonValue invalidData = Json.parse("\"not-a-number\""); - JtdValidator validator = new JtdValidator(); - ValidationResult invalidResult = validator.validate(schema, invalidData); - assertThat(invalidResult.isValid()).isFalse(); - assertThat(invalidResult.errors()).isNotEmpty(); - LOG.fine(() -> "Numeric types invalid test - schema: " + schema + ", invalid data: " + invalidData + ", errors: " + invalidResult.errors()); - } - - /// Nullable types - @Test - public void testNullableTypes() throws Exception { - String[] nullableSchemas = {"{ \"type\": \"string\", \"nullable\": true }", "{ \"enum\": [\"foo\", \"bar\"], \"nullable\": true }", "{ \"elements\": { \"type\": \"string\" }, \"nullable\": true }"}; - - for (String schemaJson : nullableSchemas) { - JsonValue result; - result = Json.parse(schemaJson); - JsonValue schema = result; - assertThat(schema).isNotNull(); - LOG.fine(() -> "Nullable type test - schema: " + schema); - } - } - - /// Counter-test: Nullable types should still fail for non-matching non-null data - /// Tests that nullable doesn't bypass type validation for non-null values - @Test - public void testNullableTypesInvalid() throws Exception { - String schemaJson = "{ \"type\": \"string\", \"nullable\": true }"; - JsonValue result; - result = Json.parse(schemaJson); - JsonValue schema = result; - - // Test validation failure - should fail for non-string, non-null data - JsonValue invalidData = Json.parse("123"); - JtdValidator validator = new JtdValidator(); - ValidationResult invalidResult = validator.validate(schema, invalidData); - assertThat(invalidResult.isValid()).isFalse(); - assertThat(invalidResult.errors()).isNotEmpty(); - LOG.fine(() -> "Nullable types invalid test - schema: " + schema + ", invalid data: " + invalidData + ", errors: " + invalidResult.errors()); - } -} diff --git a/json-java21-jtd/src/test/java/json/java21/jtd/JtdExhaustiveTest.java.backup b/json-java21-jtd/src/test/java/json/java21/jtd/JtdExhaustiveTest.java similarity index 68% rename from json-java21-jtd/src/test/java/json/java21/jtd/JtdExhaustiveTest.java.backup rename to json-java21-jtd/src/test/java/json/java21/jtd/JtdExhaustiveTest.java index bfc0b40..c2f2b9d 100644 --- a/json-java21-jtd/src/test/java/json/java21/jtd/JtdExhaustiveTest.java.backup +++ b/json-java21-jtd/src/test/java/json/java21/jtd/JtdExhaustiveTest.java @@ -2,12 +2,10 @@ import jdk.sandbox.java.util.json.*; import net.jqwik.api.*; -import net.jqwik.api.providers.ArbitraryProvider; -import net.jqwik.api.providers.TypeUsage; +import org.junit.jupiter.api.Assertions; import java.math.BigDecimal; import java.util.*; -import java.util.logging.Logger; import java.util.stream.Collectors; import static org.assertj.core.api.Assertions.assertThat; @@ -26,6 +24,7 @@ class JtdExhaustiveTest extends JtdTestBase { ); private static final List DISCRIMINATOR_VALUES = List.of("type1", "type2", "type3"); private static final List ENUM_VALUES = List.of("red", "green", "blue", "yellow"); + private static final Random RANDOM = new Random(); @Provide Arbitrary jtdSchemas() { @@ -70,9 +69,9 @@ void exhaustiveJtdValidation(@ForAll("jtdSchemas") JtdExhaustiveTest.JtdTestSche final var failingDocuments = createFailingJtdDocuments(schema, compliantDocument); - // Empty schema accepts everything, so no failing documents are expected - // Nullable schema also accepts null, so may have limited failing cases - if (!(schema instanceof EmptySchema) && !(schema instanceof NullableSchema)) { + // RFC 8927: Empty schema {} and PropertiesSchema with no properties accept everything + // Nullable schema accepts null, so may have limited failing cases + if (!(schema instanceof EmptySchema) && !(schema instanceof NullableSchema) && !isEmptyPropertiesSchema(schema)) { assertThat(failingDocuments) .as("Negative cases should be generated for JTD schema %s", schemaDescription) .isNotEmpty(); @@ -84,7 +83,14 @@ void exhaustiveJtdValidation(@ForAll("jtdSchemas") JtdExhaustiveTest.JtdTestSche LOG.finest(() -> "Failing JTD documents: " + failingDocumentStrings); failingDocuments.forEach(failing -> { + LOG.finest(() -> String.format("Testing failing document: %s against schema: %s", failing, schemaJson)); final var failingResult = validator.validate(schemaJson, failing); + + if (failingResult.isValid()) { + LOG.severe(() -> String.format("UNEXPECTED: Failing document passed validation!%nSchema: %s%nDocument: %s%nExpected: FAILURE, Got: SUCCESS", + schemaJson, failing)); + } + assertThat(failingResult.isValid()) .as("Expected JTD validation failure for %s against schema %s", failing, schemaDescription) .isFalse(); @@ -96,15 +102,15 @@ void exhaustiveJtdValidation(@ForAll("jtdSchemas") JtdExhaustiveTest.JtdTestSche private static JsonValue buildCompliantJtdDocument(JtdTestSchema schema) { return switch (schema) { - case EmptySchema() -> JsonString.of("any value works"); - case RefSchema(var ref) -> JsonString.of("ref-compliant-value"); + case EmptySchema() -> generateAnyJsonValue(); // RFC 8927: {} accepts anything + case RefSchema(var ignored) -> JsonString.of("ref-compliant-value"); case TypeSchema(var type) -> buildCompliantTypeValue(type); case EnumSchema(var values) -> JsonString.of(values.getFirst()); case ElementsSchema(var elementSchema) -> JsonArray.of(List.of( buildCompliantJtdDocument(elementSchema), buildCompliantJtdDocument(elementSchema) )); - case PropertiesSchema(var required, var optional, var additional) -> { + case PropertiesSchema(var required, var optional, var ignored1) -> { final var members = new LinkedHashMap(); required.forEach((key, valueSchema) -> members.put(key, buildCompliantJtdDocument(valueSchema)) @@ -119,26 +125,56 @@ case ValuesSchema(var valueSchema) -> JsonObject.of(Map.of( "key2", buildCompliantJtdDocument(valueSchema) )); case DiscriminatorSchema(var discriminator, var mapping) -> { - final var firstEntry = mapping.entrySet().iterator().next(); - final var discriminatorValue = firstEntry.getKey(); - final var variantSchema = firstEntry.getValue(); - - // Discriminator schemas always generate objects with the discriminator field - final var members = new LinkedHashMap(); - members.put(discriminator, JsonString.of(discriminatorValue)); - - // Add properties based on the variant schema type - if (variantSchema instanceof PropertiesSchema props) { - props.properties().forEach((key, valueSchema) -> - members.put(key, buildCompliantJtdDocument(valueSchema)) - ); - } - // For TypeSchema variants, the object with just the discriminator field should be valid - // For EnumSchema variants, same logic applies - - yield JsonObject.of(members); + final var firstEntry = mapping.entrySet().iterator().next(); + final var discriminatorValue = firstEntry.getKey(); + final var variantSchema = firstEntry.getValue(); + + // Discriminator schemas always generate objects with the discriminator field + final var members = new LinkedHashMap(); + members.put(discriminator, JsonString.of(discriminatorValue)); + + // Add properties based on the variant schema type + if (variantSchema instanceof PropertiesSchema props) { + // Don't re-add the discriminator field when processing properties + props.properties().forEach((key, valueSchema) -> { + if (!key.equals(discriminator)) { // Skip discriminator field to avoid overwriting + members.put(key, buildCompliantJtdDocument(valueSchema)); + } + }); + props.optionalProperties().forEach((key, valueSchema) -> { + if (!key.equals(discriminator)) { // Skip discriminator field to avoid overwriting + members.put(key, buildCompliantJtdDocument(valueSchema)); + } + }); + } + // For TypeSchema variants, the object with just the discriminator field should be valid + // For EnumSchema variants, same logic applies + yield JsonObject.of(members); } - case NullableSchema(var inner) -> JsonNull.of(); + case NullableSchema(var ignored) -> JsonNull.of(); + }; + } + + private static boolean isEmptyPropertiesSchema(JtdTestSchema schema) { + return schema instanceof PropertiesSchema props && + props.properties().isEmpty() && + props.optionalProperties().isEmpty(); + } + + private static JsonValue generateAnyJsonValue() { + // Generate a random JSON value of any type for RFC 8927 empty schema + return switch (RANDOM.nextInt(7)) { + case 0 -> JsonNull.of(); + case 1 -> JsonBoolean.of(RANDOM.nextBoolean()); + case 2 -> JsonNumber.of(RANDOM.nextInt(100)); + case 3 -> JsonNumber.of(RANDOM.nextDouble()); + case 4 -> JsonString.of("random-string-" + RANDOM.nextInt(1000)); + case 5 -> JsonArray.of(List.of(generateAnyJsonValue(), generateAnyJsonValue())); + case 6 -> JsonObject.of(Map.of( + "key" + RANDOM.nextInt(10), generateAnyJsonValue(), + "prop" + RANDOM.nextInt(10), generateAnyJsonValue() + )); + default -> JsonString.of("fallback"); }; } @@ -153,18 +189,17 @@ private static JsonValue buildCompliantTypeValue(String type) { case "uint16" -> JsonNumber.of(50000); case "int32" -> JsonNumber.of(1000000); case "uint32" -> JsonNumber.of(3000000000L); - case "float32" -> JsonNumber.of(new BigDecimal("3.14159")); - case "float64" -> JsonNumber.of(new BigDecimal("3.14159")); - default -> JsonString.of("unknown-type-value"); + case "float32", "float64" -> JsonNumber.of(new BigDecimal("3.14159")); + default -> JsonString.of("unknown-type-value"); }; } private static List createFailingJtdDocuments(JtdTestSchema schema, JsonValue compliant) { return switch (schema) { - case EmptySchema unused -> List.of(); // Empty schema accepts everything - case RefSchema unused -> List.of(JsonNull.of()); // Ref should fail on null + case EmptySchema ignored -> List.of(); // RFC 8927: {} accepts everything - no failing documents + case RefSchema ignored -> List.of(JsonNull.of()); // Ref should fail on null case TypeSchema(var type) -> createFailingTypeValues(type); - case EnumSchema(var values) -> List.of(JsonString.of("invalid-enum-value")); + 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()); @@ -179,6 +214,12 @@ case ElementsSchema(var elementSchema) -> { yield List.of(JsonNull.of()); } case PropertiesSchema(var required, var optional, var additional) -> { + // RFC 8927: PropertiesSchema with no properties behaves like empty schema + if (required.isEmpty() && optional.isEmpty()) { + // No properties defined - this is equivalent to empty schema, accepts everything + yield List.of(); + } + final var failures = new ArrayList(); if (!required.isEmpty()) { final var firstKey = required.keySet().iterator().next(); @@ -190,14 +231,14 @@ case PropertiesSchema(var required, var optional, var additional) -> { failures.add(JsonNull.of()); yield failures; } - case ValuesSchema unused -> List.of(JsonNull.of(), JsonString.of("not-an-object")); - case DiscriminatorSchema(var discriminator, var mapping) -> { + case ValuesSchema ignored -> List.of(JsonNull.of(), JsonString.of("not-an-object")); + case DiscriminatorSchema(var ignored, var ignored1) -> { final var failures = new ArrayList(); failures.add(replaceDiscriminatorValue((JsonObject) compliant, "invalid-discriminator")); failures.add(JsonNull.of()); yield failures; } - case NullableSchema unused -> List.of(); // Nullable accepts null + case NullableSchema ignored -> List.of(); // Nullable accepts null }; } @@ -205,15 +246,9 @@ private static List createFailingTypeValues(String type) { return switch (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" -> List.of(JsonString.of("not-integer"), JsonNumber.of(new BigDecimal("3.14"))); - case "uint8" -> List.of(JsonString.of("not-integer"), JsonNumber.of(new BigDecimal("3.14"))); - case "int16" -> List.of(JsonString.of("not-integer"), JsonNumber.of(new BigDecimal("3.14"))); - case "uint16" -> List.of(JsonString.of("not-integer"), JsonNumber.of(new BigDecimal("3.14"))); - case "int32" -> List.of(JsonString.of("not-integer"), JsonNumber.of(new BigDecimal("3.14"))); - case "uint32" -> List.of(JsonString.of("not-integer"), JsonNumber.of(new BigDecimal("3.14"))); - case "float32" -> List.of(JsonString.of("not-float"), JsonBoolean.of(true)); - case "float64" -> List.of(JsonString.of("not-float"), JsonBoolean.of(true)); - default -> List.of(JsonNull.of()); + case "int8", "uint8", "int16", "int32", "uint32", "uint16" -> List.of(JsonString.of("not-integer"), JsonNumber.of(new BigDecimal("3.14"))); + case "float32", "float64" -> List.of(JsonString.of("not-float"), JsonBoolean.of(true)); + default -> List.of(JsonNull.of()); }; } @@ -229,12 +264,14 @@ private static JsonObject removeProperty(JsonObject original, String missingProp return JsonObject.of(filtered); } + @SuppressWarnings("SameParameterValue") private static JsonObject addExtraProperty(JsonObject original, String extraProperty) { final var extended = new LinkedHashMap<>(original.members()); extended.put(extraProperty, JsonString.of("extra-value")); return JsonObject.of(extended); } + @SuppressWarnings("SameParameterValue") private static JsonValue replaceDiscriminatorValue(JsonObject original, String newValue) { final var modified = new LinkedHashMap<>(original.members()); // Find and replace discriminator field @@ -334,20 +371,7 @@ case DiscriminatorSchema(var discriminator, var mapping) -> }; } - /// Custom arbitrary provider for JTD test schemas - static final class JtdSchemaArbitraryProvider implements ArbitraryProvider { - @Override - public boolean canProvideFor(TypeUsage targetType) { - return targetType.isOfType(JtdExhaustiveTest.JtdTestSchema.class); - } - - @Override - public Set> provideFor(TypeUsage targetType, SubtypeProvider subtypeProvider) { - return Set.of(jtdSchemaArbitrary(MAX_DEPTH)); - } - } - - @SuppressWarnings("unchecked") + @SuppressWarnings("unchecked") private static Arbitrary jtdSchemaArbitrary(int depth) { final var primitives = Arbitraries.of( new EmptySchema(), @@ -361,14 +385,15 @@ private static Arbitrary jtdSchemaArbitrary(int depth) { if (depth == 0) { return (Arbitrary) (Arbitrary) primitives; } - - return (Arbitrary) (Arbitrary) Arbitraries.oneOf( + + //noinspection RedundantCast + return (Arbitrary) (Arbitrary) Arbitraries.oneOf( primitives, enumSchemaArbitrary(), elementsSchemaArbitrary(depth), propertiesSchemaArbitrary(depth), valuesSchemaArbitrary(depth), - discriminatorSchemaArbitrary(depth), + discriminatorSchemaArbitrary(), nullableSchemaArbitrary(depth) ); } @@ -403,24 +428,36 @@ private static Arbitrary propertiesSchemaArbitrary(int depth) { final var singleRequired = Combinators.combine( Arbitraries.of(PROPERTY_NAMES), jtdSchemaArbitrary(childDepth) - ).as((name, schema) -> new PropertiesSchema( - Map.of(name, schema), - Map.of(), - false - )); + ).as((name, schema) -> { + Assertions.assertNotNull(name); + Assertions.assertNotNull(schema); + return new PropertiesSchema( + Map.of(name, schema), + Map.of(), + false + ); + }); final var mixed = Combinators.combine( Arbitraries.of(PROPERTY_PAIRS), jtdSchemaArbitrary(childDepth), jtdSchemaArbitrary(childDepth) - ).as((names, requiredSchema, optionalSchema) -> new PropertiesSchema( - Map.of(names.getFirst(), requiredSchema), - Map.of(names.getLast(), optionalSchema), - false - )); + ).as((names, requiredSchema, optionalSchema) -> { + Assertions.assertNotNull(names); + Assertions.assertNotNull(requiredSchema); + Assertions.assertNotNull(optionalSchema); + return new PropertiesSchema( + Map.of(names.getFirst(), requiredSchema), + Map.of(names.getLast(), optionalSchema), + false + ); + }); - final var withAdditional = mixed.map(props -> - new PropertiesSchema(props.properties(), props.optionalProperties(), true) + final var withAdditional = mixed.map(props -> + { + Assertions.assertNotNull(props); + return new PropertiesSchema(props.properties(), props.optionalProperties(), true); + } ); return Arbitraries.oneOf(empty, singleRequired, mixed, withAdditional); @@ -430,20 +467,80 @@ private static Arbitrary valuesSchemaArbitrary(int depth) { return jtdSchemaArbitrary(depth - 1) .map(ValuesSchema::new); } + /// Creates simple PropertiesSchema instances for discriminator mappings without recursion + /// This prevents stack overflow while ensuring RFC 8927 compliance + private static Arbitrary simplePropertiesSchemaArbitrary() { + // Create primitive schemas that don't recurse + final var primitiveSchemas = Arbitraries.of( + new EmptySchema(), + new TypeSchema("boolean"), + new TypeSchema("string"), + new TypeSchema("int32"), + new EnumSchema(List.of("red", "green", "blue")) + ); - private static Arbitrary discriminatorSchemaArbitrary(int depth) { - final var childDepth = depth - 1; - - return Combinators.combine( + return Arbitraries.oneOf( + // Empty properties schema + Arbitraries.of(new PropertiesSchema(Map.of(), Map.of(), false)), + + // Single required property with primitive schema + Combinators.combine( + Arbitraries.of(PROPERTY_NAMES), + primitiveSchemas + ).as((name, schema) -> { + Assertions.assertNotNull(name); + Assertions.assertNotNull(schema); + return new PropertiesSchema( + Map.of(name, schema), + Map.of(), + false + ); + }), + + // Single optional property with primitive schema + Combinators.combine( + Arbitraries.of(PROPERTY_NAMES), + primitiveSchemas + ).as((name, schema) -> { + Assertions.assertNotNull(name); + Assertions.assertNotNull(schema); + return new PropertiesSchema( + Map.of(), + Map.of(name, schema), + false + ); + }), + + // Required + optional property with primitive schemas + Combinators.combine( + Arbitraries.of(PROPERTY_PAIRS), + primitiveSchemas, + primitiveSchemas + ).as((names, requiredSchema, optionalSchema) -> { + Assertions.assertNotNull(names); + Assertions.assertNotNull(requiredSchema); + Assertions.assertNotNull(optionalSchema); + return new PropertiesSchema( + Map.of(names.getFirst(), requiredSchema), + Map.of(names.get(1), optionalSchema), + false + ); + }) + ); + } + private static Arbitrary discriminatorSchemaArbitrary() { + + return Combinators.combine( Arbitraries.of(PROPERTY_NAMES), Arbitraries.of(DISCRIMINATOR_VALUES), Arbitraries.of(DISCRIMINATOR_VALUES), - jtdSchemaArbitrary(childDepth), - jtdSchemaArbitrary(childDepth) + simplePropertiesSchemaArbitrary(), + simplePropertiesSchemaArbitrary() ).as((discriminatorKey, value1, value2, schema1, schema2) -> { final var mapping = new LinkedHashMap(); mapping.put(value1, schema1); - if (!value1.equals(value2)) { + Assertions.assertNotNull(value1); + if (!value1.equals(value2)) { mapping.put(value2, schema2); } return new DiscriminatorSchema(discriminatorKey, mapping); @@ -474,4 +571,4 @@ record PropertiesSchema( record ValuesSchema(JtdTestSchema values) implements JtdTestSchema {} record DiscriminatorSchema(String discriminator, Map mapping) implements JtdTestSchema {} record NullableSchema(JtdTestSchema schema) implements JtdTestSchema {} -} \ No newline at end of file +} diff --git a/json-java21-jtd/src/test/java/json/java21/jtd/JtdSpecIT.java b/json-java21-jtd/src/test/java/json/java21/jtd/JtdSpecIT.java index d0510c5..6ccdf35 100644 --- a/json-java21-jtd/src/test/java/json/java21/jtd/JtdSpecIT.java +++ b/json-java21-jtd/src/test/java/json/java21/jtd/JtdSpecIT.java @@ -19,7 +19,6 @@ import static org.junit.jupiter.api.Assertions.assertEquals; /// Runs the official JTD Test Suite as JUnit dynamic tests. -/// Based on the pattern from JsonSchemaCheckDraft4IT but simplified for JTD. /// /// This test class loads and runs two types of tests from the JTD specification: /// @@ -100,7 +99,7 @@ private Stream runValidationTests() throws Exception { LOG.info(() -> "Running validation tests from: " + VALIDATION_TEST_FILE); JsonNode validationSuite = loadTestFile(VALIDATION_TEST_FILE); - return StreamSupport.stream(((Iterable>) () -> validationSuite.fields()).spliterator(), false) + return StreamSupport.stream(((Iterable>) validationSuite::fields).spliterator(), false) .map(entry -> { String testName = "validation: " + entry.getKey(); JsonNode testCase = entry.getValue(); @@ -112,7 +111,7 @@ private Stream runInvalidSchemaTests() throws Exception { LOG.info(() -> "Running invalid schema tests from: " + INVALID_SCHEMAS_FILE); JsonNode invalidSchemas = loadTestFile(INVALID_SCHEMAS_FILE); - return StreamSupport.stream(((Iterable>) () -> invalidSchemas.fields()).spliterator(), false) + return StreamSupport.stream(((Iterable>) invalidSchemas::fields).spliterator(), false) .map(entry -> { String testName = "invalid schema: " + entry.getKey(); JsonNode schema = entry.getValue(); @@ -193,7 +192,7 @@ private DynamicTest createValidationTest(String testName, JsonNode testCase) { Jtd.Result result = validator.validate(schema, instance); // Check if validation result matches expected - boolean expectedValid = expectedErrorsNode.isArray() && expectedErrorsNode.size() == 0; + boolean expectedValid = expectedErrorsNode.isArray() && expectedErrorsNode.isEmpty(); boolean actualValid = result.isValid(); if (expectedValid != actualValid) { diff --git a/json-java21-jtd/src/test/java/json/java21/jtd/JtdTestBase.java b/json-java21-jtd/src/test/java/json/java21/jtd/JtdTestBase.java index 2d24718..a3943e5 100644 --- a/json-java21-jtd/src/test/java/json/java21/jtd/JtdTestBase.java +++ b/json-java21-jtd/src/test/java/json/java21/jtd/JtdTestBase.java @@ -8,7 +8,7 @@ /// Base class for all JTD tests. /// - Emits an INFO banner per test. /// - Provides common helpers for loading resources and assertions. -class JtdTestBase extends JtdLoggingConfig { +public class JtdTestBase extends JtdLoggingConfig { static final Logger LOG = Logger.getLogger("json.java21.jtd"); @@ -19,4 +19,4 @@ void announce(TestInfo testInfo) { .orElseGet(testInfo::getDisplayName); LOG.info(() -> "TEST: " + cls + "#" + name); } -} \ No newline at end of file +} 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 9beacc5..bfd53e6 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 @@ -19,7 +19,7 @@ public class TestRfc8927 extends JtdTestBase { /// Test ref schema resolution with valid definitions /// RFC 8927 Section 3.3.2: Ref schemas must resolve against definitions @Test - public void testRefSchemaValid() throws Exception { + public void testRefSchemaValid() { JsonValue schema = Json.parse("{\"ref\": \"address\", \"definitions\": {\"address\": {\"type\": \"string\"}}}"); JsonValue validData = Json.parse("\"123 Main St\""); @@ -34,7 +34,7 @@ public void testRefSchemaValid() throws Exception { /// Counter-test: Ref schema with invalid definition reference /// Should fail when ref points to non-existent definition @Test - public void testRefSchemaInvalidDefinition() throws Exception { + public void testRefSchemaInvalidDefinition() { JsonValue schema = Json.parse("{\"ref\": \"nonexistent\", \"definitions\": {\"address\": {\"type\": \"string\"}}}"); JsonValue data = Json.parse("\"anything\""); @@ -50,7 +50,7 @@ public void testRefSchemaInvalidDefinition() throws Exception { /// Test timestamp format validation (RFC 3339) /// RFC 8927 Section 3.3.3: timestamp must follow RFC 3339 format @Test - public void testTimestampValid() throws Exception { + public void testTimestampValid() { JsonValue schema = Json.parse("{\"type\": \"timestamp\"}"); // Valid RFC 3339 timestamps @@ -74,7 +74,7 @@ public void testTimestampValid() throws Exception { /// Counter-test: Invalid timestamp formats /// Should reject non-RFC 3339 timestamp strings @Test - public void testTimestampInvalid() throws Exception { + public void testTimestampInvalid() { JsonValue schema = Json.parse("{\"type\": \"timestamp\"}"); // Invalid timestamp formats @@ -104,7 +104,7 @@ public void testTimestampInvalid() throws Exception { /// Test integer type range validation /// RFC 8927 Table 2: Specific ranges for each integer type @Test - public void testIntegerRangesValid() throws Exception { + public void testIntegerRangesValid() { // Test valid ranges for each integer type testIntegerTypeRange("int8", "-128", "127", "0"); testIntegerTypeRange("uint8", "0", "255", "128"); @@ -117,7 +117,7 @@ public void testIntegerRangesValid() throws Exception { /// Counter-test: Integer values outside valid ranges /// Should reject values that exceed type ranges @Test - public void testIntegerRangesInvalid() throws Exception { + public void testIntegerRangesInvalid() { // Test invalid ranges for each integer type testIntegerTypeInvalid("int8", "-129", "128"); // Below min, above max testIntegerTypeInvalid("uint8", "-1", "256"); // Below min, above max @@ -128,7 +128,7 @@ public void testIntegerRangesInvalid() throws Exception { } /// Helper method to test valid integer ranges - private void testIntegerTypeRange(String type, String min, String max, String middle) throws Exception { + private void testIntegerTypeRange(String type, String min, String max, String middle) { JsonValue schema = Json.parse("{\"type\": \"" + type + "\"}"); Jtd validator = new Jtd(); @@ -144,7 +144,7 @@ private void testIntegerTypeRange(String type, String min, String max, String mi } /// Helper method to test invalid integer ranges - private void testIntegerTypeInvalid(String type, String belowMin, String aboveMax) throws Exception { + private void testIntegerTypeInvalid(String type, String belowMin, String aboveMax) { JsonValue schema = Json.parse("{\"type\": \"" + type + "\"}"); Jtd validator = new Jtd(); @@ -165,7 +165,7 @@ private void testIntegerTypeInvalid(String type, String belowMin, String aboveMa /// Test error path information (instancePath and schemaPath) /// RFC 8927 Section 3.2: All errors must include instancePath and schemaPath @Test - public void testErrorPathInformation() throws Exception { + public void testErrorPathInformation() { JsonValue schema = Json.parse("{\"properties\": {\"name\": {\"type\": \"string\"}, \"age\": {\"type\": \"int32\"}}}"); JsonValue invalidData = Json.parse("{\"name\": 123, \"age\": \"not-a-number\"}"); @@ -185,7 +185,7 @@ public void testErrorPathInformation() throws Exception { /// Test multiple error collection /// Should collect all validation errors, not just the first one @Test - public void testMultipleErrorCollection() throws Exception { + public void testMultipleErrorCollection() { JsonValue schema = Json.parse("{\"elements\": {\"type\": \"string\"}}"); JsonValue invalidData = Json.parse("[123, 456, \"valid\", 789]"); @@ -209,7 +209,7 @@ public void testMultipleErrorCollection() throws Exception { /// Test discriminator tag exemption /// RFC 8927 §2.2.8: Only the discriminator field itself is exempt from additionalProperties enforcement @Test - public void testDiscriminatorTagExemption() throws Exception { + public void testDiscriminatorTagExemption() { JsonValue schema = Json.parse("{\"discriminator\": \"type\", \"mapping\": {\"person\": {\"properties\": {\"name\": {\"type\": \"string\"}}}}}"); // Valid data with discriminator and no additional properties @@ -235,7 +235,7 @@ public void testDiscriminatorTagExemption() throws Exception { /// Counter-test: Discriminator with invalid mapping /// Should fail when discriminator value is not in mapping @Test - public void testDiscriminatorInvalidMapping() throws Exception { + public void testDiscriminatorInvalidMapping() { JsonValue schema = Json.parse("{\"discriminator\": \"type\", \"mapping\": {\"person\": {\"properties\": {\"name\": {\"type\": \"string\"}}}}}"); JsonValue invalidData = Json.parse("{\"type\": \"invalid\", \"name\": \"John\"}"); @@ -250,7 +250,7 @@ public void testDiscriminatorInvalidMapping() throws Exception { /// Test float type validation /// RFC 8927 Section 3.3.3: float32 and float64 validation @Test - public void testFloatTypesValid() throws Exception { + public void testFloatTypesValid() { JsonValue schema32 = Json.parse("{\"type\": \"float32\"}"); JsonValue schema64 = Json.parse("{\"type\": \"float64\"}"); @@ -273,7 +273,7 @@ public void testFloatTypesValid() throws Exception { /// Counter-test: Invalid float values /// Should reject non-numeric values for float types @Test - public void testFloatTypesInvalid() throws Exception { + public void testFloatTypesInvalid() { JsonValue schema = Json.parse("{\"type\": \"float32\"}"); // Invalid values for float @@ -294,7 +294,7 @@ public void testFloatTypesInvalid() throws Exception { /// Test boolean type validation /// RFC 8927 Section 3.3.3: boolean type validation @Test - public void testBooleanTypeValid() throws Exception { + public void testBooleanTypeValid() { JsonValue schema = Json.parse("{\"type\": \"boolean\"}"); Jtd validator = new Jtd(); @@ -314,7 +314,7 @@ public void testBooleanTypeValid() throws Exception { /// Counter-test: Invalid boolean values /// Should reject non-boolean values @Test - public void testBooleanTypeInvalid() throws Exception { + public void testBooleanTypeInvalid() { JsonValue schema = Json.parse("{\"type\": \"boolean\"}"); // Invalid values for boolean @@ -334,7 +334,7 @@ public void testBooleanTypeInvalid() throws Exception { /// Test nullable default behavior - non-nullable schemas must reject null @Test - public void testNonNullableBooleanRejectsNull() throws Exception { + public void testNonNullableBooleanRejectsNull() { JsonValue schema = Json.parse("{\"type\":\"boolean\"}"); JsonValue instance = Json.parse("null"); Jtd.Result result = new Jtd().validate(schema, instance); @@ -344,7 +344,7 @@ public void testNonNullableBooleanRejectsNull() throws Exception { /// Test nullable boolean accepts null when explicitly nullable @Test - public void testNullableBooleanAcceptsNull() throws Exception { + public void testNullableBooleanAcceptsNull() { JsonValue schema = Json.parse("{\"type\":\"boolean\",\"nullable\":true}"); JsonValue instance = Json.parse("null"); Jtd.Result result = new Jtd().validate(schema, instance); @@ -353,7 +353,7 @@ public void testNullableBooleanAcceptsNull() throws Exception { /// Test timestamp validation with leap second @Test - public void testTimestampLeapSecond() throws Exception { + public void testTimestampLeapSecond() { JsonValue schema = Json.parse("{\"type\":\"timestamp\"}"); JsonValue instance = Json.parse("\"1990-12-31T23:59:60Z\""); Jtd.Result result = new Jtd().validate(schema, instance); @@ -362,7 +362,7 @@ public void testTimestampLeapSecond() throws Exception { /// Test timestamp validation with timezone offset @Test - public void testTimestampWithTimezoneOffset() throws Exception { + public void testTimestampWithTimezoneOffset() { JsonValue schema = Json.parse("{\"type\":\"timestamp\"}"); JsonValue instance = Json.parse("\"1990-12-31T15:59:60-08:00\""); Jtd.Result result = new Jtd().validate(schema, instance); @@ -371,7 +371,7 @@ public void testTimestampWithTimezoneOffset() throws Exception { /// Test nested ref schema resolution @Test - public void testRefSchemaNested() throws Exception { + public void testRefSchemaNested() { JsonValue schema = Json.parse(""" { "definitions": { @@ -387,7 +387,7 @@ public void testRefSchemaNested() throws Exception { /// Test recursive ref schema resolution @Test - public void testRefSchemaRecursive() throws Exception { + public void testRefSchemaRecursive() { JsonValue schema = Json.parse(""" { "definitions": { @@ -408,7 +408,7 @@ public void testRefSchemaRecursive() throws Exception { /// Test recursive ref schema validation - should reject invalid nested data /// "ref schema - recursive schema, bad" from JTD specification test suite @Test - public void testRefSchemaRecursiveBad() throws Exception { + public void testRefSchemaRecursiveBad() { JsonValue schema = Json.parse(""" { "definitions": { @@ -445,7 +445,7 @@ public void testRefSchemaRecursiveBad() throws Exception { /// Micro test to debug int32 validation with decimal values /// Should reject non-integer values like 3.14 for int32 type @Test - public void testInt32RejectsDecimal() throws Exception { + public void testInt32RejectsDecimal() { JsonValue schema = Json.parse("{\"type\": \"int32\"}"); JsonValue decimalValue = JsonNumber.of(new java.math.BigDecimal("3.14")); @@ -474,7 +474,7 @@ public void testInt32RejectsDecimal() throws Exception { /// RFC 8927 §2.2.3.1: "An integer value is a number without a fractional component" /// Values like 3.0, 3.000 are valid integers despite positive scale @Test - public void testIntegerTypesAcceptTrailingZeros() throws Exception { + public void testIntegerTypesAcceptTrailingZeros() { JsonValue schema = Json.parse("{\"type\": \"int32\"}"); // Valid integer representations with trailing zeros @@ -504,7 +504,7 @@ public void testIntegerTypesAcceptTrailingZeros() throws Exception { /// Test that integer types reject values with actual fractional components /// RFC 8927 §2.2.3.1: "An integer value is a number without a fractional component" @Test - public void testIntegerTypesRejectFractionalComponents() throws Exception { + public void testIntegerTypesRejectFractionalComponents() { JsonValue schema = Json.parse("{\"type\": \"int32\"}"); // Invalid values with actual fractional components @@ -534,7 +534,7 @@ public void testIntegerTypesRejectFractionalComponents() throws Exception { /// Test for Issue #91: additionalProperties should default to false when no properties defined /// Empty properties schema should reject additional properties @Test - public void testAdditionalPropertiesDefaultsToFalse() throws Exception { + public void testAdditionalPropertiesDefaultsToFalse() { JsonValue schema = Json.parse("{\"elements\": {\"properties\": {}}}"); JsonValue invalidData = Json.parse("[{\"extraProperty\":\"extra-value\"}]"); @@ -550,59 +550,89 @@ public void testAdditionalPropertiesDefaultsToFalse() throws Exception { .isNotEmpty(); } - /// Test case from JtdExhaustiveTest property test failure - /// Schema: {"elements":{"properties":{"alpha":{"discriminator":"alpha","mapping":{"type1":{"type":"boolean"}}}}}} - /// Document: [{"alpha":{"alpha":"type1"}},{"alpha":{"alpha":"type1"}}] - /// This should pass validation but currently fails with "expected boolean, got JsonObjectImpl" + /// Test discriminator schema nested within elements schema (RFC 8927 compliant) + /// Schema has array elements with discriminator properties that map to valid properties forms @Test - public void testDiscriminatorInElementsSchema() throws Exception { + public void testDiscriminatorInElementsSchema() { JsonValue schema = Json.parse(""" - { - "elements": { - "properties": { - "alpha": { - "discriminator": "alpha", - "mapping": { - "type1": {"type": "boolean"} + { + "elements": { + "properties": { + "alpha": { + "discriminator": "type", + "mapping": { + "config": { + "properties": { + "type": {}, + "value": {"type": "string"} + }, + "additionalProperties": false + }, + "flag": { + "properties": { + "type": {}, + "enabled": {"type": "boolean"} + }, + "additionalProperties": false } } } - } + }, + "additionalProperties": false } - """); - JsonValue document = Json.parse(""" - [ - {"alpha": {"alpha": "type1"}}, - {"alpha": {"alpha": "type1"}} - ] - """); - - LOG.info(() -> "Testing discriminator in elements schema - property test failure case"); + } + """); + + JsonValue validDocument = Json.parse(""" + [ + {"alpha": {"type": "config", "value": "test"}}, + {"alpha": {"type": "flag", "enabled": true}} + ] + """); + + JsonValue invalidDocument = Json.parse(""" + [ + {"alpha": {"type": "config"}}, + {"alpha": {"type": "flag", "enabled": true}} + ] + """); + + LOG.info(() -> "Testing RFC 8927 compliant discriminator in elements schema"); LOG.fine(() -> "Schema: " + schema); - LOG.fine(() -> "Document: " + document); - + LOG.fine(() -> "Valid document: " + validDocument); + LOG.fine(() -> "Invalid document: " + invalidDocument); + Jtd validator = new Jtd(); - Jtd.Result result = validator.validate(schema, document); - - LOG.fine(() -> "Validation result: " + (result.isValid() ? "VALID" : "INVALID")); - if (!result.isValid()) { - LOG.fine(() -> "Errors: " + result.errors()); + + // Valid case: all required properties present + Jtd.Result validResult = validator.validate(schema, validDocument); + LOG.fine(() -> "Valid validation result: " + (validResult.isValid() ? "VALID" : "INVALID")); + if (!validResult.isValid()) { + LOG.fine(() -> "Valid errors: " + validResult.errors()); } - - // This should be valid according to the property test expectation - // but currently fails with "expected boolean, got JsonObjectImpl" - assertThat(result.isValid()) - .as("Discriminator in elements schema should validate the property test case") - .isTrue(); - } + assertThat(validResult.isValid()) + .as("RFC 8927 compliant discriminator in elements should validate correctly") + .isTrue(); + + // Invalid case: missing required property in first element + Jtd.Result invalidResult = validator.validate(schema, invalidDocument); + LOG.fine(() -> "Invalid validation result: " + (invalidResult.isValid() ? "VALID" : "INVALID")); + if (!invalidResult.isValid()) { + LOG.fine(() -> "Invalid errors: " + invalidResult.errors()); + } + + assertThat(invalidResult.isValid()) + .as("Should reject document with missing required properties") + .isFalse(); + } /// Test case from JtdExhaustiveTest property test failure /// Nested elements containing properties schemas should reject additional properties /// Schema: {"elements":{"elements":{"properties":{}}}} /// Document: [[{},{},[{},{extraProperty":"extra-value"}]] /// This should fail validation but currently passes incorrectly @Test - public void testNestedElementsPropertiesRejectsAdditionalProperties() throws Exception { + public void testNestedElementsPropertiesRejectsAdditionalProperties() { JsonValue schema = Json.parse(""" { "elements": { @@ -640,4 +670,220 @@ public void testNestedElementsPropertiesRejectsAdditionalProperties() throws Exc .as("Should have validation errors for additional property") .isNotEmpty(); } + + /// Test for Issue #99: RFC 8927 empty form semantics + /// Empty schema {} accepts everything, including objects with properties + @Test + public void testEmptySchemaAcceptsObjectsWithProperties() { + JsonValue schema = Json.parse("{}"); + JsonValue document = Json.parse("{\"extraProperty\":\"extra-value\"}"); + + LOG.info(() -> "Testing empty schema {} - should accept objects with properties per RFC 8927"); + LOG.fine(() -> "Schema: " + schema); + LOG.fine(() -> "Document: " + document); + + Jtd validator = new Jtd(); + Jtd.Result result = validator.validate(schema, document); + + LOG.fine(() -> "Validation result: " + (result.isValid() ? "VALID" : "INVALID")); + + // RFC 8927 §3.3.1: Empty form accepts all instances, including objects with properties + assertThat(result.isValid()) + .as("Empty schema {} should accept objects with properties per RFC 8927") + .isTrue(); + assertThat(result.errors()) + .as("Empty schema should produce no validation errors") + .isEmpty(); + } + + /// Test case for Issue #99: RFC 8927 {} empty form semantics + /// Empty schema {} must accept all JSON instances per RFC 8927 §3.3.1 + @Test + public void testEmptySchemaAcceptsAnything_perRfc8927() { + JsonValue schema = Json.parse("{}"); + Jtd validator = new Jtd(); + + // RFC 8927 §3.3.1: "If a schema is of the 'empty' form, then it accepts all instances" + assertThat(validator.validate(schema, Json.parse("null")).isValid()).isTrue(); + assertThat(validator.validate(schema, Json.parse("true")).isValid()).isTrue(); + assertThat(validator.validate(schema, Json.parse("123")).isValid()).isTrue(); + assertThat(validator.validate(schema, Json.parse("3.14")).isValid()).isTrue(); + assertThat(validator.validate(schema, Json.parse("\"foo\"")).isValid()).isTrue(); + assertThat(validator.validate(schema, Json.parse("[]")).isValid()).isTrue(); + assertThat(validator.validate(schema, Json.parse("{}")).isValid()).isTrue(); + } + + /// Test $ref to empty schema also accepts anything per RFC 8927 + @Test + public void testRefToEmptySchemaAcceptsAnything() { + JsonValue schema = Json.parse(""" + { + "definitions": { "foo": {} }, + "ref": "foo" + } + """); + + Jtd validator = new Jtd(); + assertThat(validator.validate(schema, Json.parse("false")).isValid()).isTrue(); + assertThat(validator.validate(schema, Json.parse("\"bar\"")).isValid()).isTrue(); + assertThat(validator.validate(schema, Json.parse("[]")).isValid()).isTrue(); + assertThat(validator.validate(schema, Json.parse("{}")).isValid()).isTrue(); + } + + /// Test discriminator form with empty schema for discriminator property + /// RFC 8927 §2.4: Discriminator mapping schemas must use empty schema {} for discriminator property + /// The discriminator property itself should not be re-validated against the empty schema + @Test + public void testDiscriminatorFormWithEmptySchemaProperty() { + JsonValue schema = Json.parse(""" + { + "discriminator": "alpha", + "mapping": { + "type1": { + "properties": { + "alpha": {} + } + } + } + } + """); + + // Valid: discriminator value matches mapping key + JsonValue validDocument = Json.parse("{\"alpha\": \"type1\"}"); + + // Invalid: discriminator value doesn't match any mapping key + JsonValue invalidDocument = Json.parse("{\"alpha\": \"wrong\"}"); + + Jtd validator = new Jtd(); + + // Should pass - discriminator value "type1" is in mapping + Jtd.Result validResult = validator.validate(schema, validDocument); + assertThat(validResult.isValid()) + .as("Discriminator with empty schema property should accept valid discriminator value") + .isTrue(); + assertThat(validResult.errors()) + .as("Should have no validation errors for valid discriminator") + .isEmpty(); + + // Should fail - discriminator value "wrong" is not in mapping + Jtd.Result invalidResult = validator.validate(schema, invalidDocument); + assertThat(invalidResult.isValid()) + .as("Discriminator should reject invalid discriminator value") + .isFalse(); + assertThat(invalidResult.errors()) + .as("Should have validation errors for invalid discriminator") + .isNotEmpty(); + + LOG.fine(() -> "Discriminator empty schema test - valid: " + validDocument + ", invalid: " + invalidDocument); + } + + /// Test discriminator form with additional required properties + /// Ensures discriminator field exemption doesn't break other property validation + @Test + public void testDiscriminatorWithAdditionalRequiredProperties() { + JsonValue schema = Json.parse(""" + { + "discriminator": "type", + "mapping": { + "user": { + "properties": { + "type": {}, + "name": {"type": "string"} + }, + "additionalProperties": false + } + } + } + """); + + // Valid: has discriminator and required property + JsonValue validDocument = Json.parse("{\"type\": \"user\", \"name\": \"John\"}"); + + // Invalid: missing required property (not discriminator) + JsonValue invalidDocument = Json.parse("{\"type\": \"user\"}"); + + Jtd validator = new Jtd(); + + Jtd.Result validResult = validator.validate(schema, validDocument); + assertThat(validResult.isValid()) + .as("Should accept document with discriminator and all required properties") + .isTrue(); + + Jtd.Result invalidResult = validator.validate(schema, invalidDocument); + assertThat(invalidResult.isValid()) + .as("Should reject document missing non-discriminator required properties") + .isFalse(); + assertThat(invalidResult.errors()) + .as("Should report missing required property") + .anyMatch(error -> error.contains("missing required property: name")); + } + + /// Test discriminator form with optional properties + /// Ensures discriminator field exemption works with optional properties too + @Test + public void testDiscriminatorWithOptionalProperties() { + JsonValue schema = Json.parse(""" + { + "discriminator": "kind", + "mapping": { + "circle": { + "properties": { + "kind": {} + }, + "optionalProperties": { + "radius": {"type": "float32"} + }, + "additionalProperties": false + } + } + } + """); + + // Valid: discriminator only + JsonValue minimalDocument = Json.parse("{\"kind\": \"circle\"}"); + + // Valid: discriminator with optional property + JsonValue withOptionalDocument = Json.parse("{\"kind\": \"circle\", \"radius\": 5.5}"); + + Jtd validator = new Jtd(); + + Jtd.Result minimalResult = validator.validate(schema, minimalDocument); + assertThat(minimalResult.isValid()) + .as("Should accept document with only discriminator") + .isTrue(); + + Jtd.Result optionalResult = validator.validate(schema, withOptionalDocument); + assertThat(optionalResult.isValid()) + .as("Should accept document with discriminator and optional property") + .isTrue(); + } + + /// Test discriminator form where discriminator appears in optionalProperties + /// Edge case: discriminator field might be in optionalProperties instead of properties + @Test + public void testDiscriminatorInOptionalProperties() { + JsonValue schema = Json.parse(""" + { + "discriminator": "mode", + "mapping": { + "default": { + "optionalProperties": { + "mode": {}, + "config": {"type": "string"} + }, + "additionalProperties": false + } + } + } + """); + + JsonValue validDocument = Json.parse("{\"mode\": \"default\"}"); + + Jtd validator = new Jtd(); + + Jtd.Result result = validator.validate(schema, validDocument); + assertThat(result.isValid()) + .as("Should accept discriminator field in optionalProperties") + .isTrue(); + } } diff --git a/json-java21-jtd/src/test/java/json/java21/jtd/TestRfc8927Compliance.java b/json-java21-jtd/src/test/java/json/java21/jtd/TestRfc8927Compliance.java index f7cacd5..3c522c4 100644 --- a/json-java21-jtd/src/test/java/json/java21/jtd/TestRfc8927Compliance.java +++ b/json-java21-jtd/src/test/java/json/java21/jtd/TestRfc8927Compliance.java @@ -14,8 +14,9 @@ public class TestRfc8927Compliance extends JtdTestBase { /// Test ref schema with nested definitions /// "ref schema - nested ref" from JTD specification test suite /// Should resolve nested ref "bar" inside definition "foo" + /// RFC 8927: {} accepts anything - ref to {} should also accept anything @Test - public void testRefSchemaNestedRef() throws Exception { + public void testRefSchemaNestedRef() { // Schema with nested ref: foo references bar, bar is empty schema JsonValue schema = Json.parse(""" { @@ -29,7 +30,7 @@ public void testRefSchemaNestedRef() throws Exception { } """); - JsonValue instance = Json.parse("true"); + JsonValue instance = Json.parse("\"anything\""); // RFC 8927: {} accepts anything via ref LOG.info(() -> "Testing ref schema - nested ref"); LOG.fine(() -> "Schema: " + schema); @@ -50,10 +51,10 @@ public void testRefSchemaNestedRef() throws Exception { } /// Test ref schema with recursive definitions - /// "ref schema - recursive schema, ok" from JTD specification test suite + /// "ref schema - recursive schema, OK" from JTD specification test suite /// Should handle recursive ref to self in elements schema @Test - public void testRefSchemaRecursive() throws Exception { + public void testRefSchemaRecursive() { // Schema with recursive ref: root references itself in elements JsonValue schema = Json.parse(""" { @@ -92,7 +93,7 @@ public void testRefSchemaRecursive() throws Exception { /// "timestamp type schema - 1990-12-31T23:59:60Z" from JTD specification test suite /// Should accept valid RFC 3339 timestamp with leap second @Test - public void testTimestampWithLeapSecond() throws Exception { + public void testTimestampWithLeapSecond() { JsonValue schema = Json.parse(""" { "type": "timestamp" @@ -123,7 +124,7 @@ public void testTimestampWithLeapSecond() throws Exception { /// "timestamp type schema - 1990-12-31T15:59:60-08:00" from JTD specification test suite /// Should accept valid RFC 3339 timestamp with timezone offset @Test - public void testTimestampWithTimezone() throws Exception { + public void testTimestampWithTimezone() { JsonValue schema = Json.parse(""" { "type": "timestamp" @@ -154,7 +155,7 @@ public void testTimestampWithTimezone() throws Exception { /// "strict properties - bad additional property" from JTD specification test suite /// Should reject object with additional property not in schema @Test - public void testStrictPropertiesBadAdditionalProperty() throws Exception { + public void testStrictPropertiesBadAdditionalProperty() { JsonValue schema = Json.parse(""" { "properties": { @@ -199,7 +200,7 @@ public void testStrictPropertiesBadAdditionalProperty() throws Exception { /// "strict optionalProperties - bad additional property" from JTD specification test suite /// Should reject object with additional property not in optionalProperties @Test - public void testStrictOptionalPropertiesBadAdditionalProperty() throws Exception { + public void testStrictOptionalPropertiesBadAdditionalProperty() { JsonValue schema = Json.parse(""" { "optionalProperties": { @@ -244,7 +245,7 @@ public void testStrictOptionalPropertiesBadAdditionalProperty() throws Exception /// "strict mixed properties and optionalProperties - bad additional property" from JTD specification test suite /// Should reject object with additional property not in properties or optionalProperties @Test - public void testStrictMixedPropertiesBadAdditionalProperty() throws Exception { + public void testStrictMixedPropertiesBadAdditionalProperty() { JsonValue schema = Json.parse(""" { "properties": { diff --git a/json-java21-jtd/src/test/java/json/java21/jtd/TestValidationErrors.java b/json-java21-jtd/src/test/java/json/java21/jtd/TestValidationErrors.java index a5f8da0..52194f5 100644 --- a/json-java21-jtd/src/test/java/json/java21/jtd/TestValidationErrors.java +++ b/json-java21-jtd/src/test/java/json/java21/jtd/TestValidationErrors.java @@ -12,7 +12,7 @@ public class TestValidationErrors extends JtdTestBase { /// Test that TypeSchema validation errors are standardized @Test - public void testTypeSchemaErrorMessages() throws Exception { + public void testTypeSchemaErrorMessages() { JsonValue booleanSchema = Json.parse("{\"type\": \"boolean\"}"); JsonValue stringSchema = Json.parse("{\"type\": \"string\"}"); JsonValue intSchema = Json.parse("{\"type\": \"int32\"}"); @@ -27,38 +27,38 @@ public void testTypeSchemaErrorMessages() throws Exception { Jtd.Result booleanResult = validator.validate(booleanSchema, invalidData); assertThat(booleanResult.isValid()).isFalse(); assertThat(booleanResult.errors()).hasSize(1); - assertThat(booleanResult.errors().get(0)).contains("expected boolean, got JsonNumber"); + assertThat(booleanResult.errors().getFirst()).contains("expected boolean, got JsonNumber"); // Test string type error Jtd.Result stringResult = validator.validate(stringSchema, invalidData); assertThat(stringResult.isValid()).isFalse(); assertThat(stringResult.errors()).hasSize(1); - assertThat(stringResult.errors().get(0)).contains("expected string, got JsonNumber"); + assertThat(stringResult.errors().getFirst()).contains("expected string, got JsonNumber"); // Test integer type error Jtd.Result intResult = validator.validate(intSchema, Json.parse("\"not-a-number\"")); assertThat(intResult.isValid()).isFalse(); assertThat(intResult.errors()).hasSize(1); - assertThat(intResult.errors().get(0)).contains("expected int32, got JsonString"); + assertThat(intResult.errors().getFirst()).contains("expected int32, got JsonString"); // Test float type error Jtd.Result floatResult = validator.validate(floatSchema, Json.parse("\"not-a-float\"")); assertThat(floatResult.isValid()).isFalse(); assertThat(floatResult.errors()).hasSize(1); - assertThat(floatResult.errors().get(0)).contains("expected float32, got JsonString"); + assertThat(floatResult.errors().getFirst()).contains("expected float32, got JsonString"); // Test timestamp type error Jtd.Result timestampResult = validator.validate(timestampSchema, invalidData); assertThat(timestampResult.isValid()).isFalse(); assertThat(timestampResult.errors()).hasSize(1); - assertThat(timestampResult.errors().get(0)).contains("expected timestamp (string), got JsonNumber"); + assertThat(timestampResult.errors().getFirst()).contains("expected timestamp (string), got JsonNumber"); LOG.fine(() -> "Type schema error messages test completed successfully"); } /// Test verbose error mode includes actual JSON values @Test - public void testVerboseErrorMode() throws Exception { + public void testVerboseErrorMode() { // Test direct schema validation with verbose errors JsonValue schema = Json.parse("{\"type\": \"boolean\"}"); JsonValue invalidData = Json.parse("{\"key\": \"value\", \"nested\": {\"item\": 123}}"); @@ -69,7 +69,7 @@ public void testVerboseErrorMode() throws Exception { Jtd.Result conciseResult = jtdSchema.validate(invalidData); assertThat(conciseResult.isValid()).isFalse(); assertThat(conciseResult.errors()).hasSize(1); - String conciseError = conciseResult.errors().get(0); + String conciseError = conciseResult.errors() .getFirst(); assertThat(conciseError).doesNotContain("(was:"); LOG.fine(() -> "Concise error: " + conciseError); @@ -77,7 +77,7 @@ public void testVerboseErrorMode() throws Exception { Jtd.Result verboseResult = jtdSchema.validate(invalidData, true); assertThat(verboseResult.isValid()).isFalse(); assertThat(verboseResult.errors()).hasSize(1); - String verboseError = verboseResult.errors().get(0); + String verboseError = verboseResult.errors() .getFirst(); assertThat(verboseError).contains("(was:"); assertThat(verboseError).contains("\"key\""); assertThat(verboseError).contains("\"value\""); @@ -89,7 +89,7 @@ public void testVerboseErrorMode() throws Exception { /// Test enum schema error messages @Test - public void testEnumSchemaErrorMessages() throws Exception { + public void testEnumSchemaErrorMessages() { JsonValue enumSchema = Json.parse("{\"enum\": [\"red\", \"green\", \"blue\"]}"); Jtd validator = new Jtd(); @@ -98,20 +98,20 @@ public void testEnumSchemaErrorMessages() throws Exception { Jtd.Result invalidValueResult = validator.validate(enumSchema, Json.parse("\"yellow\"")); assertThat(invalidValueResult.isValid()).isFalse(); assertThat(invalidValueResult.errors()).hasSize(1); - assertThat(invalidValueResult.errors().get(0)).contains("value 'yellow' not in enum: [red, green, blue]"); + assertThat(invalidValueResult.errors().getFirst()).contains("value 'yellow' not in enum: [red, green, blue]"); // Test non-string value for enum Jtd.Result nonStringResult = validator.validate(enumSchema, Json.parse("123")); assertThat(nonStringResult.isValid()).isFalse(); assertThat(nonStringResult.errors()).hasSize(1); - assertThat(nonStringResult.errors().get(0)).contains("expected string for enum, got JsonNumber"); + assertThat(nonStringResult.errors().getFirst()).contains("expected string for enum, got JsonNumber"); LOG.fine(() -> "Enum schema error messages test completed successfully"); } /// Test array schema error messages @Test - public void testArraySchemaErrorMessages() throws Exception { + public void testArraySchemaErrorMessages() { JsonValue arraySchema = Json.parse("{\"elements\": {\"type\": \"string\"}}"); Jtd validator = new Jtd(); @@ -120,20 +120,20 @@ public void testArraySchemaErrorMessages() throws Exception { Jtd.Result nonArrayResult = validator.validate(arraySchema, Json.parse("\"not-an-array\"")); assertThat(nonArrayResult.isValid()).isFalse(); assertThat(nonArrayResult.errors()).hasSize(1); - assertThat(nonArrayResult.errors().get(0)).contains("expected array, got JsonString"); + assertThat(nonArrayResult.errors().getFirst()).contains("expected array, got JsonString"); // Test invalid element in array Jtd.Result invalidElementResult = validator.validate(arraySchema, Json.parse("[\"valid\", 123, \"also-valid\"]")); assertThat(invalidElementResult.isValid()).isFalse(); assertThat(invalidElementResult.errors()).hasSize(1); - assertThat(invalidElementResult.errors().get(0)).contains("expected string, got JsonNumber"); + assertThat(invalidElementResult.errors().getFirst()).contains("expected string, got JsonNumber"); LOG.fine(() -> "Array schema error messages test completed successfully"); } /// Test object schema error messages @Test - public void testObjectSchemaErrorMessages() throws Exception { + public void testObjectSchemaErrorMessages() { JsonValue objectSchema = Json.parse("{\"properties\": {\"name\": {\"type\": \"string\"}, \"age\": {\"type\": \"int32\"}}}"); Jtd validator = new Jtd(); @@ -142,26 +142,26 @@ public void testObjectSchemaErrorMessages() throws Exception { Jtd.Result nonObjectResult = validator.validate(objectSchema, Json.parse("\"not-an-object\"")); assertThat(nonObjectResult.isValid()).isFalse(); assertThat(nonObjectResult.errors()).hasSize(1); - assertThat(nonObjectResult.errors().get(0)).contains("expected object, got JsonString"); + assertThat(nonObjectResult.errors().getFirst()).contains("expected object, got JsonString"); // Test missing required property Jtd.Result missingPropertyResult = validator.validate(objectSchema, Json.parse("{\"name\": \"John\"}")); assertThat(missingPropertyResult.isValid()).isFalse(); assertThat(missingPropertyResult.errors()).hasSize(1); - assertThat(missingPropertyResult.errors().get(0)).contains("missing required property: age"); + assertThat(missingPropertyResult.errors().getFirst()).contains("missing required property: age"); // Test invalid property value Jtd.Result invalidPropertyResult = validator.validate(objectSchema, Json.parse("{\"name\": 123, \"age\": 25}")); assertThat(invalidPropertyResult.isValid()).isFalse(); assertThat(invalidPropertyResult.errors()).hasSize(1); - assertThat(invalidPropertyResult.errors().get(0)).contains("expected string, got JsonNumber"); + assertThat(invalidPropertyResult.errors().getFirst()).contains("expected string, got JsonNumber"); LOG.fine(() -> "Object schema error messages test completed successfully"); } /// Test additional properties error messages @Test - public void testAdditionalPropertiesErrorMessages() throws Exception { + public void testAdditionalPropertiesErrorMessages() { JsonValue objectSchema = Json.parse("{\"properties\": {\"name\": {\"type\": \"string\"}}, \"additionalProperties\": false}"); Jtd validator = new Jtd(); @@ -170,14 +170,14 @@ public void testAdditionalPropertiesErrorMessages() throws Exception { Jtd.Result additionalPropResult = validator.validate(objectSchema, Json.parse("{\"name\": \"John\", \"extra\": \"not-allowed\"}")); assertThat(additionalPropResult.isValid()).isFalse(); assertThat(additionalPropResult.errors()).hasSize(1); - assertThat(additionalPropResult.errors().get(0)).contains("additional property not allowed: extra"); + assertThat(additionalPropResult.errors().getFirst()).contains("additional property not allowed: extra"); LOG.fine(() -> "Additional properties error messages test completed successfully"); } /// Test discriminator schema error messages @Test - public void testDiscriminatorSchemaErrorMessages() throws Exception { + public void testDiscriminatorSchemaErrorMessages() { JsonValue discriminatorSchema = Json.parse("{\"discriminator\": \"type\", \"mapping\": {\"person\": {\"properties\": {\"name\": {\"type\": \"string\"}}}}}"); Jtd validator = new Jtd(); @@ -186,26 +186,26 @@ public void testDiscriminatorSchemaErrorMessages() throws Exception { Jtd.Result nonObjectResult = validator.validate(discriminatorSchema, Json.parse("\"not-an-object\"")); assertThat(nonObjectResult.isValid()).isFalse(); assertThat(nonObjectResult.errors()).hasSize(1); - assertThat(nonObjectResult.errors().get(0)).contains("expected object, got JsonString"); + assertThat(nonObjectResult.errors().getFirst()).contains("expected object, got JsonString"); // Test invalid discriminator type Jtd.Result invalidDiscriminatorResult = validator.validate(discriminatorSchema, Json.parse("{\"type\": 123}")); assertThat(invalidDiscriminatorResult.isValid()).isFalse(); assertThat(invalidDiscriminatorResult.errors()).hasSize(1); - assertThat(invalidDiscriminatorResult.errors().get(0)).contains("discriminator 'type' must be a string"); + assertThat(invalidDiscriminatorResult.errors().getFirst()).contains("discriminator 'type' must be a string"); // Test discriminator value not in mapping Jtd.Result unknownDiscriminatorResult = validator.validate(discriminatorSchema, Json.parse("{\"type\": \"unknown\"}")); assertThat(unknownDiscriminatorResult.isValid()).isFalse(); assertThat(unknownDiscriminatorResult.errors()).hasSize(1); - assertThat(unknownDiscriminatorResult.errors().get(0)).contains("discriminator value 'unknown' not in mapping"); + assertThat(unknownDiscriminatorResult.errors().getFirst()).contains("discriminator value 'unknown' not in mapping"); LOG.fine(() -> "Discriminator schema error messages test completed successfully"); } /// Test unknown type error message @Test - public void testUnknownTypeErrorMessage() throws Exception { + public void testUnknownTypeErrorMessage() { JsonValue unknownTypeSchema = Json.parse("{\"type\": \"unknown-type\"}"); Jtd validator = new Jtd(); @@ -213,14 +213,14 @@ public void testUnknownTypeErrorMessage() throws Exception { Jtd.Result result = validator.validate(unknownTypeSchema, Json.parse("\"anything\"")); assertThat(result.isValid()).isFalse(); assertThat(result.errors()).hasSize(1); - assertThat(result.errors().get(0)).contains("unknown type: unknown-type"); + assertThat(result.errors().getFirst()).contains("unknown type: unknown-type"); LOG.fine(() -> "Unknown type error message test completed successfully"); } /// Test that error messages are consistent across different schema types @Test - public void testErrorMessageConsistency() throws Exception { + public void testErrorMessageConsistency() { JsonValue invalidData = Json.parse("123"); Jtd validator = new Jtd(); @@ -237,7 +237,7 @@ public void testErrorMessageConsistency() throws Exception { Jtd.Result result = validator.validate(schema, invalidData); assertThat(result.isValid()).isFalse(); assertThat(result.errors()).hasSize(1); - String error = result.errors().get(0); + String error = result.errors() .getFirst(); if (schema.toString().contains("elements")) { // Elements schema expects array @@ -250,4 +250,4 @@ public void testErrorMessageConsistency() throws Exception { LOG.fine(() -> "Error message consistency test completed successfully"); } -} \ No newline at end of file +}