diff --git a/src/main/java/com/networknt/schema/AnyOfValidator.java b/src/main/java/com/networknt/schema/AnyOfValidator.java index 7df650104..931c4e33b 100644 --- a/src/main/java/com/networknt/schema/AnyOfValidator.java +++ b/src/main/java/com/networknt/schema/AnyOfValidator.java @@ -106,7 +106,9 @@ && canShortCircuit() && canShortCircuit(executionContext)) { // return empty errors. return errors; } else if (this.validationContext.getConfig().isOpenAPI3StyleDiscriminators()) { - if (executionContext.getCurrentDiscriminatorContext().isDiscriminatorMatchFound()) { + DiscriminatorContext currentDiscriminatorContext = executionContext.getCurrentDiscriminatorContext(); + if (currentDiscriminatorContext.isDiscriminatorMatchFound() + || currentDiscriminatorContext.isDiscriminatorIgnore()) { if (!errors.isEmpty()) { // The following is to match the previous logic adding to all errors // which is generally discarded as it returns errors but the allErrors @@ -137,7 +139,8 @@ && canShortCircuit() && canShortCircuit(executionContext)) { } if (this.validationContext.getConfig().isOpenAPI3StyleDiscriminators() - && executionContext.getCurrentDiscriminatorContext().isActive()) { + && executionContext.getCurrentDiscriminatorContext().isActive() + && !executionContext.getCurrentDiscriminatorContext().isDiscriminatorIgnore()) { return Collections.singleton(message().instanceNode(node).instanceLocation(instanceLocation) .locale(executionContext.getExecutionConfig().getLocale()) .arguments( diff --git a/src/main/java/com/networknt/schema/BaseJsonValidator.java b/src/main/java/com/networknt/schema/BaseJsonValidator.java index 75c2a08b7..eebcd9c71 100644 --- a/src/main/java/com/networknt/schema/BaseJsonValidator.java +++ b/src/main/java/com/networknt/schema/BaseJsonValidator.java @@ -114,7 +114,7 @@ protected static void checkDiscriminatorMatch(final DiscriminatorContext current final String discriminatorPropertyValue, final JsonSchema jsonSchema) { if (discriminatorPropertyValue == null) { - currentDiscriminatorContext.markMatch(); + currentDiscriminatorContext.markIgnore(); return; } diff --git a/src/main/java/com/networknt/schema/DiscriminatorContext.java b/src/main/java/com/networknt/schema/DiscriminatorContext.java index 5ffeac39c..1ab2ddd76 100644 --- a/src/main/java/com/networknt/schema/DiscriminatorContext.java +++ b/src/main/java/com/networknt/schema/DiscriminatorContext.java @@ -10,6 +10,8 @@ public class DiscriminatorContext { private boolean discriminatorMatchFound = false; + private boolean discriminatorIgnore = false; + public void registerDiscriminator(final SchemaLocation schemaLocation, final ObjectNode discriminator) { this.discriminators.put("#" + schemaLocation.getFragment().toString(), discriminator); } @@ -26,10 +28,25 @@ public void markMatch() { this.discriminatorMatchFound = true; } + /** + * Indicate that discriminator processing should be ignored. + *

+ * This is used when the discriminator property value is missing from the data. + *

+ * See issue #436 for background. + */ + public void markIgnore() { + this.discriminatorIgnore = true; + } + public boolean isDiscriminatorMatchFound() { return this.discriminatorMatchFound; } + public boolean isDiscriminatorIgnore() { + return this.discriminatorIgnore; + } + /** * Returns true if we have a discriminator active. In this case no valid match in anyOf should lead to validation failure * diff --git a/src/main/java/com/networknt/schema/OneOfValidator.java b/src/main/java/com/networknt/schema/OneOfValidator.java index 3a5b91a12..3ba564a44 100644 --- a/src/main/java/com/networknt/schema/OneOfValidator.java +++ b/src/main/java/com/networknt/schema/OneOfValidator.java @@ -110,23 +110,37 @@ public Set validate(ExecutionContext executionContext, JsonNo // matching discriminator to be discarded. Note that the discriminator cannot // affect the actual validation result. if (discriminator != null && !discriminator.getPropertyName().isEmpty()) { - String discriminatorPropertyValue = node.get(discriminator.getPropertyName()).asText(); - discriminatorPropertyValue = discriminator.getMapping().getOrDefault(discriminatorPropertyValue, - discriminatorPropertyValue); - JsonNode refNode = schema.getSchemaNode().get("$ref"); - if (refNode != null) { - String ref = refNode.asText(); - if (ref.equals(discriminatorPropertyValue) || ref.endsWith("/" + discriminatorPropertyValue)) { - executionContext.getCurrentDiscriminatorContext().markMatch(); + JsonNode discriminatorPropertyNode = node.get(discriminator.getPropertyName()); + if (discriminatorPropertyNode != null) { + String discriminatorPropertyValue = discriminatorPropertyNode.asText(); + discriminatorPropertyValue = discriminator.getMapping().getOrDefault(discriminatorPropertyValue, + discriminatorPropertyValue); + JsonNode refNode = schema.getSchemaNode().get("$ref"); + if (refNode != null) { + String ref = refNode.asText(); + if (ref.equals(discriminatorPropertyValue) || ref.endsWith("/" + discriminatorPropertyValue)) { + executionContext.getCurrentDiscriminatorContext().markMatch(); + } } + } else { + // See issue 436 where the condition was relaxed to not cause an assertion + // due to missing discriminator property value + // Also see BaseJsonValidator#checkDiscriminatorMatch + executionContext.getCurrentDiscriminatorContext().markIgnore(); } } - boolean discriminatorMatchFound = executionContext.getCurrentDiscriminatorContext().isDiscriminatorMatchFound(); - if (discriminatorMatchFound && childErrors == null) { + DiscriminatorContext currentDiscriminatorContext = executionContext.getCurrentDiscriminatorContext(); + if (currentDiscriminatorContext.isDiscriminatorMatchFound() && childErrors == null) { // Note that the match is set if found and not reset so checking if childErrors // found is null triggers on the correct schema childErrors = new SetView<>(); childErrors.union(schemaErrors); + } else if (currentDiscriminatorContext.isDiscriminatorIgnore()) { + // This is the normal handling when discriminators aren't enabled + if (childErrors == null) { + childErrors = new SetView<>(); + } + childErrors.union(schemaErrors); } } else if (!schemaErrors.isEmpty() && reportChildErrors(executionContext)) { // This is the normal handling when discriminators aren't enabled @@ -140,7 +154,8 @@ public Set validate(ExecutionContext executionContext, JsonNo if (this.validationContext.getConfig().isOpenAPI3StyleDiscriminators() && (discriminator != null || executionContext.getCurrentDiscriminatorContext().isActive()) - && !executionContext.getCurrentDiscriminatorContext().isDiscriminatorMatchFound()) { + && !executionContext.getCurrentDiscriminatorContext().isDiscriminatorMatchFound() + && !executionContext.getCurrentDiscriminatorContext().isDiscriminatorIgnore()) { errors = Collections.singleton(message().instanceNode(node).instanceLocation(instanceLocation) .locale(executionContext.getExecutionConfig().getLocale()) .arguments( diff --git a/src/test/java/com/networknt/schema/DiscriminatorValidatorTest.java b/src/test/java/com/networknt/schema/DiscriminatorValidatorTest.java index f6a95f500..eb2f92fa8 100644 --- a/src/test/java/com/networknt/schema/DiscriminatorValidatorTest.java +++ b/src/test/java/com/networknt/schema/DiscriminatorValidatorTest.java @@ -538,7 +538,7 @@ void discriminatorInOneOfShouldOnlyReportErrorsInMatchingDiscriminator() { assertEquals("required", list.get(1).getType()); assertEquals("numberOfBeds", list.get(1).getProperty()); } - + @Test void discriminatorMappingInOneOfShouldOnlyReportErrorsInMatchingDiscriminator() { String schemaData = "{\r\n" @@ -640,4 +640,151 @@ void discriminatorMappingInOneOfShouldOnlyReportErrorsInMatchingDiscriminator() assertEquals("numberOfBeds", list.get(1).getProperty()); } + /** + * See issue 436 and 985. + */ + @Test + void oneOfMissingDiscriminatorValue() { + String schemaData = " {\r\n" + + " \"type\": \"object\",\r\n" + + " \"discriminator\": { \"propertyName\": \"name\" },\r\n" + + " \"oneOf\": [\r\n" + + " {\r\n" + + " \"$ref\": \"#/defs/Foo\"\r\n" + + " },\r\n" + + " {\r\n" + + " \"$ref\": \"#/defs/Bar\"\r\n" + + " }\r\n" + + " ],\r\n" + + " \"defs\": {\r\n" + + " \"Foo\": {\r\n" + + " \"type\": \"object\",\r\n" + + " \"properties\": {\r\n" + + " \"name\": {\r\n" + + " \"const\": \"Foo\"\r\n" + + " }\r\n" + + " },\r\n" + + " \"required\": [ \"name\" ],\r\n" + + " \"additionalProperties\": false\r\n" + + " },\r\n" + + " \"Bar\": {\r\n" + + " \"type\": \"object\",\r\n" + + " \"properties\": {\r\n" + + " \"name\": {\r\n" + + " \"const\": \"Bar\"\r\n" + + " }\r\n" + + " },\r\n" + + " \"required\": [ \"name\" ],\r\n" + + " \"additionalProperties\": false\r\n" + + " }\r\n" + + " }\r\n" + + " }"; + + String inputData = "{}"; + + JsonSchemaFactory factory = JsonSchemaFactory.getInstance(VersionFlag.V202012); + SchemaValidatorsConfig config = new SchemaValidatorsConfig(); + config.setOpenAPI3StyleDiscriminators(true); + JsonSchema schema = factory.getSchema(schemaData, config); + Set messages = schema.validate(inputData, InputFormat.JSON); + assertEquals(3, messages.size()); + List list = messages.stream().collect(Collectors.toList()); + assertEquals("oneOf", list.get(0).getType()); + assertEquals("required", list.get(1).getType()); + assertEquals("required", list.get(2).getType()); + } + + /** + * See issue 436. + */ + @Test + void anyOfMissingDiscriminatorValue() { + String schemaData = "{\r\n" + + " \"type\": \"array\",\r\n" + + " \"items\": {\r\n" + + " \"anyOf\": [\r\n" + + " {\r\n" + + " \"$ref\": \"#/components/schemas/Kitchen\"\r\n" + + " },\r\n" + + " {\r\n" + + " \"$ref\": \"#/components/schemas/BedRoom\"\r\n" + + " }\r\n" + + " ]\r\n" + + " },\r\n" + + " \"components\": {\r\n" + + " \"schemas\": {\r\n" + + " \"Room\": {\r\n" + + " \"type\": \"object\",\r\n" + + " \"properties\": {\r\n" + + " \"@type\": {\r\n" + + " \"type\": \"string\"\r\n" + + " }\r\n" + + " },\r\n" + + " \"required\": [\r\n" + + " \"@type\"\r\n" + + " ],\r\n" + + " \"discriminator\": {\r\n" + + " \"propertyName\": \"@type\"\r\n" + + " }\r\n" + + " },\r\n" + + " \"BedRoom\": {\r\n" + + " \"type\": \"object\",\r\n" + + " \"allOf\": [\r\n" + + " {\r\n" + + " \"$ref\": \"#/components/schemas/Room\"\r\n" + + " },\r\n" + + " {\r\n" + + " \"type\": \"object\",\r\n" + + " \"properties\": {\r\n" + + " \"numberOfBeds\": {\r\n" + + " \"type\": \"integer\"\r\n" + + " }\r\n" + + " },\r\n" + + " \"required\": [\r\n" + + " \"numberOfBeds\"\r\n" + + " ]\r\n" + + " }\r\n" + + " ]\r\n" + + " },\r\n" + + " \"Kitchen\": {\r\n" + + " \"type\": \"object\",\r\n" + + " \"allOf\": [\r\n" + + " {\r\n" + + " \"$ref\": \"#/components/schemas/Room\"\r\n" + + " },\r\n" + + " {\r\n" + + " \"type\": \"object\",\r\n" + + " \"properties\": {\r\n" + + " \"hasMicrowaveOven\": {\r\n" + + " \"type\": \"boolean\"\r\n" + + " }\r\n" + + " },\r\n" + + " \"required\": [\r\n" + + " \"hasMicrowaveOven\"\r\n" + + " ]\r\n" + + " }\r\n" + + " ]\r\n" + + " }\r\n" + + " }\r\n" + + " }\r\n" + + "}"; + + String inputData = "[\r\n" + + " {\r\n" + + " \"hasMicrowaveOven\": true\r\n" + + " },\r\n" + + " {\r\n" + + " \"@type\": \"BedRoom\",\r\n" + + " \"numberOfBeds\": 4\r\n" + + " }\r\n" + + "]"; + + JsonSchemaFactory factory = JsonSchemaFactory.getInstance(VersionFlag.V202012); + SchemaValidatorsConfig config = new SchemaValidatorsConfig(); + config.setOpenAPI3StyleDiscriminators(true); + JsonSchema schema = factory.getSchema(schemaData, config); + Set messages = schema.validate(inputData, InputFormat.JSON); + List list = messages.stream().collect(Collectors.toList()); + assertEquals("required", list.get(0).getType()); + } }