Repository: networknt/json-schema-validator
Pull request #399 (2021-04-12 14:56:33)

Fix for #398. 
Also fixes the special situation of nested discriminators not working correctly when the parent schema has a discriminator, but is not referenced via anyOf/$ref

FWiesner: corrected the issue link. Should have been #398

Patch:
diff --git a/src/main/java/com/networknt/schema/AllOfValidator.java b/src/main/java/com/networknt/schema/AllOfValidator.java
index 548635f..c636f1d 100644
--- a/src/main/java/com/networknt/schema/AllOfValidator.java
+++ b/src/main/java/com/networknt/schema/AllOfValidator.java
@@ -63,20 +63,22 @@ public class AllOfValidator extends BaseJsonValidator implements JsonValidator {
                     if (null != $ref) {
                         final ValidationContext.DiscriminatorContext currentDiscriminatorContext = validationContext
                                 .getCurrentDiscriminatorContext();
-                        final ObjectNode discriminator = currentDiscriminatorContext
-                                .getDiscriminatorForPath(allOfEntry.get("$ref").asText());
-                        if (null != discriminator) {
-                            registerAndMergeDiscriminator(currentDiscriminatorContext, discriminator, parentSchema, at);
-                            // now we have to check whether we have hit the right target
-                            final String discriminatorPropertyName = discriminator.get("propertyName").asText();
-                            final String discriminatorPropertyValue = node.get(discriminatorPropertyName).textValue();
-
-                            final JsonSchema jsonSchema = parentSchema;
-                            checkDiscriminatorMatch(
-                                    currentDiscriminatorContext,
-                                    discriminator,
-                                    discriminatorPropertyValue,
-                                    jsonSchema);
+                        if (null != currentDiscriminatorContext) {
+                            final ObjectNode discriminator = currentDiscriminatorContext
+                                    .getDiscriminatorForPath(allOfEntry.get("$ref").asText());
+                            if (null != discriminator) {
+                                registerAndMergeDiscriminator(currentDiscriminatorContext, discriminator, parentSchema, at);
+                                // now we have to check whether we have hit the right target
+                                final String discriminatorPropertyName = discriminator.get("propertyName").asText();
+                                final String discriminatorPropertyValue = node.get(discriminatorPropertyName).textValue();
+
+                                final JsonSchema jsonSchema = parentSchema;
+                                checkDiscriminatorMatch(
+                                        currentDiscriminatorContext,
+                                        discriminator,
+                                        discriminatorPropertyValue,
+                                        jsonSchema);
+                            }
                         }
                     }
                 }
@@ -86,8 +88,6 @@ public class AllOfValidator extends BaseJsonValidator implements JsonValidator {
         return Collections.unmodifiableSet(errors);
     }
 
-
-
     @Override
     public Set<ValidationMessage> walk(JsonNode node, JsonNode rootNode, String at, boolean shouldValidateSchema) {
         Set<ValidationMessage> validationMessages = new LinkedHashSet<ValidationMessage>();
diff --git a/src/main/java/com/networknt/schema/AnyOfValidator.java b/src/main/java/com/networknt/schema/AnyOfValidator.java
index 6ec5dad..fdc918a 100644
--- a/src/main/java/com/networknt/schema/AnyOfValidator.java
+++ b/src/main/java/com/networknt/schema/AnyOfValidator.java
@@ -83,7 +83,12 @@ public class AnyOfValidator extends BaseJsonValidator implements JsonValidator {
                     }
                 }
                 allErrors.addAll(errors);
-//                allErrors.add(renderMissingMatchValidationMessage(at));
+            }
+
+            if (config.isOpenAPI3StyleDiscriminators() && discriminatorContext.isActive()) {
+               final Set<ValidationMessage> errors = new HashSet<ValidationMessage>();
+               errors.add(buildValidationMessage(at, "based on the provided discriminator. No alternative could be chosen based on the discriminator property"));
+               return Collections.unmodifiableSet(errors);
             }
         } finally {
             if (config.isOpenAPI3StyleDiscriminators()) {
@@ -92,11 +97,4 @@ public class AnyOfValidator extends BaseJsonValidator implements JsonValidator {
         }
         return Collections.unmodifiableSet(allErrors);
     }
-
-    private ValidationMessage renderMissingMatchValidationMessage(final String at) {
-        if (config.isOpenAPI3StyleDiscriminators()) {
-            return super.buildValidationMessage(at, "and no match could be found (respecting discriminators). " + REMARK);
-        }
-        return super.buildValidationMessage(at, "and no match could be found. " + REMARK);
-    }
 }
