Repository: networknt/json-schema-validator
Pull request #390 (2021-03-23 20:23:44)

Add discriminator support for #35
Attempt to implement #35 for `anyOf` and `allOf`

stevehu: @FWiesner I looked into the implementation and haven't gone through the details on the OneOfValidator for all the logic yet. One thing that I want to point out is we might not need to create a new spec version OPENAPI_V3_0_3. The OpenAPI validator can use either V7 or V201909 for the validation. Is there any reason you want to create a new spec version? Thanks.
FWiesner: well, this was just an idea. I've converted to the feature flag approach
you mentioned yesterday and will push a new commit with it today

Am Mi., 24. März 2021 um 02:17 Uhr schrieb Steve Hu <
***@***.***>:

> @FWiesner <https://github.com/FWiesner> I looked into the implementation
> and haven't gone through the details on the OneOfValidator for all the
> logic yet. One thing that I want to point out is we might not need to
> create a new spec version OPENAPI_V3_0_3. The OpenAPI validator can use
> either V7 or V201909 for the validation. Is there any reason you want to
> create a new spec version? Thanks.
>
> —
> You are receiving this because you were mentioned.
> Reply to this email directly, view it on GitHub
> <https://github.com/networknt/json-schema-validator/pull/390#issuecomment-805397961>,
> or unsubscribe
> <https://github.com/notifications/unsubscribe-auth/AAK6BBPCMC57WABE5HMQILDTFE4THANCNFSM4ZV5BHBA>
> .
>

FWiesner: the implementation has two limitations:
1.) I have not yet implemented a custom keyword to validate the discriminator
2.) the discriminator spec specifies that inline schemas get ignored. So,
```yml
Schema:
  properties:
    foo:
      type: string
  required: ["foo"]
  allOf:
    - $ref: ...
    - type: object
       properties:
         bar:
           type: string
       required: ["bar"]
```
should ignore `foo`, but enforce `bar`. Right now `foo` is not ignored
stevehu: @FWiesner I think the limitations are OK for now as there is still something in discussion from the spec team. Could you please write a small document on how to use the new feature. I am sure a lot of people will start using it after upgrading to the latest specification. Thanks.

Patch:
diff --git a/doc/validators.md b/doc/validators.md
index f8869b8..0cfbe9c 100644
--- a/doc/validators.md
+++ b/doc/validators.md
@@ -80,3 +80,27 @@ You can use GroovyKeyword like below:
   }
 }
 ````
