From 7677cc47d37b0208774184b9c22d3baea98b2122 Mon Sep 17 00:00:00 2001 From: Steve Hu Date: Sun, 8 Mar 2026 15:24:52 -0400 Subject: [PATCH 1/2] oneOf and discriminator value mismatch doesnot fail --- .../schema/keyword/OneOfValidator.java | 17 ++++- .../schema/DiscriminatorValidatorTest.java | 73 ++++++++++++++++++- .../networknt/schema/oas/OpenApi31Test.java | 17 ++++- 3 files changed, 101 insertions(+), 6 deletions(-) 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 messages = schema.validate(inputData, InputFormat.JSON); + // This should be invalid because type=string maps to $defs/string via discriminator, + // but the data does NOT validate against $defs/string (value:1 is not a string). + // The discriminator mapping mismatch means the wrong schema matched. + assertEquals(1, messages.size()); + assertEquals("discriminator", messages.get(0).getKeyword()); + } + } 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 messages = schema.validate(input, InputFormat.JSON); - assertEquals(0, messages.size()); + // petType=dog maps Dog schema via discriminator, but data only validates against Lizard. + // The discriminator-indicated schema (Dog) is not the one that matched, so this should fail. + assertEquals(1, messages.size()); + assertEquals("discriminator", messages.get(0).getKeyword()); } } From f6aa2cfcb202b968729b3b2742b7a370102796f0 Mon Sep 17 00:00:00 2001 From: Steve Hu Date: Sun, 8 Mar 2026 15:51:32 -0400 Subject: [PATCH 2/2] Gracefully handle RuntimeException from YAML parser in deserialize (#1231) --- .../java/com/networknt/schema/Schema.java | 4 +++ .../com/networknt/schema/SchemaException.java | 5 +++ .../java/com/networknt/schema/SchemaTest.java | 32 +++++++++++++++++++ 3 files changed, 41 insertions(+) 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/test/java/com/networknt/schema/SchemaTest.java b/src/test/java/com/networknt/schema/SchemaTest.java index ff1ac400b..f888c5372 100644 --- a/src/test/java/com/networknt/schema/SchemaTest.java +++ b/src/test/java/com/networknt/schema/SchemaTest.java @@ -98,4 +98,36 @@ public void run() { throw instance[0]; } } + + /** + * Issue 1231. + *

+ * 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 + } + } }