diff --git a/src/main/java/com/networknt/schema/Schema.java b/src/main/java/com/networknt/schema/Schema.java index 87999f107..3b3181dcc 100644 --- a/src/main/java/com/networknt/schema/Schema.java +++ b/src/main/java/com/networknt/schema/Schema.java @@ -1192,6 +1192,8 @@ private JsonNode deserialize(String input, InputFormat inputFormat) { return this.getSchemaContext().getSchemaRegistry().readTree(input, inputFormat); } catch (IOException e) { throw new UncheckedIOException("Invalid input", e); + } catch (RuntimeException e) { + throw new SchemaException("Invalid " + inputFormat + " input: " + e.getMessage(), e); } } @@ -1213,6 +1215,8 @@ private JsonNode deserialize(AbsoluteIri input, InputFormat inputFormat) { } } catch (IOException e) { throw new UncheckedIOException("Invalid input", e); + } catch (RuntimeException e) { + throw new SchemaException("Invalid " + inputFormat + " input from " + input + ": " + e.getMessage(), e); } } diff --git a/src/main/java/com/networknt/schema/SchemaException.java b/src/main/java/com/networknt/schema/SchemaException.java index fe94a0ef3..3166461b0 100644 --- a/src/main/java/com/networknt/schema/SchemaException.java +++ b/src/main/java/com/networknt/schema/SchemaException.java @@ -40,6 +40,11 @@ public SchemaException(Throwable throwable) { this.error = null; } + public SchemaException(String message, Throwable throwable) { + super(message, throwable); + this.error = null; + } + @Override public String getMessage() { return this.error != null ? this.error.getMessage() : super.getMessage(); diff --git a/src/main/java/com/networknt/schema/keyword/OneOfValidator.java b/src/main/java/com/networknt/schema/keyword/OneOfValidator.java index a93632085..3895d0fbe 100644 --- a/src/main/java/com/networknt/schema/keyword/OneOfValidator.java +++ b/src/main/java/com/networknt/schema/keyword/OneOfValidator.java @@ -168,7 +168,7 @@ protected void validate(ExecutionContext executionContext, JsonNode node, JsonNo * schema can be determined and validation SHOULD fail. Mapping keys MUST be * string values, but tooling MAY convert response values to strings for * comparison. - * + * * https://spec.openapis.org/oas/v3.1.2#examples-0 */ DiscriminatorState state = executionContext.getDiscriminatorMapping().get(instanceLocation); @@ -183,7 +183,22 @@ protected void validate(ExecutionContext executionContext, JsonNode node, JsonNo .messageKey("discriminator.oneOf.no_match_found") .arguments(state.getDiscriminatingValue()).build()); } + /* + * Issue 1225: When the discriminator-indicated schema failed (discriminatorErrors + * is set) but another non-discriminated schema happened to pass (numberOfValidSchema == 1), + * validation incorrectly succeeds. The discriminator mapping constitutes an assertion + * that the payload should validate against the mapped schema. If the mapped schema + * fails but a different schema passes, this is a discriminator mismatch and should fail. + */ + if (discriminatorErrors != null && numberOfValidSchema == 1 && state != null && state.hasDiscriminatingValue()) { + existingErrors + .add(error().keyword("discriminator").instanceNode(node).instanceLocation(instanceLocation) + .evaluationPath(executionContext.getEvaluationPath()).locale(executionContext.getExecutionConfig().getLocale()) + .messageKey("discriminator.oneOf.no_match_found") + .arguments(state.getDiscriminatingValue()).build()); + } } + } finally { // Restore flag executionContext.setFailFast(failFast); diff --git a/src/test/java/com/networknt/schema/DiscriminatorValidatorTest.java b/src/test/java/com/networknt/schema/DiscriminatorValidatorTest.java index 1c4857c08..f8eb97194 100644 --- a/src/test/java/com/networknt/schema/DiscriminatorValidatorTest.java +++ b/src/test/java/com/networknt/schema/DiscriminatorValidatorTest.java @@ -980,5 +980,76 @@ void anyOfRedefinedDiscriminatorAndDiscriminatorWithMissingPropertyName() { // There is still a schema in the anyOf that matches assertTrue(list.isEmpty()); } - + + /** + * Issue 1225. + *
+ * When oneOf with discriminator mapping is used and the discriminating value
+ * maps to a specific schema (e.g., type=string -> $defs/string), but the
+ * data only validates against a different schema (e.g., $defs/number),
+ * validation should fail because the discriminator-indicated schema is not
+ * the one that matches.
+ */
+ @Test
+ void oneOfDiscriminatorEnabledWithDiscriminatorMismatch() {
+ String schemaData = "{\r\n"
+ + " \"discriminator\": {\r\n"
+ + " \"propertyName\": \"type\",\r\n"
+ + " \"mapping\": {\r\n"
+ + " \"string\": \"#/$defs/string\",\r\n"
+ + " \"number\": \"#/$defs/number\"\r\n"
+ + " }\r\n"
+ + " },\r\n"
+ + " \"oneOf\": [\r\n"
+ + " {\r\n"
+ + " \"$ref\": \"#/$defs/string\"\r\n"
+ + " },\r\n"
+ + " {\r\n"
+ + " \"$ref\": \"#/$defs/number\"\r\n"
+ + " }\r\n"
+ + " ],\r\n"
+ + " \"$defs\": {\r\n"
+ + " \"string\": {\r\n"
+ + " \"properties\": {\r\n"
+ + " \"type\": {\r\n"
+ + " \"type\": \"string\"\r\n"
+ + " },\r\n"
+ + " \"value\": {\r\n"
+ + " \"type\": \"string\"\r\n"
+ + " }\r\n"
+ + " }\r\n"
+ + " },\r\n"
+ + " \"number\": {\r\n"
+ + " \"properties\": {\r\n"
+ + " \"type\": {\r\n"
+ + " \"type\": \"string\"\r\n"
+ + " },\r\n"
+ + " \"value\": {\r\n"
+ + " \"type\": \"number\"\r\n"
+ + " }\r\n"
+ + " }\r\n"
+ + " }\r\n"
+ + " }\r\n"
+ + "}";
+
+ SchemaRegistry factory = SchemaRegistry.withDialect(Dialects.getOpenApi31());
+ Schema schema = factory.getSchema(schemaData);
+
+ // type=string maps to $defs/string via explicit discriminator mapping.
+ // However, value=1 is a number, so $defs/string fails (value must be string).
+ // $defs/number succeeds (value=1 is a valid number).
+ // oneOf passes because exactly one schema matches, but it's the WRONG schema.
+ // The discriminator says type=string should map to $defs/string, so this should fail.
+ String inputData = "{\r\n"
+ + " \"type\": \"string\",\r\n"
+ + " \"value\": 1\r\n"
+ + "}";
+ List
+ * When the YAML input length lands exactly on the snakeyaml-engine StreamReader
+ * 1024-char chunk boundary, an IndexOutOfBoundsException is thrown internally.
+ * This should be caught and wrapped in a SchemaException with a clear message
+ * rather than propagating as a raw IndexOutOfBoundsException.
+ */
+ @Test
+ void yamlInputAtChunkBoundaryShouldThrowSchemaException() {
+ // Build a YAML key that is exactly 1024 chars so the total YAML content
+ // hits the snakeyaml-engine StreamReader boundary.
+ String longKey = "a".repeat(1024);
+ String yamlInput = longKey + ": value\n";
+
+ String schemaData = "{\"type\": \"object\"}";
+ SchemaRegistry factory = SchemaRegistry.withDialect(com.networknt.schema.dialect.Dialects.getOpenApi31());
+ Schema schema = factory.getSchema(schemaData);
+
+ // Before the fix this threw IndexOutOfBoundsException directly.
+ // After the fix it should throw SchemaException with a clear message.
+ try {
+ schema.validate(yamlInput, InputFormat.YAML);
+ } catch (IndexOutOfBoundsException e) {
+ // Raw IOOBE must not escape - this is the bug we're fixing
+ throw new AssertionError("Expected SchemaException but got raw IndexOutOfBoundsException", e);
+ } catch (RuntimeException e) {
+ // Any other RuntimeException (including SchemaException) is acceptable
+ // as long as it's not the raw IndexOutOfBoundsException
+ }
+ }
}
diff --git a/src/test/java/com/networknt/schema/oas/OpenApi31Test.java b/src/test/java/com/networknt/schema/oas/OpenApi31Test.java
index 84ab471b2..ae5d9ec21 100644
--- a/src/test/java/com/networknt/schema/oas/OpenApi31Test.java
+++ b/src/test/java/com/networknt/schema/oas/OpenApi31Test.java
@@ -129,11 +129,17 @@ void discriminatorOneOfNoMatchShouldFail() {
}
/**
- * Test oneOf with one match but incorrect discriminator should succeed. Note
- * that the discriminator does not affect the validation outcome.
+ * Test oneOf with one match but incorrect discriminator should fail.
+ *
+ * When petType=dog maps to the Dog schema via discriminator mapping, but the
+ * data only validates against Lizard (because lovesRocks=true satisfies Lizard),
+ * validation should fail because the discriminator-indicated schema (Dog) is not
+ * the one that matched.
+ *
+ * Fix for issue 1225.
*/
@Test
- void discriminatorOneOfOneMatchWrongDiscriminatorShouldSucceed() {
+ void discriminatorOneOfOneMatchWrongDiscriminatorShouldFail() {
SchemaRegistry factory = SchemaRegistry.withDialect(Dialects.getOpenApi31());
Schema schema = factory
.getSchema(SchemaLocation.of("classpath:schema/oas/3.1/petstore.yaml#/components/schemas/PetResponse"));
@@ -142,7 +148,10 @@ void discriminatorOneOfOneMatchWrongDiscriminatorShouldSucceed() {
+ " \"lovesRocks\": true\r\n"
+ "}";
List