+
+### Override Email/UUID/DateTime Validator
+
+In this library, if the format keyword is "email", "uuid", "date", "date-time", default validator provided by the library will be used.
+
+If you want to override this behaivor, do as belows.
+
+```
+public JsonSchemaFactory mySchemaFactory() {
+    // base on JsonMetaSchema.V201909 copy code below
+    String URI = "https://json-schema.org/draft/2019-09/schema";
+    String ID = "$id";
+
+    JsonMetaSchema overrideEmailValidatorMetaSchema = new JsonMetaSchema.Builder(URI)
+            .idKeyword(ID)
+            // Override EmailValidator
+            .addFormat(new PatternFormat("email", "^[a-zA-Z0-9.!#$%&'*+\\/=?^_`{|}~-]+@[a-zA-Z0-9-]+(?:\\.[a-zA-Z0-9-]+)*$"))
+            .build();
+
+    return new JsonSchemaFactory.Builder().defaultMetaSchemaURI(overrideEmailValidatorMetaSchema.getUri())
+            .addMetaSchema(overrideEmailValidatorMetaSchema)
+            .build();
+}
+```
\ No newline at end of file
diff --git a/src/main/java/com/networknt/schema/AllOfValidator.java b/src/main/java/com/networknt/schema/AllOfValidator.java
index 82b193d..548635f 100644
--- a/src/main/java/com/networknt/schema/AllOfValidator.java
+++ b/src/main/java/com/networknt/schema/AllOfValidator.java
@@ -16,22 +16,34 @@
 
 package com.networknt.schema;
 
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.Iterator;
+import java.util.LinkedHashSet;
+import java.util.List;
+import java.util.Set;
+
 import com.fasterxml.jackson.databind.JsonNode;
+import com.fasterxml.jackson.databind.node.ObjectNode;
 import org.slf4j.Logger;
 import org.slf4j.LoggerFactory;
 
-import java.util.*;
-
 public class AllOfValidator extends BaseJsonValidator implements JsonValidator {
     private static final Logger logger = LoggerFactory.getLogger(AllOfValidator.class);
 
-    private List<JsonSchema> schemas = new ArrayList<JsonSchema>();
-    
+    private final ValidationContext validationContext;
+    private final List<JsonSchema> schemas = new ArrayList<JsonSchema>();
+
     public AllOfValidator(String schemaPath, JsonNode schemaNode, JsonSchema parentSchema, ValidationContext validationContext) {
         super(schemaPath, schemaNode, parentSchema, ValidatorTypeCode.ALL_OF, validationContext);
+        this.validationContext = validationContext;
         int size = schemaNode.size();
         for (int i = 0; i < size; i++) {
-            schemas.add(new JsonSchema(validationContext, getValidatorType().getValue(), parentSchema.getCurrentUri(), schemaNode.get(i), parentSchema));
+            schemas.add(new JsonSchema(validationContext,
+                                       getValidatorType().getValue(),
+                                       parentSchema.getCurrentUri(),
+                                       schemaNode.get(i),
+                                       parentSchema));
         }
     }
 
@@ -42,20 +54,49 @@ public class AllOfValidator extends BaseJsonValidator implements JsonValidator {
 
         for (JsonSchema schema : schemas) {
             errors.addAll(schema.validate(node, rootNode, at));
+
+            if (config.isOpenAPI3StyleDiscriminators()) {
+                final Iterator<JsonNode> arrayElements = schemaNode.elements();
+                while (arrayElements.hasNext()) {
+                    final ObjectNode allOfEntry = (ObjectNode) arrayElements.next();
+                    final JsonNode $ref = allOfEntry.get("$ref");
+                    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);
+                        }
+                    }
+                }
+            }
         }
 
         return Collections.unmodifiableSet(errors);
     }
-    
-	@Override
-	public Set<ValidationMessage> walk(JsonNode node, JsonNode rootNode, String at, boolean shouldValidateSchema) {
-		Set<ValidationMessage> validationMessages = new LinkedHashSet<ValidationMessage>();
-
-		for (JsonSchema schema : schemas) {
-			// Walk through the schema
-			validationMessages.addAll(schema.walk(node, rootNode, at, shouldValidateSchema));
-		}
-		return Collections.unmodifiableSet(validationMessages);
-	}
+
+
+
+    @Override
+    public Set<ValidationMessage> walk(JsonNode node, JsonNode rootNode, String at, boolean shouldValidateSchema) {
+        Set<ValidationMessage> validationMessages = new LinkedHashSet<ValidationMessage>();
+
+        for (JsonSchema schema : schemas) {
+            // Walk through the schema
+            validationMessages.addAll(schema.walk(node, rootNode, at, shouldValidateSchema));
+        }
+        return Collections.unmodifiableSet(validationMessages);
+    }
 
 }
diff --git a/src/main/java/com/networknt/schema/AnyOfValidator.java b/src/main/java/com/networknt/schema/AnyOfValidator.java
index 5028e82..6ec5dad 100644
--- a/src/main/java/com/networknt/schema/AnyOfValidator.java
+++ b/src/main/java/com/networknt/schema/AnyOfValidator.java
@@ -24,40 +24,79 @@ import java.util.*;
 
 public class AnyOfValidator extends BaseJsonValidator implements JsonValidator {
     private static final Logger logger = LoggerFactory.getLogger(RequiredValidator.class);
+    private static final String REMARK = "Remaining validation messages report why candidate schemas didn't match";
+    private static final String DISCRIMINATOR_REMARK = "and the discriminator-selected candidate schema didn't pass validation";
 
-    private List<JsonSchema> schemas = new ArrayList<JsonSchema>();
+    private final List<JsonSchema> schemas = new ArrayList<JsonSchema>();
+    private final ValidationContext validationContext;
+    private final ValidationContext.DiscriminatorContext discriminatorContext;
 
     public AnyOfValidator(String schemaPath, JsonNode schemaNode, JsonSchema parentSchema, ValidationContext validationContext) {
         super(schemaPath, schemaNode, parentSchema, ValidatorTypeCode.ANY_OF, validationContext);
+        this.validationContext = validationContext;
         int size = schemaNode.size();
         for (int i = 0; i < size; i++) {
-            schemas.add(new JsonSchema(validationContext, getValidatorType().getValue(), parentSchema.getCurrentUri(), schemaNode.get(i), parentSchema));
+            schemas.add(new JsonSchema(validationContext,
+                                       getValidatorType().getValue(),
+                                       parentSchema.getCurrentUri(),
+                                       schemaNode.get(i),
+                                       parentSchema));
+        }
+
+        if (config.isOpenAPI3StyleDiscriminators()) {
+            this.discriminatorContext = new ValidationContext.DiscriminatorContext();
+        } else {
+            this.discriminatorContext = null;
         }
     }
 
     public Set<ValidationMessage> validate(JsonNode node, JsonNode rootNode, String at) {
         debug(logger, node, rootNode, at);
 
+        if (config.isOpenAPI3StyleDiscriminators()) {
+            validationContext.enterDiscriminatorContext(this.discriminatorContext, at);
+        }
+
         Set<ValidationMessage> allErrors = new LinkedHashSet<ValidationMessage>();
         String typeValidatorName = "anyOf/type";
 
-        for (JsonSchema schema : schemas) {
-            if (schema.getValidators().containsKey(typeValidatorName)) {
-                TypeValidator typeValidator = ((TypeValidator) schema.getValidators().get(typeValidatorName));
-                //If schema has type validator and node type doesn't match with schemaType then ignore it
-                //For union type, it is must to call TypeValidator
-                if (typeValidator.getSchemaType() != JsonType.UNION && !typeValidator.equalsToSchemaType(node)) {
-                    allErrors.add(buildValidationMessage(at, typeValidator.getSchemaType().toString()));
-                    continue;
+        try {
+            for (JsonSchema schema : schemas) {
+                if (schema.getValidators().containsKey(typeValidatorName)) {
+                    TypeValidator typeValidator = ((TypeValidator) schema.getValidators().get(typeValidatorName));
+                    //If schema has type validator and node type doesn't match with schemaType then ignore it
+                    //For union type, it is must to call TypeValidator
+                    if (typeValidator.getSchemaType() != JsonType.UNION && !typeValidator.equalsToSchemaType(node)) {
+                        allErrors.add(buildValidationMessage(at, typeValidator.getSchemaType().toString()));
+                        continue;
+                    }
                 }
+                Set<ValidationMessage> errors = schema.validate(node, rootNode, at);
+                if (errors.isEmpty() && !config.isOpenAPI3StyleDiscriminators()) {
+                    return errors;
+                } else if (config.isOpenAPI3StyleDiscriminators()) {
+                    if (discriminatorContext.isDiscriminatorMatchFound()) {
+                        if (!errors.isEmpty()) {
+                            errors.add(buildValidationMessage(at, DISCRIMINATOR_REMARK));
+                        }
+                        return errors;
+                    }
+                }
+                allErrors.addAll(errors);
+//                allErrors.add(renderMissingMatchValidationMessage(at));
             }
-            Set<ValidationMessage> errors = schema.validate(node, rootNode, at);
-            if (errors.isEmpty()) {
-                return errors;
+        } finally {
+            if (config.isOpenAPI3StyleDiscriminators()) {
+                validationContext.leaveDiscriminatorContextImmediately(at);
             }
-            allErrors.addAll(errors);
         }
         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/BaseJsonValidator.java b/src/main/java/com/networknt/schema/BaseJsonValidator.java
index 6299259..e03fc8f 100644
--- a/src/main/java/com/networknt/schema/BaseJsonValidator.java
+++ b/src/main/java/com/networknt/schema/BaseJsonValidator.java
@@ -16,14 +16,18 @@
 
 package com.networknt.schema;
 
-import com.fasterxml.jackson.databind.JsonNode;
-import org.apache.commons.lang3.StringUtils;
-import org.slf4j.Logger;
-
 import java.net.URI;
+import java.util.Iterator;
 import java.util.LinkedHashSet;
+import java.util.Map;
 import java.util.Set;
 
+import com.fasterxml.jackson.databind.JsonNode;
+import com.fasterxml.jackson.databind.node.ObjectNode;
+import com.networknt.schema.ValidationContext.DiscriminatorContext;
+import org.apache.commons.lang3.StringUtils;
+import org.slf4j.Logger;
+
 public abstract class BaseJsonValidator implements JsonValidator {
     protected String schemaPath;
     protected JsonNode schemaNode;
@@ -40,7 +44,7 @@ public abstract class BaseJsonValidator implements JsonValidator {
     public BaseJsonValidator(String schemaPath, JsonNode schemaNode, JsonSchema parentSchema,
                              ValidatorTypeCode validatorType, ValidationContext validationContext) {
         this(schemaPath, schemaNode, parentSchema, validatorType, false,
-                validationContext.getConfig() != null && validationContext.getConfig().isFailFast());
+             validationContext.getConfig() != null && validationContext.getConfig().isFailFast());
         this.config = validationContext.getConfig() == null ? new SchemaValidatorsConfig() : validationContext.getConfig();
     }
 
@@ -72,11 +76,14 @@ public abstract class BaseJsonValidator implements JsonValidator {
         return suppressSubSchemaRetrieval ? null : obtainSubSchemaNode(schemaNode, validationContext);
     }
 
-
     private static JsonSchema obtainSubSchemaNode(final JsonNode schemaNode, final ValidationContext validationContext) {
         final JsonNode node = schemaNode.get("id");
-        if (node == null) return null;
-        if (node.equals(schemaNode.get("$schema"))) return null;
+        if (node == null) {
+            return null;
+        }
+        if (node.equals(schemaNode.get("$schema"))) {
+            return null;
+        }
 
         final String text = node.textValue();
         if (text == null) {
@@ -118,7 +125,6 @@ public abstract class BaseJsonValidator implements JsonValidator {
         }
     }
 
-
     protected ValidationMessage buildValidationMessage(String at, String... arguments) {
         final ValidationMessage message = ValidationMessage.of(getValidatorType().getValue(), errorMessageType, at, arguments);
         if (failFast && !isPartOfOneOfMultipleType()) {
@@ -158,8 +164,126 @@ public abstract class BaseJsonValidator implements JsonValidator {
         return validationMessages;
     }
 
-    protected boolean isPartOfOneOfMultipleType(){
+    protected boolean isPartOfOneOfMultipleType() {
         return parentSchema.schemaPath.equals(ValidatorTypeCode.ONE_OF.getValue());
     }
 
+    /* ********************** START OF OpenAPI 3.0.x DISCRIMINATOR METHODS ********************************* */
+
+    /**
+     * Checks based on the current {@link DiscriminatorContext} whether the provided {@link JsonSchema} a match against
+     * against the current discriminator.
+     *
+     * @param currentDiscriminatorContext the currently active {@link DiscriminatorContext}
+     * @param discriminator the discriminator to use for the check
+     * @param discriminatorPropertyValue the value of the <code>discriminator/propertyName</code> field
+     * @param jsonSchema the {@link JsonSchema} to check
+     */
+    protected static void checkDiscriminatorMatch(final ValidationContext.DiscriminatorContext currentDiscriminatorContext,
+                                                  final ObjectNode discriminator,
+                                                  final String discriminatorPropertyValue,
+                                                  final JsonSchema jsonSchema) {
+        final JsonNode discriminatorMapping = discriminator.get("mapping");
+        if (null == discriminatorMapping) {
+            checkForImplicitDiscriminatorMappingMatch(currentDiscriminatorContext,
+                                                      discriminatorPropertyValue,
+                                                      jsonSchema);
+        } else {
+            checkForExplicitDiscriminatorMappingMatch(currentDiscriminatorContext,
+                                                      discriminatorPropertyValue,
+                                                      discriminatorMapping,
+                                                      jsonSchema);
+            if (!currentDiscriminatorContext.isDiscriminatorMatchFound()
+                    && noExplicitDiscriminatorKeyOverride(discriminatorMapping, jsonSchema)) {
+                checkForImplicitDiscriminatorMappingMatch(currentDiscriminatorContext,
+                                                          discriminatorPropertyValue,
+                                                          jsonSchema);
+            }
+        }
+    }
+
+    /**
+     * Rolls up all nested and compatible discriminators to the root discriminator of the type. Detects attempts to redefine
+     * the <code>propertyName</code> or mappings.
+     *
+     * @param currentDiscriminatorContext the currently active {@link DiscriminatorContext}
+     * @param discriminator the discriminator to use for the check
+     * @param schema the value of the <code>discriminator/propertyName</code> field
+     * @param at the logging prefix
+     */
+    protected static void registerAndMergeDiscriminator(final DiscriminatorContext currentDiscriminatorContext,
+                                                        final ObjectNode discriminator,
+                                                        final JsonSchema schema,
+                                                        final String at) {
+        final JsonNode discriminatorOnSchema = schema.schemaNode.get("discriminator");
+        if (null != discriminatorOnSchema && null != currentDiscriminatorContext
+                .getDiscriminatorForPath(schema.schemaPath)) {
+            // this is where A -> B -> C inheritance exists, A has the root discriminator and B adds to the mapping
+            final JsonNode propertyName = discriminatorOnSchema.get("propertyName");
+            if (null != propertyName) {
+                throw new JsonSchemaException(at + " schema " + schema + " attempts redefining the discriminator property");
+            }
+            final ObjectNode mappingOnContextDiscriminator = (ObjectNode) discriminator.get("mapping");
+            final ObjectNode mappingOnCurrentSchemaDiscriminator = (ObjectNode) discriminatorOnSchema.get("mapping");
+            if (null == mappingOnContextDiscriminator && null != mappingOnCurrentSchemaDiscriminator) {
+                // here we have a mapping on a nested discriminator and none on the root discriminator, so we can simply
+                // make it the root's
+                discriminator.set("mapping", discriminatorOnSchema);
+            } else if (null != mappingOnContextDiscriminator && null != mappingOnCurrentSchemaDiscriminator) {
+                // here we have to merge. The spec doesn't specify anything on this, but here we don't accept redefinition of
+                // mappings that already exist
+                final Iterator<Map.Entry<String, JsonNode>> fieldsToAdd = mappingOnCurrentSchemaDiscriminator.fields();
+                while (fieldsToAdd.hasNext()) {
+                    final Map.Entry<String, JsonNode> fieldToAdd = fieldsToAdd.next();
+                    final String mappingKeyToAdd = fieldToAdd.getKey();
+                    final JsonNode mappingValueToAdd = fieldToAdd.getValue();
+
+                    final JsonNode currentMappingValue = mappingOnContextDiscriminator.get(mappingKeyToAdd);
+                    if (null != currentMappingValue && currentMappingValue != mappingValueToAdd) {
+                        throw new JsonSchemaException(at + "discriminator mapping redefinition from " + mappingKeyToAdd
+                                                              + "/" + currentMappingValue + " to " + mappingValueToAdd);
+                    } else if (null == currentMappingValue) {
+                        mappingOnContextDiscriminator.set(mappingKeyToAdd, mappingValueToAdd);
+                    }
+                }
+            }
+        }
+        currentDiscriminatorContext.registerDiscriminator(schema.schemaPath, discriminator);
+    }
+
+    private static void checkForImplicitDiscriminatorMappingMatch(final DiscriminatorContext currentDiscriminatorContext,
+                                                                  final String discriminatorPropertyValue,
+                                                                  final JsonSchema schema) {
+        if (schema.schemaPath.endsWith("/" + discriminatorPropertyValue)) {
+            currentDiscriminatorContext.markMatch();
+        }
+    }
+
+    private static void checkForExplicitDiscriminatorMappingMatch(final DiscriminatorContext currentDiscriminatorContext,
+                                                                  final String discriminatorPropertyValue,
+                                                                  final JsonNode discriminatorMapping,
+                                                                  final JsonSchema schema) {
+        final Iterator<Map.Entry<String, JsonNode>> explicitMappings = discriminatorMapping.fields();
+        while (explicitMappings.hasNext()) {
+            final Map.Entry<String, JsonNode> candidateExplicitMapping = explicitMappings.next();
+            if (candidateExplicitMapping.getKey().equals(discriminatorPropertyValue)
+                    && schema.schemaPath.equals(candidateExplicitMapping.getValue().asText())) {
+                currentDiscriminatorContext.markMatch();
+                break;
+            }
+        }
+    }
+
+    private static boolean noExplicitDiscriminatorKeyOverride(final JsonNode discriminatorMapping,
+                                                              final JsonSchema parentSchema) {
+        final Iterator<Map.Entry<String, JsonNode>> explicitMappings = discriminatorMapping.fields();
+        while (explicitMappings.hasNext()) {
+            final Map.Entry<String, JsonNode> candidateExplicitMapping = explicitMappings.next();
+            if (candidateExplicitMapping.getValue().asText().equals(parentSchema.schemaPath)) {
+                return false;
+            }
+        }
+        return true;
+    }
+
 }
diff --git a/src/main/java/com/networknt/schema/FormatKeyword.java b/src/main/java/com/networknt/schema/FormatKeyword.java
index 931da5e..f17d0e6 100644
--- a/src/main/java/com/networknt/schema/FormatKeyword.java
+++ b/src/main/java/com/networknt/schema/FormatKeyword.java
@@ -48,6 +48,10 @@ public class FormatKeyword implements Keyword {
         if (schemaNode != null && schemaNode.isTextual()) {
             String formatName = schemaNode.textValue();
             format = formats.get(formatName);
+            // if you set custom format, override default Email/DateTime/UUID Validator
+            if (format != null) {
+                return new FormatValidator(schemaPath, schemaNode, parentSchema, validationContext, format);
+            }
             // Validate date and time separately
             if (formatName.equals(DATE) || formatName.equals(DATE_TIME)) {
                 return new DateTimeValidator(schemaPath, schemaNode, parentSchema, validationContext, formatName);
diff --git a/src/main/java/com/networknt/schema/JsonSchema.java b/src/main/java/com/networknt/schema/JsonSchema.java
index ac6497c..d065178 100644
--- a/src/main/java/com/networknt/schema/JsonSchema.java
+++ b/src/main/java/com/networknt/schema/JsonSchema.java
@@ -17,6 +17,8 @@
 package com.networknt.schema;
 
 import com.fasterxml.jackson.databind.JsonNode;
+import com.fasterxml.jackson.databind.node.ObjectNode;
+import com.networknt.schema.ValidationContext.DiscriminatorContext;
 import com.networknt.schema.walk.DefaultKeywordWalkListenerRunner;
 import com.networknt.schema.walk.JsonSchemaWalker;
 import com.networknt.schema.walk.WalkListenerRunner;
@@ -70,14 +72,21 @@ public class JsonSchema extends BaseJsonValidator {
     private JsonSchema(ValidationContext validationContext, String schemaPath, URI currentUri, JsonNode schemaNode,
                        JsonSchema parent, boolean suppressSubSchemaRetrieval) {
         super(schemaPath, schemaNode, parent, null, suppressSubSchemaRetrieval,
-                validationContext.getConfig() != null && validationContext.getConfig().isFailFast());
+              validationContext.getConfig() != null && validationContext.getConfig().isFailFast());
         this.validationContext = validationContext;
         this.config = validationContext.getConfig();
         this.idKeyword = validationContext.getMetaSchema().getIdKeyword();
         this.currentUri = this.combineCurrentUriWithIds(currentUri, schemaNode);
-		if (config != null) {
-			this.keywordWalkListenerRunner = new DefaultKeywordWalkListenerRunner(config.getKeywordWalkListenersMap());
-		}
+        if (config != null) {
+            this.keywordWalkListenerRunner = new DefaultKeywordWalkListenerRunner(config.getKeywordWalkListenersMap());
+
+            if (config.isOpenAPI3StyleDiscriminators()) {
+                ObjectNode discriminator = (ObjectNode) schemaNode.get("discriminator");
+                if (null != discriminator) {
+                    validationContext.getCurrentDiscriminatorContext().registerDiscriminator(schemaPath, discriminator);
+                }
+            }
+        }
     }
 
     private URI combineCurrentUriWithIds(URI currentUri, JsonNode schemaNode) {
@@ -90,7 +99,10 @@ public class JsonSchema extends BaseJsonValidator {
             try {
                 return this.validationContext.getURIFactory().create(currentUri, id);
             } catch (IllegalArgumentException e) {
-                throw new JsonSchemaException(ValidationMessage.of(ValidatorTypeCode.ID.getValue(), ValidatorTypeCode.ID, id, currentUri == null ? "null" : currentUri.toString()));
+                throw new JsonSchemaException(ValidationMessage.of(ValidatorTypeCode.ID.getValue(),
+                                                                   ValidatorTypeCode.ID,
+                                                                   id,
+                                                                   currentUri == null ? "null" : currentUri.toString()));
             }
         }
     }
@@ -182,11 +194,10 @@ public class JsonSchema extends BaseJsonValidator {
         return false;
     }
 
-    
-	/**
-	 * Please note that the key in {@link #validators} map is a schema path. It is
-	 * used in {@link KeywordWalkListenerRunner} to derive the keyword.
-	 */
+    /**
+     * Please note that the key in {@link #validators} map is a schema path. It is
+     * used in {@link com.networknt.schema.walk.DefaultKeywordWalkListenerRunner} to derive the keyword.
+     */
     private Map<String, JsonValidator> read(JsonNode schemaNode) {
         Map<String, JsonValidator> validators = new HashMap<String, JsonValidator>();
         if (schemaNode.isBoolean()) {
@@ -207,15 +218,16 @@ public class JsonSchema extends BaseJsonValidator {
                 if (validator != null) {
                     validators.put(getSchemaPath() + "/" + pname, validator);
 
-                    if (pname.equals("required"))
+                    if (pname.equals("required")) {
                         requiredValidator = validator;
+                    }
                 }
 
             }
         }
         return validators;
     }
-    
+
     /************************ START OF VALIDATE METHODS **********************************/
 
     public Set<ValidationMessage> validate(JsonNode jsonNode, JsonNode rootNode, String at) {
@@ -228,6 +240,21 @@ public class JsonSchema extends BaseJsonValidator {
             // Validate.
             errors.addAll(v.validate(jsonNode, rootNode, at));
         }
+        if (null != config && config.isOpenAPI3StyleDiscriminators()) {
+            ObjectNode discriminator = (ObjectNode) schemaNode.get("discriminator");
+            if (null != discriminator) {
+                final DiscriminatorContext discriminatorContext = validationContext.getCurrentDiscriminatorContext();
+                if (null != discriminatorContext) {
+                    final ObjectNode discriminatorFromContext = discriminatorContext.getDiscriminatorForPath(schemaPath);
+                    final String discriminatorPropertyName = discriminatorFromContext.get("propertyName").asText();
+                    final String discriminatorPropertyValue = jsonNode.get(discriminatorPropertyName).asText();
+                    checkDiscriminatorMatch(discriminatorContext,
+                                            discriminatorFromContext,
+                                            discriminatorPropertyValue,
+                                            this);
+                }
+            }
+        }
         return errors;
     }
 
@@ -260,9 +287,9 @@ public class JsonSchema extends BaseJsonValidator {
             ThreadInfo.remove(CollectorContext.COLLECTOR_CONTEXT_THREAD_LOCAL_KEY);
         }
     }
-    
+
     /************************ END OF VALIDATE METHODS **********************************/
-    
+
     /************************ START OF WALK METHODS **********************************/
 
     /**
@@ -284,7 +311,6 @@ public class JsonSchema extends BaseJsonValidator {
         ValidationResult validationResult = new ValidationResult(errors, collectorContext);
         return validationResult;
     }
-    
 
     @Override
     public Set<ValidationMessage> walk(JsonNode node, JsonNode rootNode, String at, boolean shouldValidateSchema) {
@@ -296,47 +322,60 @@ public class JsonSchema extends BaseJsonValidator {
             try {
                 // Call all the pre-walk listeners. If atleast one of the pre walk listeners
                 // returns SKIP, then skip the walk.
-                if (keywordWalkListenerRunner.runPreWalkListeners(schemaPathWithKeyword, node, rootNode, at, schemaPath,
-                        schemaNode, parentSchema, validationContext.getJsonSchemaFactory())) {
+                if (keywordWalkListenerRunner.runPreWalkListeners(schemaPathWithKeyword,
+                                                                  node,
+                                                                  rootNode,
+                                                                  at,
+                                                                  schemaPath,
+                                                                  schemaNode,
+                                                                  parentSchema,
+                                                                  validationContext.getJsonSchemaFactory())) {
                     validationMessages.addAll(jsonWalker.walk(node, rootNode, at, shouldValidateSchema));
                 }
             } finally {
                 // Call all the post-walk listeners.
-                keywordWalkListenerRunner.runPostWalkListeners(schemaPathWithKeyword, node, rootNode, at, schemaPath,
-                        schemaNode, parentSchema, validationContext.getJsonSchemaFactory(), validationMessages);
+                keywordWalkListenerRunner.runPostWalkListeners(schemaPathWithKeyword,
+                                                               node,
+                                                               rootNode,
+                                                               at,
+                                                               schemaPath,
+                                                               schemaNode,
+                                                               parentSchema,
+                                                               validationContext.getJsonSchemaFactory(),
+                                                               validationMessages);
             }
         }
         return validationMessages;
-	}
-	
-	 /************************ END OF WALK METHODS **********************************/
-
-     private void setValidatorState(boolean isWalkEnabled, boolean shouldValidateSchema) {
-         // Get the Validator state object storing validation data
-         Object stateObj = CollectorContext.getInstance().get(ValidatorState.VALIDATOR_STATE_KEY);
-         // if one has not been created, instantiate one
-         if (stateObj == null) {
-             ValidatorState state = new ValidatorState();
-             state.setWalkEnabled(isWalkEnabled);
-             state.setValidationEnabled(shouldValidateSchema);
-             CollectorContext.getInstance().add(ValidatorState.VALIDATOR_STATE_KEY, state);
-         }
-     }
-
-
-     public CollectorContext getCollectorContext() {
-        CollectorContext collectorContext = (CollectorContext) ThreadInfo.get(CollectorContext.COLLECTOR_CONTEXT_THREAD_LOCAL_KEY);
-         if (collectorContext == null) {
-             if (this.config != null && this.config.getCollectorContext() != null) {
-                 collectorContext = this.config.getCollectorContext();
-             } else {
-                 collectorContext = new CollectorContext();
-             }
-             // Set the collector context in thread info, this is unique for every thread.
-             ThreadInfo.set(CollectorContext.COLLECTOR_CONTEXT_THREAD_LOCAL_KEY, collectorContext);
-         }
-         return collectorContext;
-     }
+    }
+
+    /************************ END OF WALK METHODS **********************************/
+
+    private void setValidatorState(boolean isWalkEnabled, boolean shouldValidateSchema) {
+        // Get the Validator state object storing validation data
+        Object stateObj = CollectorContext.getInstance().get(ValidatorState.VALIDATOR_STATE_KEY);
+        // if one has not been created, instantiate one
+        if (stateObj == null) {
+            ValidatorState state = new ValidatorState();
+            state.setWalkEnabled(isWalkEnabled);
+            state.setValidationEnabled(shouldValidateSchema);
+            CollectorContext.getInstance().add(ValidatorState.VALIDATOR_STATE_KEY, state);
+        }
+    }
+
+    public CollectorContext getCollectorContext() {
+        CollectorContext collectorContext = (CollectorContext) ThreadInfo
+                .get(CollectorContext.COLLECTOR_CONTEXT_THREAD_LOCAL_KEY);
+        if (collectorContext == null) {
+            if (this.config != null && this.config.getCollectorContext() != null) {
+                collectorContext = this.config.getCollectorContext();
+            } else {
+                collectorContext = new CollectorContext();
+            }
+            // Set the collector context in thread info, this is unique for every thread.
+            ThreadInfo.set(CollectorContext.COLLECTOR_CONTEXT_THREAD_LOCAL_KEY, collectorContext);
+        }
+        return collectorContext;
+    }
 
     @Override
     public String toString() {
@@ -344,7 +383,7 @@ public class JsonSchema extends BaseJsonValidator {
     }
 
     public boolean hasRequiredValidator() {
-        return requiredValidator != null ? true : false;
+        return requiredValidator != null;
     }
 
     public JsonValidator getRequiredValidator() {
diff --git a/src/main/java/com/networknt/schema/JsonSchemaRef.java b/src/main/java/com/networknt/schema/JsonSchemaRef.java
index d2bd1d2..8acedf4 100644
--- a/src/main/java/com/networknt/schema/JsonSchemaRef.java
+++ b/src/main/java/com/networknt/schema/JsonSchemaRef.java
@@ -28,23 +28,12 @@ import java.util.Set;
 
 public class JsonSchemaRef {
 
-    private JsonSchema schema;
-    private ValidationContext validationContext;
-    private String refValue;
-
-    public JsonSchemaRef(ValidationContext validationContext, String refValue) {
-        this.validationContext = validationContext;
-        this.refValue = refValue;
-    }
+    private final JsonSchema schema;
 
     public JsonSchemaRef(JsonSchema schema) {
         this.schema = schema;
     }
 
-    public void set(JsonSchema schema) {
-        this.schema = schema;
-    }
-
     public Set<ValidationMessage> validate(JsonNode node, JsonNode rootNode, String at) {
         return schema.validate(node, rootNode, at);
     }
diff --git a/src/main/java/com/networknt/schema/RefValidator.java b/src/main/java/com/networknt/schema/RefValidator.java
index 51db5d3..167d379 100644
--- a/src/main/java/com/networknt/schema/RefValidator.java
+++ b/src/main/java/com/networknt/schema/RefValidator.java
@@ -95,10 +95,9 @@ public class RefValidator extends BaseJsonValidator implements JsonValidator {
             if (node != null) {
                 JsonSchemaRef ref = validationContext.getReferenceParsingInProgress(refValueOriginal);
                 if (ref == null) {
-                    ref = new JsonSchemaRef(validationContext, refValue);
+                    final JsonSchema schema = new JsonSchema(validationContext, refValue, parentSchema.getCurrentUri(), node, parentSchema);
+                    ref = new JsonSchemaRef(schema);
                     validationContext.setReferenceParsingInProgress(refValueOriginal, ref);
-                    JsonSchema ret = new JsonSchema(validationContext, refValue, parentSchema.getCurrentUri(), node, parentSchema);
-                    ref.set(ret);
                 }
                 return ref;
             }
diff --git a/src/main/java/com/networknt/schema/SchemaValidatorsConfig.java b/src/main/java/com/networknt/schema/SchemaValidatorsConfig.java
index 2fa9899..6d96cad 100644
--- a/src/main/java/com/networknt/schema/SchemaValidatorsConfig.java
+++ b/src/main/java/com/networknt/schema/SchemaValidatorsConfig.java
@@ -16,14 +16,19 @@
 
 package com.networknt.schema;
 
+import java.util.ArrayList;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+import java.util.Set;
+
 import com.fasterxml.jackson.databind.JsonNode;
 import com.networknt.schema.walk.JsonSchemaWalkListener;
 
-import java.util.*;
-
 public class SchemaValidatorsConfig {
     /**
-     * when validate type, if TYPE_LOOSE = true, will try to convert string to different types to match the type defined in schema.
+     * when validate type, if TYPE_LOOSE = true, will try to convert string to different types to match the type defined in
+     * schema.
      */
     private boolean typeLoose;
 
@@ -47,6 +52,12 @@ public class SchemaValidatorsConfig {
      */
     private boolean losslessNarrowing;
 
+    /**
+     * When set to true, support for discriminators is enabled for validations of oneOf, anyOf and allOf as described
+     * on <a href="https://github.com/OAI/OpenAPI-Specification/blob/master/versions/3.0.3.md#discriminatorObject">GitHub</a>.
+     */
+    private boolean openAPI3StyleDiscriminators = false;
+
     /**
      * Map of public, normally internet accessible schema URLs to alternate locations; this allows for offline
      * validation of schemas that refer to public URLs. This is merged with any mappings the {@link JsonSchemaFactory}
@@ -63,14 +74,15 @@ public class SchemaValidatorsConfig {
      * validator using the SchemaValidator to handle it.
      */
     private boolean handleNullableField = true;
-    
+
     // This is just a constant for listening to all Keywords.
     public static final String ALL_KEYWORD_WALK_LISTENER_KEY = "com.networknt.AllKeywordWalkListener";
 
-    private final Map<String, List<JsonSchemaWalkListener>> keywordWalkListenersMap = new HashMap<String, List<JsonSchemaWalkListener>>();
-    
+    private final Map<String, List<JsonSchemaWalkListener>> keywordWalkListenersMap = new HashMap<String,
+            List<JsonSchemaWalkListener>>();
+
     private final List<JsonSchemaWalkListener> propertyWalkListeners = new ArrayList<JsonSchemaWalkListener>();
-    
+
     private final List<JsonSchemaWalkListener> itemWalkListeners = new ArrayList<JsonSchemaWalkListener>();
 
     private CollectorContext collectorContext;
@@ -123,70 +135,73 @@ public class SchemaValidatorsConfig {
         this.ecma262Validator = ecma262Validator;
     }
 
-    public boolean isJavaSemantics() { return javaSemantics; }
+    public boolean isJavaSemantics() {
+        return javaSemantics;
+    }
 
-    public void setJavaSemantics(boolean javaSemantics) { this.javaSemantics = javaSemantics; }
+    public void setJavaSemantics(boolean javaSemantics) {
+        this.javaSemantics = javaSemantics;
+    }
 
     public void addKeywordWalkListener(JsonSchemaWalkListener keywordWalkListener) {
-		if (keywordWalkListenersMap.get(ALL_KEYWORD_WALK_LISTENER_KEY) == null) {
-			List<JsonSchemaWalkListener> keywordWalkListeners = new ArrayList<JsonSchemaWalkListener>();
-			keywordWalkListenersMap.put(ALL_KEYWORD_WALK_LISTENER_KEY, keywordWalkListeners);
-		}
-		keywordWalkListenersMap.get(ALL_KEYWORD_WALK_LISTENER_KEY).add(keywordWalkListener);
-	}
-	
-	public void addKeywordWalkListener(String keyword, JsonSchemaWalkListener keywordWalkListener) {
-		if (keywordWalkListenersMap.get(keyword) == null) {
-			List<JsonSchemaWalkListener> keywordWalkListeners = new ArrayList<JsonSchemaWalkListener>();
-			keywordWalkListenersMap.put(keyword, keywordWalkListeners);
-		}
-		keywordWalkListenersMap.get(keyword).add(keywordWalkListener);
-	}
-    
-   
-	public void addKeywordWalkListeners(List<JsonSchemaWalkListener> keywordWalkListeners) {
-		if (keywordWalkListenersMap.get(ALL_KEYWORD_WALK_LISTENER_KEY) == null) {
-			List<JsonSchemaWalkListener> ikeywordWalkListeners = new ArrayList<JsonSchemaWalkListener>();
-			keywordWalkListenersMap.put(ALL_KEYWORD_WALK_LISTENER_KEY, ikeywordWalkListeners);
-		}
-		keywordWalkListenersMap.get(ALL_KEYWORD_WALK_LISTENER_KEY).addAll(keywordWalkListeners);
-	}
-	
-	public void addKeywordWalkListeners(String keyword, List<JsonSchemaWalkListener> keywordWalkListeners) {
-		if (keywordWalkListenersMap.get(keyword) == null) {
-			List<JsonSchemaWalkListener> ikeywordWalkListeners = new ArrayList<JsonSchemaWalkListener>();
-			keywordWalkListenersMap.put(keyword, ikeywordWalkListeners);
-		}
-		keywordWalkListenersMap.get(keyword).addAll(keywordWalkListeners);
-	}
-	
-	public void addPropertyWalkListeners(List<JsonSchemaWalkListener> propertyWalkListeners) {
-		this.propertyWalkListeners.addAll(propertyWalkListeners);
-	}
-
-	public void addPropertyWalkListener(JsonSchemaWalkListener propertyWalkListener) {
-		this.propertyWalkListeners.add(propertyWalkListener);
-    }
-    
+        if (keywordWalkListenersMap.get(ALL_KEYWORD_WALK_LISTENER_KEY) == null) {
+            List<JsonSchemaWalkListener> keywordWalkListeners = new ArrayList<JsonSchemaWalkListener>();
+            keywordWalkListenersMap.put(ALL_KEYWORD_WALK_LISTENER_KEY, keywordWalkListeners);
+        }
+        keywordWalkListenersMap.get(ALL_KEYWORD_WALK_LISTENER_KEY).add(keywordWalkListener);
+    }
+
+    public void addKeywordWalkListener(String keyword, JsonSchemaWalkListener keywordWalkListener) {
+        if (keywordWalkListenersMap.get(keyword) == null) {
+            List<JsonSchemaWalkListener> keywordWalkListeners = new ArrayList<JsonSchemaWalkListener>();
+            keywordWalkListenersMap.put(keyword, keywordWalkListeners);
+        }
+        keywordWalkListenersMap.get(keyword).add(keywordWalkListener);
+    }
+
+    public void addKeywordWalkListeners(List<JsonSchemaWalkListener> keywordWalkListeners) {
+        if (keywordWalkListenersMap.get(ALL_KEYWORD_WALK_LISTENER_KEY) == null) {
+            List<JsonSchemaWalkListener> ikeywordWalkListeners = new ArrayList<JsonSchemaWalkListener>();
+            keywordWalkListenersMap.put(ALL_KEYWORD_WALK_LISTENER_KEY, ikeywordWalkListeners);
+        }
+        keywordWalkListenersMap.get(ALL_KEYWORD_WALK_LISTENER_KEY).addAll(keywordWalkListeners);
+    }
+
+    public void addKeywordWalkListeners(String keyword, List<JsonSchemaWalkListener> keywordWalkListeners) {
+        if (keywordWalkListenersMap.get(keyword) == null) {
+            List<JsonSchemaWalkListener> ikeywordWalkListeners = new ArrayList<JsonSchemaWalkListener>();
+            keywordWalkListenersMap.put(keyword, ikeywordWalkListeners);
+        }
+        keywordWalkListenersMap.get(keyword).addAll(keywordWalkListeners);
+    }
+
+    public void addPropertyWalkListeners(List<JsonSchemaWalkListener> propertyWalkListeners) {
+        this.propertyWalkListeners.addAll(propertyWalkListeners);
+    }
+
+    public void addPropertyWalkListener(JsonSchemaWalkListener propertyWalkListener) {
+        this.propertyWalkListeners.add(propertyWalkListener);
+    }
+
     public void addItemWalkListener(JsonSchemaWalkListener itemWalkListener) {
-		this.itemWalkListeners.add(itemWalkListener);
+        this.itemWalkListeners.add(itemWalkListener);
     }
-    
+
     public void addItemWalkListeners(List<JsonSchemaWalkListener> itemWalkListeners) {
-		this.itemWalkListeners.addAll(itemWalkListeners);
-	}
-	
-	public List<JsonSchemaWalkListener> getPropertyWalkListeners() {
-		return this.propertyWalkListeners;
-	}
+        this.itemWalkListeners.addAll(itemWalkListeners);
+    }
+
+    public List<JsonSchemaWalkListener> getPropertyWalkListeners() {
+        return this.propertyWalkListeners;
+    }
+
+    public Map<String, List<JsonSchemaWalkListener>> getKeywordWalkListenersMap() {
+        return this.keywordWalkListenersMap;
+    }
 
-	public Map<String, List<JsonSchemaWalkListener>> getKeywordWalkListenersMap() {
-		return this.keywordWalkListenersMap;
+    public List<JsonSchemaWalkListener> getArrayItemWalkListeners() {
+        return this.itemWalkListeners;
     }
-    
-	public List<JsonSchemaWalkListener> getArrayItemWalkListeners() {
-		return this.itemWalkListeners;
-	}
 
     public SchemaValidatorsConfig() {
     }
@@ -194,6 +209,7 @@ public class SchemaValidatorsConfig {
     public CollectorContext getCollectorContext() {
         return collectorContext;
     }
+
     public void setCollectorContext(CollectorContext collectorContext) {
         this.collectorContext = collectorContext;
     }
@@ -205,4 +221,40 @@ public class SchemaValidatorsConfig {
     public void setLosslessNarrowing(boolean losslessNarrowing) {
         this.losslessNarrowing = losslessNarrowing;
     }
+
+    /**
+     * Indicates whether OpenAPI 3 style discriminators should be supported
+     * @return true in case discriminators are enabled
+     * @since 1.0.51
+     */
+    public boolean isOpenAPI3StyleDiscriminators() {
+        return openAPI3StyleDiscriminators;
+    }
+
+    /**
+     * When enabled, the validation of <code>anyOf</code> and <code>allOf</code> in polymorphism will respect
+     * OpenAPI 3 style discriminators as described in the
+     * <a href="https://github.com/OAI/OpenAPI-Specification/blob/master/versions/3.0.3.md#discriminatorObject">OpenAPI 3.0.3 spec</a>.
+     * The presence of a discriminator configuration on the schema will lead to the following changes in the behavior:
+     * <ul>
+     *     <li>for <code>oneOf</code> the spec is unfortunately very vague. Whether <code>oneOf</code> semantics should be
+     *     affected by discriminators or not is not even 100% clear within the members of the OAS steering committee. Therefore
+     *     <code>oneOf</code> at the moment ignores discriminators</li>
+     *     <li>for <code>anyOf</code> the validation will choose one of the candidate schemas for validation based on the
+     *     discriminator property value and will pass validation when this specific schema passes. This is in particular useful
+     *     when the payload could match multiple candidates in the <code>anyOf</code> list and could lead to ambiguity. Example:
+     *     type B has all mandatory properties of A and adds more mandatory ones. Whether the payload is an A or B is determined
+     *     via the discriminator property name. A payload indicating it is an instance of B then requires passing the validation
+     *     of B and passing the validation of A would not be sufficient anymore.</li>
+     *     <li>for <code>allOf</code> use cases with discriminators defined on the copied-in parent type, it is possible to
+     *     automatically validate against a subtype. Example: some schema specifies that there is a field of type A. A carries
+     *     a discriminator field and B inherits from A. Then B is automatically a candidate for validation as well and will be
+     *     chosen in case the discriminator property matches</li>
+     * </ul>
+     * @param openAPI3StyleDiscriminators whether or not discriminators should be used. Defaults to <code>false</code>
+     * @since 1.0.51
+     */
+    public void setOpenAPI3StyleDiscriminators(boolean openAPI3StyleDiscriminators) {
+        this.openAPI3StyleDiscriminators = openAPI3StyleDiscriminators;
+    }
 }
diff --git a/src/main/java/com/networknt/schema/ValidationContext.java b/src/main/java/com/networknt/schema/ValidationContext.java
index d8f3a1f..05d4b5a 100644
--- a/src/main/java/com/networknt/schema/ValidationContext.java
+++ b/src/main/java/com/networknt/schema/ValidationContext.java
@@ -16,13 +16,15 @@
 
 package com.networknt.schema;
 
+import java.util.HashMap;
+import java.util.Map;
+import java.util.Stack;
+
 import com.fasterxml.jackson.databind.JsonNode;
+import com.fasterxml.jackson.databind.node.ObjectNode;
 import com.networknt.schema.uri.URIFactory;
 import com.networknt.schema.urn.URNFactory;
 
-import java.util.HashMap;
-import java.util.Map;
-
 public class ValidationContext {
     private final URIFactory uriFactory;
     private final URNFactory urnFactory;
@@ -30,24 +32,25 @@ public class ValidationContext {
     private final JsonSchemaFactory jsonSchemaFactory;
     private SchemaValidatorsConfig config;
     private final Map<String, JsonSchemaRef> refParsingInProgress = new HashMap<String, JsonSchemaRef>();
-
-	public ValidationContext(URIFactory uriFactory, URNFactory urnFactory, JsonMetaSchema metaSchema,
-			JsonSchemaFactory jsonSchemaFactory, SchemaValidatorsConfig config) {
-		if (uriFactory == null) {
-			throw new IllegalArgumentException("URIFactory must not be null");
-		}
-		if (metaSchema == null) {
-			throw new IllegalArgumentException("JsonMetaSchema must not be null");
-		}
-		if (jsonSchemaFactory == null) {
-			throw new IllegalArgumentException("JsonSchemaFactory must not be null");
-		}
-		this.uriFactory = uriFactory;
-		this.urnFactory = urnFactory;
-		this.metaSchema = metaSchema;
-		this.jsonSchemaFactory = jsonSchemaFactory;
-		this.config = config;
-	}
+    private final Stack<DiscriminatorContext> discriminatorContexts = new Stack<DiscriminatorContext>();
+
+    public ValidationContext(URIFactory uriFactory, URNFactory urnFactory, JsonMetaSchema metaSchema,
+                             JsonSchemaFactory jsonSchemaFactory, SchemaValidatorsConfig config) {
+        if (uriFactory == null) {
+            throw new IllegalArgumentException("URIFactory must not be null");
+        }
+        if (metaSchema == null) {
+            throw new IllegalArgumentException("JsonMetaSchema must not be null");
+        }
+        if (jsonSchemaFactory == null) {
+            throw new IllegalArgumentException("JsonSchemaFactory must not be null");
+        }
+        this.uriFactory = uriFactory;
+        this.urnFactory = urnFactory;
+        this.metaSchema = metaSchema;
+        this.jsonSchemaFactory = jsonSchemaFactory;
+        this.config = config;
+    }
 
     public JsonValidator newValidator(String schemaPath, String keyword /* keyword */, JsonNode schemaNode,
                                       JsonSchema parentSchema) {
@@ -86,8 +89,41 @@ public class ValidationContext {
         return refParsingInProgress.get(refValue);
     }
 
+    public DiscriminatorContext getCurrentDiscriminatorContext() {
+        return discriminatorContexts.peek();
+    }
+
+    public void enterDiscriminatorContext(final DiscriminatorContext ctx, String at) {
+        discriminatorContexts.push(ctx);
+    }
+
+    public void leaveDiscriminatorContextImmediately(String at) {
+        discriminatorContexts.pop();
+    }
+
     protected JsonMetaSchema getMetaSchema() {
         return metaSchema;
     }
 
+    public static class DiscriminatorContext {
+        private final Map<String, ObjectNode> discriminators = new HashMap<String, ObjectNode>();
+
+        private boolean discriminatorMatchFound = false;
+
+        public void registerDiscriminator(final String schemaPath, final ObjectNode discriminator) {
+            discriminators.put(schemaPath, discriminator);
+        }
+
+        public ObjectNode getDiscriminatorForPath(final String schemaPath) {
+            return discriminators.get(schemaPath);
+        }
+
+        public void markMatch() {
+            discriminatorMatchFound = true;
+        }
+
+        public boolean isDiscriminatorMatchFound() {
+            return discriminatorMatchFound;
+        }
+    }
 }


Test patch:
diff --git a/src/test/java/com/networknt/schema/OpenAPI30JsonSchemaTest.java b/src/test/java/com/networknt/schema/OpenAPI30JsonSchemaTest.java
new file mode 100644
index 0000000..7525b4e
--- /dev/null
+++ b/src/test/java/com/networknt/schema/OpenAPI30JsonSchemaTest.java
@@ -0,0 +1,122 @@
+package com.networknt.schema;
+
+import java.io.File;
+import java.io.InputStream;
+import java.net.URI;
+import java.util.ArrayList;
+import java.util.List;
+
+import com.fasterxml.jackson.databind.JsonNode;
+import com.fasterxml.jackson.databind.ObjectMapper;
+import com.fasterxml.jackson.databind.node.ArrayNode;
+import io.undertow.Undertow;
+import io.undertow.server.handlers.resource.FileResourceManager;
+import org.junit.AfterClass;
+import org.junit.BeforeClass;
+import org.junit.Test;
+
+import static io.undertow.Handlers.resource;
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertFalse;
+
+public class OpenAPI30JsonSchemaTest {
+    protected ObjectMapper mapper = new ObjectMapper();
+    protected JsonSchemaFactory validatorFactory = JsonSchemaFactory
+            .builder(JsonSchemaFactory.getInstance(SpecVersion.VersionFlag.V4)).objectMapper(mapper).build();
+    protected static Undertow server = null;
+
+    public OpenAPI30JsonSchemaTest() {
+    }
+
+    @BeforeClass
+    public static void setUp() {
+        if (server == null) {
+            server = Undertow.builder()
+                    .addHttpListener(1234, "localhost")
+                    .setHandler(resource(new FileResourceManager(
+                            new File("./src/test/resources/remotes"), 100)))
+                    .build();
+            server.start();
+        }
+    }
+
+    @AfterClass
+    public static void tearDown() throws Exception {
+        if (server != null) {
+            try {
+                Thread.sleep(100);
+            } catch (InterruptedException ignored) {
+
+            }
+            server.stop();
+        }
+    }
+
+    private void runTestFile(String testCaseFile) throws Exception {
+        final URI testCaseFileUri = URI.create("classpath:" + testCaseFile);
+        InputStream in = Thread.currentThread().getContextClassLoader()
+                .getResourceAsStream(testCaseFile);
+        ArrayNode testCases = mapper.readValue(in, ArrayNode.class);
+
+        for (int j = 0; j < testCases.size(); j++) {
+            try {
+                JsonNode testCase = testCases.get(j);
+                SchemaValidatorsConfig config = new SchemaValidatorsConfig();
+
+                ArrayNode testNodes = (ArrayNode) testCase.get("tests");
+                for (int i = 0; i < testNodes.size(); i++) {
+                    JsonNode test = testNodes.get(i);
+                    System.out.println("=== " + test.get("description"));
+                    JsonNode node = test.get("data");
+                    JsonNode typeLooseNode = test.get("isTypeLoose");
+                    // Configure the schemaValidator to set typeLoose's value based on the test file,
+                    // if test file do not contains typeLoose flag, use default value: true.
+                    config.setTypeLoose(typeLooseNode != null && typeLooseNode.asBoolean());
+                    config.setOpenAPI3StyleDiscriminators(true);
+                    JsonSchema schema = validatorFactory.getSchema(testCaseFileUri, testCase.get("schema"), config);
+
+                    List<ValidationMessage> errors = new ArrayList<ValidationMessage>(schema.validate(node));
+
+                    if (test.get("valid").asBoolean()) {
+                        if (!errors.isEmpty()) {
+                            System.out.println("---- test case failed ----");
+                            System.out.println("schema: " + schema.toString());
+                            System.out.println("data: " + test.get("data"));
+                            System.out.println("errors:");
+                            for (ValidationMessage error : errors) {
+                                System.out.println(error);
+                            }
+                        }
+                        assertEquals(0, errors.size());
+                    } else {
+                        if (errors.isEmpty()) {
+                            System.out.println("---- test case failed ----");
+                            System.out.println("schema: " + schema);
+                            System.out.println("data: " + test.get("data"));
+                        } else {
+                            JsonNode errorCount = test.get("errorCount");
+                            if (errorCount != null && errorCount.isInt() && errors.size() != errorCount.asInt()) {
+                                System.out.println("---- test case failed ----");
+                                System.out.println("schema: " + schema);
+                                System.out.println("data: " + test.get("data"));
+                                System.out.println("errors: " + errors);
+                                for (ValidationMessage error : errors) {
+                                    System.out.println(error);
+                                }
+                                assertEquals("expected error count", errorCount.asInt(), errors.size());
+                            }
+                        }
+                        assertFalse(errors.isEmpty());
+                    }
+                }
+            } catch (JsonSchemaException e) {
+                throw new IllegalStateException(String.format("Current schema should not be invalid: %s", testCaseFile), e);
+            }
+        }
+    }
+
+    @Test
+    public void testDiscriminatorMapping() throws Exception {
+        runTestFile("openapi3/discriminator.json");
+    }
+}
diff --git a/src/test/java/com/networknt/schema/OverrideValidatorTest.java b/src/test/java/com/networknt/schema/OverrideValidatorTest.java
new file mode 100644
index 0000000..d128040
--- /dev/null
+++ b/src/test/java/com/networknt/schema/OverrideValidatorTest.java
@@ -0,0 +1,69 @@
+/*
+ * Copyright (c) 2016 Network New Technologies Inc.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.networknt.schema;
+
+import com.fasterxml.jackson.core.JsonProcessingException;
+import com.fasterxml.jackson.databind.JsonNode;
+import com.fasterxml.jackson.databind.ObjectMapper;
+import org.junit.Test;
+
+import java.io.IOException;
+import java.text.MessageFormat;
+import java.util.ArrayList;
+import java.util.List;
+import java.util.Set;
+
+import static org.junit.Assert.assertEquals;
+
+public class OverrideValidatorTest {
+
+    @Test
+    public void overrideDefaultValidator() throws JsonProcessingException, IOException {
+        ObjectMapper objectMapper = new ObjectMapper();
+        final String URI = "https://github.com/networknt/json-schema-validator/tests/schemas/example01";
+        final String schema = "{\n" +
+        "  \"$schema\":\n" +
+        "    \"https://github.com/networknt/json-schema-validator/tests/schemas/example01\",\n" +
+        "  \"properties\": {\"mailaddress\": {\"type\": \"string\", \"format\": \"email\"}}\n" +
+        "}";
+        final JsonNode targetNode = objectMapper.readTree("{\"mailaddress\": \"a-zA-Z0-9.!#$%&'*+@a---a.a--a\"}");
+        // Use Default EmailValidator
+        final JsonMetaSchema validatorMetaSchema = JsonMetaSchema
+                .builder(URI, JsonMetaSchema.getV201909())
+                .build();
+
+        final JsonSchemaFactory validatorFactory = JsonSchemaFactory.builder(JsonSchemaFactory.getInstance(SpecVersion.VersionFlag.V201909)).addMetaSchema(validatorMetaSchema).build();
+        final JsonSchema validatorSchema = validatorFactory.getSchema(schema);
+
+        Set<ValidationMessage> messages = validatorSchema.validate(targetNode);
+        assertEquals(1, messages.size());
+
+        // Override EmailValidator
+        final JsonMetaSchema overrideValidatorMetaSchema = JsonMetaSchema
+                .builder(URI, JsonMetaSchema.getV201909())
+                .addFormat(new PatternFormat("email", "^[a-zA-Z0-9.!#$%&'*+\\/=?^_`{|}~-]+@[a-zA-Z0-9-]+(?:\\.[a-zA-Z0-9-]+)*$"))
+                .build();
+
+        final JsonSchemaFactory overrideValidatorFactory = JsonSchemaFactory.builder(JsonSchemaFactory.getInstance(SpecVersion.VersionFlag.V201909)).addMetaSchema(overrideValidatorMetaSchema).build();
+        final JsonSchema overrideValidatorSchema = overrideValidatorFactory.getSchema(schema);
+
+        messages = overrideValidatorSchema.validate(targetNode);
+        assertEquals(0, messages.size());
+
+
+    }
+}
diff --git a/src/test/resources/openapi3/discriminator.json b/src/test/resources/openapi3/discriminator.json
new file mode 100644
index 0000000..f833464
--- /dev/null
+++ b/src/test/resources/openapi3/discriminator.json
@@ -0,0 +1,283 @@
+[
+    {
+        "description": "discriminator with anyOf/allOf",
+        "schema": {
+            "anyOf": [
+                {
+                    "$ref": "#/components/schemas/Room"
+                },
+                {
+                    "$ref": "#/components/schemas/BedRoom"
+                },
+                {
+                    "$ref": "#/components/schemas/KidsBedRoom"
+                },
+                {
+                    "$ref": "#/components/schemas/Kitchen"
+                },
+                {
+                    "$ref": "#/components/schemas/GuestRoom"
+                }
+            ],
+            "components": {
+                "schemas": {
+                    "Room": {
+                        "type": "object",
+                        "properties": {
+                            "@type": {
+                                "type": "string"
+                            },
+                            "floor": {
+                                "type": "integer"
+                            }
+                        },
+                        "required": [
+                            "@type"
+                        ],
+                        "discriminator": {
+                            "propertyName": "@type",
+                            "mapping": {
+                                "bed": "#/components/schemas/BedRoom"
+                            }
+                        }
+                    },
+                    "BedRoom": {
+                        "type": "object",
+                        "allOf": [
+                            {
+                                "$ref": "#/components/schemas/Room"
+                            },
+                            {
+                                "type": "object",
+                                "properties": {
+                                    "numberOfBeds": {
+                                        "type": "integer"
+                                    }
+                                },
+                                "required": [
+                                    "numberOfBeds"
+                                ]
+                            }
+                        ],
+                        "discriminator": {
+                            "mapping": {
+                                "guest": "#/components/schemas/GuestRoom"
+                            }
+                        }
+                    },
+                    "KidsBedRoom": {
+                        "type": "object",
+                        "allOf": [
+                            {
+                                "$ref": "#/components/schemas/BedRoom"
+                            },
+                            {
+                                "type": "object",
+                                "properties": {
+                                    "isTidy": {
+                                        "type": "boolean"
+                                    }
+                                },
+                                "required": [
+                                    "isTidy"
+                                ]
+                            }
+                        ]
+                    },
+                    "GuestRoom": {
+                        "type": "object",
+                        "allOf": [
+                            {
+                                "$ref": "#/components/schemas/BedRoom"
+                            },
+                            {
+                                "type": "object",
+                                "properties": {
+                                    "guest": {
+                                        "type": "string"
+                                    }
+                                },
+                                "required": [
+                                    "guest"
+                                ]
+                            }
+                        ]
+                    },
+                    "Kitchen": {
+                        "type": "object",
+                        "allOf": [
+                            {
+                                "$ref": "#/components/schemas/Room"
+                            },
+                            {
+                                "type": "object",
+                                "properties": {
+                                    "hasMicrowaveOven": {
+                                        "type": "boolean"
+                                    },
+                                    "equipment": {
+                                        "type": "array",
+                                        "items": {
+                                            "anyOf": [
+                                                {
+                                                    "$ref": "#/components/schemas/Pot"
+                                                },
+                                                {
+                                                    "$ref": "#/components/schemas/Blender"
+                                                }
+                                            ]
+                                        }
+                                    }
+                                },
+                                "required": [
+                                    "hasMicrowaveOven"
+                                ]
+                            }
+                        ]
+                    },
+                    "KitchenEquipment": {
+                        "type": "object",
+                        "properties": {
+                            "@type": {
+                                "type": "string"
+                            }
+                        },
+                        "required": [
+                            "@type"
+                        ],
+                        "discriminator": {
+                            "propertyName": "@type"
+                        }
+                    },
+                    "Pot": {
+                        "allOf": [
+                            {
+                                "$ref": "#/components/schemas/KitchenEquipment"
+                            },
+                            {
+                                "type": "object",
+                                "properties": {
+                                    "capacity": {
+                                        "type": "integer"
+                                    }
+                                },
+                                "required": [
+                                    "capacity"
+                                ]
+                            }
+                        ]
+                    },
+                    "Blender": {
+                        "allOf": [
+                            {
+                                "$ref": "#/components/schemas/KitchenEquipment"
+                            },
+                            {
+                                "type": "object",
+                                "properties": {
+                                    "maxSpeed": {
+                                        "type": "integer"
+                                    }
+                                },
+                                "required": [
+                                    "maxSpeed"
+                                ]
+                            }
+                        ]
+                    }
+                }
+            }
+        },
+        "tests": [
+            {
+                "description": "mapping to base type",
+                "data": {
+                    "@type": "Room"
+                },
+                "valid": true
+            },
+            {
+                "description": "mapped to Bedroom",
+                "data": {
+                    "@type": "bed",
+                    "numberOfBeds": 2
+                },
+                "valid": true
+            },
+            {
+                "description": "mapped to Kitchen",
+                "data": {
+                    "@type": "Kitchen",
+                    "hasMicrowaveOven": true,
+                    "equipment": [
+                        {
+                            "@type": "Blender",
+                            "maxSpeed": 1500
+                        },
+                        {
+                            "@type": "Pot",
+                            "capacity": 5
+                        }
+                    ]
+                },
+                "valid": true
+            },
+            {
+                "description": "mapped to KidsBedRoom",
+                "data": {
+                    "@type": "KidsBedRoom",
+                    "numberOfBeds": 1,
+                    "isTidy": false
+                },
+                "valid": true
+            },
+            {
+                "description": "mapped to Bedroom with missing number of beds",
+                "data": {
+                    "@type": "bed"
+                },
+                "valid": false
+            },
+            {
+                "description": "mapped to KidsBedroom with missing number of beds",
+                "data": {
+                    "@type": "KidsBedRoom",
+                    "isTidy": true
+                },
+                "valid": false
+            },
+            {
+                "description": "mapped to KidsBedroom with missing tidiness",
+                "data": {
+                    "@type": "KidsBedRoom",
+                    "numberOfBeds": 1
+                },
+                "valid": false
+            },
+            {
+                "description": "mapped to GuestRoom with correct @type (mapping override on BedRoom)",
+                "data": {
+                    "@type": "guest",
+                    "numberOfBeds": 9,
+                    "guest": "Steve"
+                },
+                "valid": true
+            },
+            {
+                "description": "mapped to GuestRoom with incorrect @type (mapping override on BedRoom)",
+                "data": {
+                    "@type": "GuestRoom",
+                    "guest": "Steve"
+                },
+                "valid": false
+            },
+            {
+                "description": "mapped to invalid Room",
+                "data": {
+                    "@type": "Bathroom"
+                },
+                "valid": false
+            }
+        ]
+    }
+]