diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 836d558..258d977 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=460 + exp_tests=463 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/json-java21-jtd/README.md b/json-java21-jtd/README.md index a0fdbfb..78b231f 100644 --- a/json-java21-jtd/README.md +++ b/json-java21-jtd/README.md @@ -69,6 +69,14 @@ Validates primitive types: Supported types: `boolean`, `string`, `timestamp`, `int8`, `uint8`, `int16`, `uint16`, `int32`, `uint32`, `float32`, `float64` +#### Integer Type Validation +Integer types (`int8`, `uint8`, `int16`, `uint16`, `int32`, `uint32`) validate based on **numeric value**, not textual representation: + +- **Valid integers**: `3`, `3.0`, `3.000`, `42.00` (mathematically integers) +- **Invalid integers**: `3.1`, `3.14`, `3.0001` (have fractional components) + +This follows RFC 8927 §2.2.3.1: "An integer value is a number without a fractional component." + ### 4. Enum Schema Validates against string values: ```json 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 3e5f44a..093c38a 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 @@ -244,6 +244,13 @@ Jtd.Result validateInteger(JsonValue instance, String type, boolean verboseError return Jtd.Result.failure(Jtd.Error.EXPECTED_INTEGER.message()); } + // Handle BigDecimal - check if it has fractional component (not just scale > 0) + // RFC 8927 §2.2.3.1: "An integer value is a number without a fractional component" + // Values like 3.0 or 3.000 are valid integers despite positive scale, but 3.1 is not + if (value instanceof java.math.BigDecimal bd && bd.remainder(java.math.BigDecimal.ONE).signum() != 0) { + return Jtd.Result.failure(Jtd.Error.EXPECTED_INTEGER.message()); + } + // Convert to long for range checking long longValue = value.longValue(); 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 9b0e124..3c9b186 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 @@ -2,6 +2,7 @@ import jdk.sandbox.java.util.json.Json; import jdk.sandbox.java.util.json.JsonValue; +import jdk.sandbox.java.util.json.JsonNumber; import org.junit.jupiter.api.Test; import static org.assertj.core.api.Assertions.assertThat; @@ -440,4 +441,93 @@ public void testRefSchemaRecursiveBad() throws Exception { .as("Recursive ref should reject heterogeneous nested data") .isFalse(); } + + /// 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 { + JsonValue schema = Json.parse("{\"type\": \"int32\"}"); + JsonValue decimalValue = JsonNumber.of(new java.math.BigDecimal("3.14")); + + LOG.info(() -> "Testing int32 validation against decimal value 3.14"); + LOG.fine(() -> "Schema: " + schema); + LOG.fine(() -> "Instance: " + decimalValue); + + Jtd validator = new Jtd(); + Jtd.Result result = validator.validate(schema, decimalValue); + + LOG.fine(() -> "Validation result: " + (result.isValid() ? "VALID" : "INVALID")); + if (!result.isValid()) { + LOG.fine(() -> "ERRORS: " + result.errors()); + } + + // This should be invalid - int32 should reject decimal values + assertThat(result.isValid()) + .as("int32 should reject decimal value 3.14") + .isFalse(); + assertThat(result.errors()) + .as("Should have validation errors for decimal value") + .isNotEmpty(); + } + + /// Test that integer types accept valid integer representations with trailing zeros + /// 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 { + JsonValue schema = Json.parse("{\"type\": \"int32\"}"); + + // Valid integer representations with trailing zeros + JsonValue[] validIntegers = { + JsonNumber.of(new java.math.BigDecimal("3.0")), + JsonNumber.of(new java.math.BigDecimal("3.000")), + JsonNumber.of(new java.math.BigDecimal("42.00")), + JsonNumber.of(new java.math.BigDecimal("0.0")) + }; + + Jtd validator = new Jtd(); + + for (JsonValue validValue : validIntegers) { + Jtd.Result result = validator.validate(schema, validValue); + + LOG.fine(() -> "Testing int32 with valid integer representation: " + validValue); + + assertThat(result.isValid()) + .as("int32 should accept integer representation %s", validValue) + .isTrue(); + assertThat(result.errors()) + .as("Should have no validation errors for integer representation %s", validValue) + .isEmpty(); + } + } + + /// 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 { + JsonValue schema = Json.parse("{\"type\": \"int32\"}"); + + // Invalid values with actual fractional components + JsonValue[] invalidValues = { + JsonNumber.of(new java.math.BigDecimal("3.1")), + JsonNumber.of(new java.math.BigDecimal("3.0001")), + JsonNumber.of(new java.math.BigDecimal("3.14")), + JsonNumber.of(new java.math.BigDecimal("0.1")) + }; + + Jtd validator = new Jtd(); + + for (JsonValue invalidValue : invalidValues) { + Jtd.Result result = validator.validate(schema, invalidValue); + + LOG.fine(() -> "Testing int32 with fractional value: " + invalidValue); + + assertThat(result.isValid()) + .as("int32 should reject fractional value %s", invalidValue) + .isFalse(); + assertThat(result.errors()) + .as("Should have validation errors for fractional value %s", invalidValue) + .isNotEmpty(); + } + } }