diff --git a/src/main/java/com/networknt/schema/JsonSchema.java b/src/main/java/com/networknt/schema/JsonSchema.java
index d065178..4451658 100644
--- a/src/main/java/com/networknt/schema/JsonSchema.java
+++ b/src/main/java/com/networknt/schema/JsonSchema.java
@@ -82,7 +82,7 @@ public class JsonSchema extends BaseJsonValidator {
 
             if (config.isOpenAPI3StyleDiscriminators()) {
                 ObjectNode discriminator = (ObjectNode) schemaNode.get("discriminator");
-                if (null != discriminator) {
+                if (null != discriminator && null != validationContext.getCurrentDiscriminatorContext()) {
                     validationContext.getCurrentDiscriminatorContext().registerDiscriminator(schemaPath, discriminator);
                 }
             }
@@ -245,11 +245,22 @@ public class JsonSchema extends BaseJsonValidator {
             if (null != discriminator) {
                 final DiscriminatorContext discriminatorContext = validationContext.getCurrentDiscriminatorContext();
                 if (null != discriminatorContext) {
+                    final ObjectNode discriminatorToUse;
                     final ObjectNode discriminatorFromContext = discriminatorContext.getDiscriminatorForPath(schemaPath);
-                    final String discriminatorPropertyName = discriminatorFromContext.get("propertyName").asText();
+                    if (null == discriminatorFromContext) {
+                        // register the current discriminator. This can only happen when the current context discriminator
+                        // was not registered via allOf. In that case we have a $ref to the schema with discriminator that gets
+                        // used for validation before allOf validation has kicked in
+                        discriminatorContext.registerDiscriminator(schemaPath, discriminator);
+                        discriminatorToUse = discriminator;
+                    } else{
+                        discriminatorToUse = discriminatorFromContext;
+                    }
+
+                    final String discriminatorPropertyName = discriminatorToUse.get("propertyName").asText();
                     final String discriminatorPropertyValue = jsonNode.get(discriminatorPropertyName).asText();
                     checkDiscriminatorMatch(discriminatorContext,
-                                            discriminatorFromContext,
+                                            discriminatorToUse,
                                             discriminatorPropertyValue,
                                             this);
                 }
diff --git a/src/main/java/com/networknt/schema/ValidationContext.java b/src/main/java/com/networknt/schema/ValidationContext.java
index 05d4b5a..5efcc65 100644
--- a/src/main/java/com/networknt/schema/ValidationContext.java
+++ b/src/main/java/com/networknt/schema/ValidationContext.java
@@ -90,7 +90,10 @@ public class ValidationContext {
     }
 
     public DiscriminatorContext getCurrentDiscriminatorContext() {
-        return discriminatorContexts.peek();
+        if (!discriminatorContexts.empty()) {
+            return discriminatorContexts.peek();
+        }
+        return null; // this is the case when we get on a schema that has a discriminator, but it's not used in anyOf
     }
 
     public void enterDiscriminatorContext(final DiscriminatorContext ctx, String at) {
@@ -125,5 +128,14 @@ public class ValidationContext {
         public boolean isDiscriminatorMatchFound() {
             return discriminatorMatchFound;
         }
+
+        /**
+         * Returns true if we have a discriminator active. In this case no valid match in anyOf should lead to validation failure
+         *
+         * @return true in case there are discriminator candidates
+         */
+        public boolean isActive() {
+            return !discriminators.isEmpty();
+        }
     }
 }


Test patch:
diff --git a/src/test/resources/openapi3/discriminator.json b/src/test/resources/openapi3/discriminator.json
index f833464..2e32b70 100644
--- a/src/test/resources/openapi3/discriminator.json
+++ b/src/test/resources/openapi3/discriminator.json
@@ -279,5 +279,245 @@
                 "valid": false
             }
         ]
+    },
+    {
+        "description": "Issue #398 - $ref pointing to base type with discriminator and w/o anyOf",
+        "schema": {
+            "$ref": "#/components/schemas/Room",
+            "components": {
+                "schemas": {
+                    "Room": {
+                        "type": "object",
+                        "properties": {
+                            "@type": {
+                                "type": "string"
+                            },
+                            "floor": {
+                                "type": "integer"
+                            },
+                            "nextRoom": {
+                                "anyOf": [
+                                    {"$ref":"#/components/schemas/Room" },
+                                    {"$ref":"#/components/schemas/BedRoom" }
+                                ]
+                            }
+                        },
+                        "required": [
+                            "@type"
+                        ],
+                        "discriminator": {
+                            "propertyName": "@type"
+                        }
+                    },
+                    "BedRoom": {
+                        "type": "object",
+                        "allOf": [
+                            {
+                                "$ref": "#/components/schemas/Room"
+                            },
+                            {
+                                "type": "object",
+                                "properties": {
+                                    "numberOfBeds": {
+                                        "type": "integer"
+                                    }
+                                },
+                                "required": [
+                                    "numberOfBeds"
+                                ]
+                            }
+                        ]
+                    }
+                }
+            }
+        },
+        "tests": [
+            {
+                "description": "$ref without anyOf and therefore no discriminator context",
+                "data": {
+                    "@type": "Room"
+                },
+                "valid": true
+            },
+            {
+                "description": "schema with discriminator and recursion with invalid BedRoom",
+                "data": {
+                    "@type": "can be ignored - discriminator not in use on root schema",
+                    "numberOfBeds": 42,
+                    "nextRoom": {
+                        "@type": "BedRoom",
+                        "floor": 1
+                    }
+                },
+                "valid": false
+            }
+        ]
+    },
+    {
+        "description": "Issue #398 - $ref pointing to extended type with discriminator and w/o anyOf",
+        "schema": {
+            "$ref": "#/components/schemas/BedRoom",
+            "components": {
+                "schemas": {
+                    "Room": {
+                        "type": "object",
+                        "properties": {
+                            "@type": {
+                                "type": "string"
+                            },
+                            "floor": {
+                                "type": "integer"
+                            },
+                            "nextRoom": {
+                                "anyOf": [
+                                    {"$ref": "#/components/schemas/Room"},
+                                    {"$ref": "#/components/schemas/BedRoom"}
+                                ]
+                            }
+                        },
+                        "required": [
+                            "@type"
+                        ],
+                        "discriminator": {
+                            "propertyName": "@type"
+                        }
+                    },
+                    "BedRoom": {
+                        "type": "object",
+                        "allOf": [
+                            {
+                                "$ref": "#/components/schemas/Room"
+                            },
+                            {
+                                "type": "object",
+                                "properties": {
+                                    "numberOfBeds": {
+                                        "type": "integer"
+                                    },
+                                    "nextRoom": {
+                                        "anyOf": [
+                                            {"$ref": "#/components/schemas/Room"},
+                                            {"$ref": "#/components/schemas/BedRoom"}
+                                        ]
+                                    }
+                                },
+                                "required": [
+                                    "numberOfBeds"
+                                ]
+                            }
+                        ]
+                    }
+                }
+            }
+        },
+        "tests": [
+            {
+                "description": "$ref without anyOf and therefore no discriminator context",
+                "data": {
+                    "@type": "can be ignored - discriminator not in use on root schema",
+                    "numberOfBeds": 42,
+                    "nextRoom": {
+                        "@type": "Room",
+                        "floor": 3
+                    }
+                },
+                "valid": true
+            },
+            {
+                "description": "schema with discriminator and recursion with valid BedRoom",
+                "data": {
+                    "@type": "can be ignored - discriminator not in use on root schema",
+                    "numberOfBeds": 42,
+                    "nextRoom": {
+                        "@type": "BedRoom",
+                        "floor": 1,
+                        "numberOfBeds": 12345
+                    }
+                },
+                "valid": true
+            },
+            {
+                "description": "schema with discriminator and recursion with invalid BedRoom",
+                "data": {
+                    "@type": "can be ignored - discriminator not in use on root schema",
+                    "numberOfBeds": 42,
+                    "nextRoom": {
+                        "@type": "BedRoom",
+                        "floor": 1
+                    }
+                },
+                "valid": false
+            }
+        ]
+    },
+    {
+        "description": "Issue #398 - invalid discriminator specification has to lead to failed validation",
+        "schema": {
+            "anyOf": [
+                {"$ref": "#/components/schemas/Room"},
+                {"$ref": "#/components/schemas/BedRoom"}
+            ],
+            "components": {
+                "schemas": {
+                    "Room": {
+                        "type": "object",
+                        "properties": {
+                            "@type": {
+                                "type": "string"
+                            },
+                            "floor": {
+                                "type": "integer"
+                            },
+                            "nextRoom": {
+                                "anyOf": [
+                                    {"$ref": "#/components/schemas/Room"},
+                                    {"$ref": "#/components/schemas/BedRoom"}
+                                ]
+                            }
+                        },
+                        "required": [
+                            "@type"
+                        ],
+                        "discriminator": {
+                            "propertyName": "@type"
+                        }
+                    },
+                    "BedRoom": {
+                        "type": "object",
+                        "allOf": [
+                            {
+                                "$ref": "#/components/schemas/Room"
+                            },
+                            {
+                                "type": "object",
+                                "properties": {
+                                    "numberOfBeds": {
+                                        "type": "integer"
+                                    },
+                                    "nextRoom": {
+                                        "anyOf": [
+                                            {"$ref": "#/components/schemas/Room"},
+                                            {"$ref": "#/components/schemas/BedRoom"}
+                                        ]
+                                    }
+                                },
+                                "required": [
+                                    "numberOfBeds"
+                                ]
+                            }
+                        ]
+                    }
+                }
+            }
+        },
+        "tests": [
+            {
+                "description": "$ref without anyOf and therefore no discriminator context",
+                "data": {
+                    "@type": "illegal discriminator property value"
+                },
+                "valid": false
+            }
+        ]
     }
 ]