Repository: networknt/json-schema-validator (java)

Issue:
Behavior change of $ref resolution
In our system we support some well known URNs that our clients can use in their schema definitions, using the `$ref` keyword, for example:

```
{
    "properties": {
        "someField": {
            "$ref": "urn:my-company:my-custom-type"
        }
    }
}
```

We use a custom `URNFactory` and works great.

But we have observed the way of the URN resolution has changed as of release `1.0.49`. Previously, if a schema contained an unknown URN the call to `JsonSchemaFactory.getSchema(...)` failed with a `JsonSchemaException`. Now it does not fail in that moment but later, when calling to `JsonSchema.validate(...)`. I suspect this change could be due to https://github.com/networknt/json-schema-validator/pull/377.

Is is possible to recover the old behavior and resolve the `$ref` keywords when getting the schema? that way we would ensure the schema instantiated is valid, or at least would it be possible to provide a way to load the `$ref` schemas eagerly after getting the schema instance?

Pull request description:
Added JsonValidator#preloadJsonSchema()
Hi @stevehu,

to fix #406, here's my proposal. I've also done a few code clean-ups, especially when member `JsonSchema` instances were not final. Please refer to `Issue406Test` for the behaviour of the change.

Florian

Patch:
diff --git a/doc/quickstart.md b/doc/quickstart.md
index 7f5d7c2..1425149 100644
--- a/doc/quickstart.md
+++ b/doc/quickstart.md
@@ -69,6 +69,10 @@ class Sample extends BaseJsonSchemaValidatorTest {
         JsonNode schemaNode = getJsonNodeFromStringContent(
             "{\"$schema\": \"http://json-schema.org/draft-06/schema#\", \"properties\": { \"id\": {\"type\": \"number\"}}}");
         JsonSchema schema = getJsonSchemaFromJsonNodeAutomaticVersion(schemaNode);
+        
+        schema.initializeValidators(); // by default all schemas are loaded lazily. You can load them eagerly via
+                                       // initializeValidators() 
+        
         JsonNode node = getJsonNodeFromStringContent("{\"id\": \"2\"}");
         Set<ValidationMessage> errors = schema.validate(node);
         assertThat(errors.size(), is(1));
diff --git a/src/main/java/com/networknt/schema/AdditionalPropertiesValidator.java b/src/main/java/com/networknt/schema/AdditionalPropertiesValidator.java
index 505d50e..018e60f 100644
--- a/src/main/java/com/networknt/schema/AdditionalPropertiesValidator.java
+++ b/src/main/java/com/networknt/schema/AdditionalPropertiesValidator.java
@@ -106,4 +106,8 @@ public class AdditionalPropertiesValidator extends BaseJsonValidator implements
         return Collections.unmodifiableSet(errors);
     }
 
+    @Override
+    public void preloadJsonSchema() {
+        additionalPropertiesSchema.initializeValidators();
+    }
 }
diff --git a/src/main/java/com/networknt/schema/AllOfValidator.java b/src/main/java/com/networknt/schema/AllOfValidator.java
index c636f1d..0219e39 100644
--- a/src/main/java/com/networknt/schema/AllOfValidator.java
+++ b/src/main/java/com/networknt/schema/AllOfValidator.java
@@ -99,4 +99,8 @@ public class AllOfValidator extends BaseJsonValidator implements JsonValidator {
         return Collections.unmodifiableSet(validationMessages);
     }
 
+    @Override
+    public void preloadJsonSchema() {
+        preloadJsonSchemas(schemas);
+    }
 }
diff --git a/src/main/java/com/networknt/schema/AnyOfValidator.java b/src/main/java/com/networknt/schema/AnyOfValidator.java
index fdc918a..72f7a6b 100644
--- a/src/main/java/com/networknt/schema/AnyOfValidator.java
+++ b/src/main/java/com/networknt/schema/AnyOfValidator.java
@@ -97,4 +97,9 @@ public class AnyOfValidator extends BaseJsonValidator implements JsonValidator {
         }
         return Collections.unmodifiableSet(allErrors);
     }
+
+    @Override
+    public void preloadJsonSchema() {
+        preloadJsonSchemas(schemas);
+    }
 }
diff --git a/src/main/java/com/networknt/schema/BaseJsonValidator.java b/src/main/java/com/networknt/schema/BaseJsonValidator.java
index e03fc8f..81f4317 100644
--- a/src/main/java/com/networknt/schema/BaseJsonValidator.java
+++ b/src/main/java/com/networknt/schema/BaseJsonValidator.java
@@ -17,6 +17,7 @@
 package com.networknt.schema;
 
 import java.net.URI;
+import java.util.Collection;
 import java.util.Iterator;
 import java.util.LinkedHashSet;
 import java.util.Map;
@@ -164,6 +165,17 @@ public abstract class BaseJsonValidator implements JsonValidator {
         return validationMessages;
     }
 
+    @Override
+    public void preloadJsonSchema() {
+        // do nothing by default - to be overridden in subclasses
+    }
+
+    protected void preloadJsonSchemas(final Collection<JsonSchema> schemas) {
+        for (final JsonSchema schema: schemas) {
+            schema.initializeValidators();
+        }
+    }
+
     protected boolean isPartOfOneOfMultipleType() {
         return parentSchema.schemaPath.equals(ValidatorTypeCode.ONE_OF.getValue());
     }
@@ -285,5 +297,4 @@ public abstract class BaseJsonValidator implements JsonValidator {
         }
         return true;
     }
-
 }
diff --git a/src/main/java/com/networknt/schema/ContainsValidator.java b/src/main/java/com/networknt/schema/ContainsValidator.java
index 73dc02b..97babf0 100644
--- a/src/main/java/com/networknt/schema/ContainsValidator.java
+++ b/src/main/java/com/networknt/schema/ContainsValidator.java
@@ -26,12 +26,14 @@ import java.util.Set;
 public class ContainsValidator extends BaseJsonValidator implements JsonValidator {
     private static final Logger logger = LoggerFactory.getLogger(ContainsValidator.class);
 
-    private JsonSchema schema;
+    private final JsonSchema schema;
 
     public ContainsValidator(String schemaPath, JsonNode schemaNode, JsonSchema parentSchema, ValidationContext validationContext) {
         super(schemaPath, schemaNode, parentSchema, ValidatorTypeCode.CONTAINS, validationContext);
         if (schemaNode.isObject() || schemaNode.isBoolean()) {
             schema = new JsonSchema(validationContext, getValidatorType().getValue(), parentSchema.getCurrentUri(), schemaNode, parentSchema);
+        } else {
+            schema = null;
         }
 
         parseErrorCode(getValidatorType().getErrorCodeKey());
@@ -70,4 +72,10 @@ public class ContainsValidator extends BaseJsonValidator implements JsonValidato
         return Collections.singleton(buildValidationMessage(at, schema.getSchemaNode().toString()));
     }
 
+    @Override
+    public void preloadJsonSchema() {
+        if (null != schema) {
+            schema.initializeValidators();
+        }
+    }
 }
diff --git a/src/main/java/com/networknt/schema/DependenciesValidator.java b/src/main/java/com/networknt/schema/DependenciesValidator.java
index 5f76203..4d1e0c8 100644
--- a/src/main/java/com/networknt/schema/DependenciesValidator.java
+++ b/src/main/java/com/networknt/schema/DependenciesValidator.java
@@ -25,7 +25,7 @@ import java.util.*;
 public class DependenciesValidator extends BaseJsonValidator implements JsonValidator {
     private static final Logger logger = LoggerFactory.getLogger(DependenciesValidator.class);
     private final Map<String, List<String>> propertyDeps = new HashMap<String, List<String>>();
-    private Map<String, JsonSchema> schemaDeps = new HashMap<String, JsonSchema>();
+    private final Map<String, JsonSchema> schemaDeps = new HashMap<String, JsonSchema>();
 
     public DependenciesValidator(String schemaPath, JsonNode schemaNode, JsonSchema parentSchema, ValidationContext validationContext) {
 
@@ -75,4 +75,8 @@ public class DependenciesValidator extends BaseJsonValidator implements JsonVali
         return Collections.unmodifiableSet(errors);
     }
 
+    @Override
+    public void preloadJsonSchema() {
+        preloadJsonSchemas(schemaDeps.values());
+    }
 }
diff --git a/src/main/java/com/networknt/schema/IfValidator.java b/src/main/java/com/networknt/schema/IfValidator.java
index e9868b8..85269b3 100644
--- a/src/main/java/com/networknt/schema/IfValidator.java
+++ b/src/main/java/com/networknt/schema/IfValidator.java
@@ -27,23 +27,31 @@ public class IfValidator extends BaseJsonValidator implements JsonValidator {
 
     private static final ArrayList<String> KEYWORDS = new ArrayList<String>(Arrays.asList("if", "then", "else"));
 
-    private JsonSchema ifSchema;
-    private JsonSchema thenSchema;
-    private JsonSchema elseSchema;
+    private final JsonSchema ifSchema;
+    private final JsonSchema thenSchema;
+    private final JsonSchema elseSchema;
 
     public IfValidator(String schemaPath, JsonNode schemaNode, JsonSchema parentSchema, ValidationContext validationContext) {
         super(schemaPath, schemaNode, parentSchema, ValidatorTypeCode.IF_THEN_ELSE, validationContext);
 
+        JsonSchema foundIfSchema = null;
+        JsonSchema foundThenSchema = null;
+        JsonSchema foundElseSchema = null;
+
         for (final String keyword : KEYWORDS) {
             final JsonNode node = schemaNode.get(keyword);
             if (keyword.equals("if")) {
-                ifSchema = new JsonSchema(validationContext, getValidatorType().getValue(), parentSchema.getCurrentUri(), node, parentSchema);
+                foundIfSchema = new JsonSchema(validationContext, getValidatorType().getValue(), parentSchema.getCurrentUri(), node, parentSchema);
             } else if (keyword.equals("then") && node != null) {
-                thenSchema = new JsonSchema(validationContext, getValidatorType().getValue(), parentSchema.getCurrentUri(), node, parentSchema);
+                foundThenSchema = new JsonSchema(validationContext, getValidatorType().getValue(), parentSchema.getCurrentUri(), node, parentSchema);
             } else if (keyword.equals("else") && node != null) {
-                elseSchema = new JsonSchema(validationContext, getValidatorType().getValue(), parentSchema.getCurrentUri(), node, parentSchema);
+                foundElseSchema = new JsonSchema(validationContext, getValidatorType().getValue(), parentSchema.getCurrentUri(), node, parentSchema);
             }
         }
+
+        ifSchema = foundIfSchema;
+        thenSchema = foundThenSchema;
+        elseSchema = foundElseSchema;
     }
 
     public Set<ValidationMessage> validate(JsonNode node, JsonNode rootNode, String at) {
@@ -61,4 +69,16 @@ public class IfValidator extends BaseJsonValidator implements JsonValidator {
         return Collections.unmodifiableSet(errors);
     }
 
+    @Override
+    public void preloadJsonSchema() {
+        if(null != ifSchema) {
+            ifSchema.initializeValidators();
+        }
+        if(null != thenSchema) {
+            thenSchema.initializeValidators();
+        }
+        if(null != elseSchema) {
+            elseSchema.initializeValidators();
+        }
+    }
 }
diff --git a/src/main/java/com/networknt/schema/ItemsValidator.java b/src/main/java/com/networknt/schema/ItemsValidator.java
index 05253df..6f8ed5a 100644
--- a/src/main/java/com/networknt/schema/ItemsValidator.java
+++ b/src/main/java/com/networknt/schema/ItemsValidator.java
@@ -29,21 +29,24 @@ public class ItemsValidator extends BaseJsonValidator implements JsonValidator {
     private static final Logger logger = LoggerFactory.getLogger(ItemsValidator.class);
     private static final String PROPERTY_ADDITIONAL_ITEMS = "additionalItems";
 
-    private JsonSchema schema;
-    private List<JsonSchema> tupleSchema;
+    private final JsonSchema schema;
+    private final List<JsonSchema> tupleSchema;
     private boolean additionalItems = true;
-    private JsonSchema additionalSchema;
-    private WalkListenerRunner arrayItemWalkListenerRunner;
-    private ValidationContext validationContext;
+    private final JsonSchema additionalSchema;
+    private final WalkListenerRunner arrayItemWalkListenerRunner;
+    private final ValidationContext validationContext;
 
     public ItemsValidator(String schemaPath, JsonNode schemaNode, JsonSchema parentSchema,
             ValidationContext validationContext) {
         super(schemaPath, schemaNode, parentSchema, ValidatorTypeCode.ITEMS, validationContext);
+        tupleSchema = new ArrayList<JsonSchema>();
+        JsonSchema foundSchema = null;
+        JsonSchema foundAdditionalSchema = null;
+
         if (schemaNode.isObject() || schemaNode.isBoolean()) {
-            schema = new JsonSchema(validationContext, schemaPath, parentSchema.getCurrentUri(), schemaNode,
+            foundSchema = new JsonSchema(validationContext, schemaPath, parentSchema.getCurrentUri(), schemaNode,
                     parentSchema);
         } else {
-            tupleSchema = new ArrayList<JsonSchema>();
             for (JsonNode s : schemaNode) {
                 tupleSchema.add(
                         new JsonSchema(validationContext, schemaPath, parentSchema.getCurrentUri(), s, parentSchema));
@@ -54,7 +57,7 @@ public class ItemsValidator extends BaseJsonValidator implements JsonValidator {
                 if (addItemNode.isBoolean()) {
                     additionalItems = addItemNode.asBoolean();
                 } else if (addItemNode.isObject()) {
-                    additionalSchema = new JsonSchema(validationContext, parentSchema.getCurrentUri(), addItemNode);
+                    foundAdditionalSchema = new JsonSchema(validationContext, parentSchema.getCurrentUri(), addItemNode);
                 }
             }
         }
@@ -63,6 +66,9 @@ public class ItemsValidator extends BaseJsonValidator implements JsonValidator {
         this.validationContext = validationContext;
 
         parseErrorCode(getValidatorType().getErrorCodeKey());
+
+        this.schema = foundSchema;
+        this.additionalSchema = foundAdditionalSchema;
     }
 
     public Set<ValidationMessage> validate(JsonNode node, JsonNode rootNode, String at) {
@@ -168,4 +174,14 @@ public class ItemsValidator extends BaseJsonValidator implements JsonValidator {
         return schema;
     }
 
+    @Override
+    public void preloadJsonSchema() {
+        if (null != schema) {
+            schema.initializeValidators();
+        }
+        preloadJsonSchemas(tupleSchema);
+        if (null != additionalSchema) {
+            additionalSchema.initializeValidators();
+        }
+    }
 }
\ No newline at end of file
diff --git a/src/main/java/com/networknt/schema/JsonSchema.java b/src/main/java/com/networknt/schema/JsonSchema.java
index 4451658..b036f03 100644
--- a/src/main/java/com/networknt/schema/JsonSchema.java
+++ b/src/main/java/com/networknt/schema/JsonSchema.java
@@ -16,21 +16,26 @@
 
 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;
-
 import java.io.UnsupportedEncodingException;
 import java.net.URI;
 import java.net.URLDecoder;
-import java.util.*;
+import java.util.Collections;
+import java.util.HashMap;
+import java.util.Iterator;
+import java.util.LinkedHashSet;
+import java.util.Map;
 import java.util.Map.Entry;
+import java.util.Set;
 import java.util.regex.Matcher;
 import java.util.regex.Pattern;
 
+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;
+
 /**
  * This is the core of json constraint implementation. It parses json constraint
  * file and generates JsonValidators. The class is thread safe, once it is
@@ -253,7 +258,7 @@ public class JsonSchema extends BaseJsonValidator {
                         // used for validation before allOf validation has kicked in
                         discriminatorContext.registerDiscriminator(schemaPath, discriminator);
                         discriminatorToUse = discriminator;
-                    } else{
+                    } else {
                         discriminatorToUse = discriminatorFromContext;
                     }
 
@@ -408,4 +413,18 @@ public class JsonSchema extends BaseJsonValidator {
         return validators;
     }
 
+    /**
+     * Initializes the validators' {@link com.networknt.schema.JsonSchema} instances.
+     * For avoiding issues with concurrency, in 1.0.49 the {@link com.networknt.schema.JsonSchema} instances affiliated with
+     * validators were modified to no more preload the schema and lazy loading is used instead.
+     * <p>This comes with the issue that this way you cannot rely on validating important schema features, in particular
+     * <code>$ref</code> resolution at instantiation from {@link com.networknt.schema.JsonSchemaFactory}.</p>
+     * <p>By calling <code>initializeValidators</code> you can enforce preloading of the {@link com.networknt.schema.JsonSchema}
+     * instances of the validators.</p>
+     */
+    public void initializeValidators() {
+        for (final JsonValidator validator : getValidators().values()) {
+            validator.preloadJsonSchema();
+        }
+    }
 }
diff --git a/src/main/java/com/networknt/schema/JsonValidator.java b/src/main/java/com/networknt/schema/JsonValidator.java
index 8e9af4c..32e59a8 100644
--- a/src/main/java/com/networknt/schema/JsonValidator.java
+++ b/src/main/java/com/networknt/schema/JsonValidator.java
@@ -16,17 +16,16 @@
 
 package com.networknt.schema;
 
+import java.util.Set;
+
 import com.fasterxml.jackson.databind.JsonNode;
 import com.networknt.schema.walk.JsonSchemaWalker;
 
-import java.util.Set;
-
 /**
  * Standard json validator interface, implemented by all validators and JsonSchema.
  */
 public interface JsonValidator extends JsonSchemaWalker {
-    public static final String AT_ROOT = "$";
-
+    String AT_ROOT = "$";
 
     /**
      * Validate the given root JsonNode, starting at the root of the data path.
@@ -48,6 +47,14 @@ public interface JsonValidator extends JsonSchemaWalker {
      * list if there is no error.
      */
     Set<ValidationMessage> validate(JsonNode node, JsonNode rootNode, String at);
-    
 
+    /**
+     * In case the {@link com.networknt.schema.JsonValidator} has a related {@link com.networknt.schema.JsonSchema} or several
+     * ones, calling preloadJsonSchema will actually load the schema document(s) eagerly.
+     *
+     * @throws JsonSchemaException (a {@link java.lang.RuntimeException}) in case the {@link com.networknt.schema.JsonSchema} or nested schemas
+     * are invalid (like <code>$ref</code> not resolving)
+     * @since 1.0.54
+     */
+    void preloadJsonSchema() throws JsonSchemaException;
 }
diff --git a/src/main/java/com/networknt/schema/NonValidationKeyword.java b/src/main/java/com/networknt/schema/NonValidationKeyword.java
index 80271ac..cdb7b7d 100644
--- a/src/main/java/com/networknt/schema/NonValidationKeyword.java
+++ b/src/main/java/com/networknt/schema/NonValidationKeyword.java
@@ -35,6 +35,11 @@ public class NonValidationKeyword extends AbstractKeyword {
         public Set<ValidationMessage> validate(JsonNode node, JsonNode rootNode, String at) {
             return Collections.emptySet();
         }
+
+        @Override
+        public void preloadJsonSchema() {
+            // not used and the Validator is not extending from BaseJsonValidator
+        }
     }
 
     public NonValidationKeyword(String keyword) {
diff --git a/src/main/java/com/networknt/schema/NotValidator.java b/src/main/java/com/networknt/schema/NotValidator.java
index 727ce23..9de8ed3 100644
--- a/src/main/java/com/networknt/schema/NotValidator.java
+++ b/src/main/java/com/networknt/schema/NotValidator.java
@@ -27,7 +27,7 @@ import java.util.Set;
 public class NotValidator extends BaseJsonValidator implements JsonValidator {
     private static final Logger logger = LoggerFactory.getLogger(RequiredValidator.class);
 
-    private JsonSchema schema;
+    private final JsonSchema schema;
 
     public NotValidator(String schemaPath, JsonNode schemaNode, JsonSchema parentSchema, ValidationContext validationContext) {
         super(schemaPath, schemaNode, parentSchema, ValidatorTypeCode.NOT, validationContext);
@@ -51,4 +51,10 @@ public class NotValidator extends BaseJsonValidator implements JsonValidator {
     	return schema.walk(node, rootNode, at, shouldValidateSchema);
     }
 
+    @Override
+    public void preloadJsonSchema() {
+        if (null != schema) {
+            schema.initializeValidators();
+        }
+    }
 }
diff --git a/src/main/java/com/networknt/schema/OneOfValidator.java b/src/main/java/com/networknt/schema/OneOfValidator.java
index 91af23b..137e811 100644
--- a/src/main/java/com/networknt/schema/OneOfValidator.java
+++ b/src/main/java/com/networknt/schema/OneOfValidator.java
@@ -23,9 +23,9 @@ import org.slf4j.LoggerFactory;
 import java.util.*;
 
 public class OneOfValidator extends BaseJsonValidator implements JsonValidator {
-    private static final Logger logger = LoggerFactory.getLogger(RequiredValidator.class);
+    private static final Logger logger = LoggerFactory.getLogger(OneOfValidator.class);
 
-    private List<ShortcutValidator> schemas = new ArrayList<ShortcutValidator>();
+    private final List<ShortcutValidator> schemas = new ArrayList<ShortcutValidator>();
 
     private static class ShortcutValidator {
         private final JsonSchema schema;
@@ -236,11 +236,14 @@ public class OneOfValidator extends BaseJsonValidator implements JsonValidator {
             msg = msg.concat(schemaValue);
         }
 
-        ValidationMessage message = ValidationMessage.of(getValidatorType().getValue(),ValidatorTypeCode.ONE_OF ,
-                at, String.format("but more than one schemas {%s} are valid ",msg));
-
-        return message;
+        return ValidationMessage.of(getValidatorType().getValue(), ValidatorTypeCode.ONE_OF ,
+                                    at, String.format("but more than one schemas {%s} are valid ",msg));
     }
 
- 
+    @Override
+    public void preloadJsonSchema() {
+        for (final ShortcutValidator scValidator: schemas) {
+            scValidator.getSchema().initializeValidators();
+        }
+    }
 }
diff --git a/src/main/java/com/networknt/schema/PatternPropertiesValidator.java b/src/main/java/com/networknt/schema/PatternPropertiesValidator.java
index d36a4fd..3d1f67a 100644
--- a/src/main/java/com/networknt/schema/PatternPropertiesValidator.java
+++ b/src/main/java/com/networknt/schema/PatternPropertiesValidator.java
@@ -27,7 +27,7 @@ import java.util.regex.Pattern;
 public class PatternPropertiesValidator extends BaseJsonValidator implements JsonValidator {
     public static final String PROPERTY = "patternProperties";
     private static final Logger logger = LoggerFactory.getLogger(PatternPropertiesValidator.class);
-    private Map<Pattern, JsonSchema> schemas = new IdentityHashMap<Pattern, JsonSchema>();
+    private final Map<Pattern, JsonSchema> schemas = new IdentityHashMap<Pattern, JsonSchema>();
 
     public PatternPropertiesValidator(String schemaPath, JsonNode schemaNode, JsonSchema parentSchema,
                                       ValidationContext validationContext) {
@@ -65,4 +65,8 @@ public class PatternPropertiesValidator extends BaseJsonValidator implements Jso
         return Collections.unmodifiableSet(errors);
     }
 
+    @Override
+    public void preloadJsonSchema() {
+        preloadJsonSchemas(schemas.values());
+    }
 }
diff --git a/src/main/java/com/networknt/schema/PatternValidator.java b/src/main/java/com/networknt/schema/PatternValidator.java
index 1a15697..36a7407 100644
--- a/src/main/java/com/networknt/schema/PatternValidator.java
+++ b/src/main/java/com/networknt/schema/PatternValidator.java
@@ -162,4 +162,8 @@ public class PatternValidator implements JsonValidator {
         }
     }
 
+    @Override
+    public void preloadJsonSchema() {
+        // not used and the validator does not inherit from BaseJsonValidator
+    }
 }
diff --git a/src/main/java/com/networknt/schema/PropertiesValidator.java b/src/main/java/com/networknt/schema/PropertiesValidator.java
index 67ef30e..16b6adc 100644
--- a/src/main/java/com/networknt/schema/PropertiesValidator.java
+++ b/src/main/java/com/networknt/schema/PropertiesValidator.java
@@ -27,20 +27,18 @@ import java.util.*;
 public class PropertiesValidator extends BaseJsonValidator implements JsonValidator {
     public static final String PROPERTY = "properties";
     private static final Logger logger = LoggerFactory.getLogger(PropertiesValidator.class);
-    private Map<String, JsonSchema> schemas;
-    private WalkListenerRunner propertyWalkListenerRunner;
-    private ValidationContext validationContext;
+    private final Map<String, JsonSchema> schemas = new HashMap<String, JsonSchema>();
+    private final WalkListenerRunner propertyWalkListenerRunner;
+    private final ValidationContext validationContext;
 
     public PropertiesValidator(String schemaPath, JsonNode schemaNode, JsonSchema parentSchema, ValidationContext validationContext) {
         super(schemaPath, schemaNode, parentSchema, ValidatorTypeCode.PROPERTIES, validationContext);
         this.validationContext = validationContext;
-        schemas = new HashMap<String, JsonSchema>();
         for (Iterator<String> it = schemaNode.fieldNames(); it.hasNext(); ) {
             String pname = it.next();
             schemas.put(pname, new JsonSchema(validationContext, schemaPath + "/" + pname, parentSchema.getCurrentUri(), schemaNode.get(pname), parentSchema));
         }
-		propertyWalkListenerRunner = new DefaultPropertyWalkListenerRunner(
-                config.getPropertyWalkListeners());    
+		propertyWalkListenerRunner = new DefaultPropertyWalkListenerRunner(config.getPropertyWalkListeners());
     }
 
     public Set<ValidationMessage> validate(JsonNode node, JsonNode rootNode, String at) {
@@ -136,4 +134,9 @@ public class PropertiesValidator extends BaseJsonValidator implements JsonValida
         return schemas;
     }
 
+
+    @Override
+    public void preloadJsonSchema() {
+        preloadJsonSchemas(schemas.values());
+    }
 }
diff --git a/src/main/java/com/networknt/schema/PropertyNamesValidator.java b/src/main/java/com/networknt/schema/PropertyNamesValidator.java
index a001ed6..7a6bf84 100644
--- a/src/main/java/com/networknt/schema/PropertyNamesValidator.java
+++ b/src/main/java/com/networknt/schema/PropertyNamesValidator.java
@@ -53,4 +53,10 @@ public class PropertyNamesValidator extends BaseJsonValidator implements JsonVal
         }
         return Collections.unmodifiableSet(errors);
     }
+
+
+    @Override
+    public void preloadJsonSchema() {
+        innerSchema.initializeValidators();
+    }
 }
diff --git a/src/main/java/com/networknt/schema/RefValidator.java b/src/main/java/com/networknt/schema/RefValidator.java
index 167d379..b08b986 100644
--- a/src/main/java/com/networknt/schema/RefValidator.java
+++ b/src/main/java/com/networknt/schema/RefValidator.java
@@ -33,10 +33,6 @@ public class RefValidator extends BaseJsonValidator implements JsonValidator {
     private static final Logger logger = LoggerFactory.getLogger(RefValidator.class);
 
     protected JsonSchemaRef schema;
-    
-    private JsonSchema parentSchema;
-    
-    private ValidationContext validationContext;
 
     private static final String REF_CURRENT = "#";
 
@@ -44,10 +40,10 @@ public class RefValidator extends BaseJsonValidator implements JsonValidator {
         super(schemaPath, schemaNode, parentSchema, ValidatorTypeCode.REF, validationContext);
         String refValue = schemaNode.asText();
         schema = getRefSchema(parentSchema, validationContext, refValue);
-        this.parentSchema = parentSchema;
-        this.validationContext = validationContext;
         if (schema == null) {
-            throw new JsonSchemaException(ValidationMessage.of(ValidatorTypeCode.REF.getValue(), CustomErrorMessageType.of("internal.unresolvedRef", new MessageFormat("{0}: Reference {1} cannot be resolved")), schemaPath, refValue));
+            throw new JsonSchemaException(
+                    ValidationMessage.of(ValidatorTypeCode.REF.getValue(),
+                                                               CustomErrorMessageType.of("internal.unresolvedRef", new MessageFormat("{0}: Reference {1} cannot be resolved")), schemaPath, refValue));
         }
     }
 
@@ -139,7 +135,7 @@ public class RefValidator extends BaseJsonValidator implements JsonValidator {
             return Collections.emptySet();
         }
     }
-    
+
 	@Override
 	public Set<ValidationMessage> walk(JsonNode node, JsonNode rootNode, String at, boolean shouldValidateSchema) {
 		if (schema != null) {
@@ -152,4 +148,9 @@ public class RefValidator extends BaseJsonValidator implements JsonValidator {
 		return schema;
 	}
 
+
+    @Override
+    public void preloadJsonSchema() {
+        schema.getSchema().initializeValidators();
+    }
 }
diff --git a/src/main/java/com/networknt/schema/UnionTypeValidator.java b/src/main/java/com/networknt/schema/UnionTypeValidator.java
index 74934bc..547eafe 100644
--- a/src/main/java/com/networknt/schema/UnionTypeValidator.java
+++ b/src/main/java/com/networknt/schema/UnionTypeValidator.java
@@ -28,12 +28,11 @@ import java.util.Set;
 public class UnionTypeValidator extends BaseJsonValidator implements JsonValidator {
     private static final Logger logger = LoggerFactory.getLogger(UnionTypeValidator.class);
 
-    private List<JsonValidator> schemas;
-    private String error;
+    private final List<JsonValidator> schemas = new ArrayList<JsonValidator>();
+    private final String error;
 
     public UnionTypeValidator(String schemaPath, JsonNode schemaNode, JsonSchema parentSchema, ValidationContext validationContext) {
         super(schemaPath, schemaNode, parentSchema, ValidatorTypeCode.UNION_TYPE, validationContext);
-        schemas = new ArrayList<JsonValidator>();
 
         StringBuilder errorBuilder = new StringBuilder();
 
@@ -84,4 +83,10 @@ public class UnionTypeValidator extends BaseJsonValidator implements JsonValidat
         return Collections.emptySet();
     }
 
+    @Override
+    public void preloadJsonSchema() {
+        for (final JsonValidator validator : schemas) {
+            validator.preloadJsonSchema();
+        }
+    }
 }


Test patch:
diff --git a/src/test/java/com/networknt/schema/CollectorContextTest.java b/src/test/java/com/networknt/schema/CollectorContextTest.java
index 2ba81ae..ec52b46 100644
--- a/src/test/java/com/networknt/schema/CollectorContextTest.java
+++ b/src/test/java/com/networknt/schema/CollectorContextTest.java
@@ -297,6 +297,10 @@ public class CollectorContextTest {
             return null;
         }
 
+        @Override
+        public void preloadJsonSchema() {
+            // not used in testing
+        }
     }
 
     private class CustomCollector extends AbstractCollector<List<String>> {
@@ -374,6 +378,11 @@ public class CollectorContextTest {
             // Ignore this method for testing.
             return null;
         }
+
+        @Override
+        public void preloadJsonSchema() {
+            // not used in testing
+        }
     }
 
     private ValidationResult validate(String jsonData) throws JsonMappingException, JsonProcessingException, Exception {
diff --git a/src/test/java/com/networknt/schema/CustomMetaSchemaTest.java b/src/test/java/com/networknt/schema/CustomMetaSchemaTest.java
index e2ee5e4..fc18913 100644
--- a/src/test/java/com/networknt/schema/CustomMetaSchemaTest.java
+++ b/src/test/java/com/networknt/schema/CustomMetaSchemaTest.java
@@ -66,6 +66,11 @@ public class CustomMetaSchemaTest {
                 String valueName = enumNames.get(idx);
                 return fail(CustomErrorMessageType.of("tests.example.enumNames", new MessageFormat("{0}: enumName is {1}")), at, valueName);
             }
+
+            @Override
+            public void preloadJsonSchema() {
+                // not used in testing
+            }
         }
 
 
diff --git a/src/test/java/com/networknt/schema/Issue406Test.java b/src/test/java/com/networknt/schema/Issue406Test.java
new file mode 100644
index 0000000..b677bf4
--- /dev/null
+++ b/src/test/java/com/networknt/schema/Issue406Test.java
@@ -0,0 +1,34 @@
+/*
+ * Copyright (c) 2020, 2021 Oracle and/or its affiliates.
+ */
+package com.networknt.schema;
+
+import org.junit.Assert;
+import org.junit.Test;
+import org.junit.function.ThrowingRunnable;
+
+public class Issue406Test {
+    protected static final String INVALID_$REF_SCHEMA = "{\"$ref\":\"urn:unresolved\"}";
+
+    @Test
+    public void testPreloadingNotHappening() {
+        final JsonSchemaFactory factory = JsonSchemaFactory.getInstance(SpecVersion.VersionFlag.V7);
+        final JsonSchema schema = factory.getSchema(INVALID_$REF_SCHEMA);
+        // not breaking - pass
+        Assert.assertNotNull(schema);
+    }
+
+    @Test
+    public void testPreloadingHappening() {
+        final JsonSchemaFactory factory = JsonSchemaFactory.getInstance(SpecVersion.VersionFlag.V7);
+        final JsonSchema schema = factory.getSchema(INVALID_$REF_SCHEMA);
+        Assert.assertThrows("#/$ref: Reference urn:unresolved cannot be resolved",
+                            JsonSchemaException.class,
+                            new ThrowingRunnable() {
+                                @Override
+                                public void run() {
+                                    schema.initializeValidators();
+                                }
+                            });
+    }
+}
diff --git a/src/test/java/com/networknt/schema/JsonWalkTest.java b/src/test/java/com/networknt/schema/JsonWalkTest.java
index ae94223..8137bee 100644
--- a/src/test/java/com/networknt/schema/JsonWalkTest.java
+++ b/src/test/java/com/networknt/schema/JsonWalkTest.java
@@ -120,6 +120,11 @@ public class JsonWalkTest {
                                                boolean shouldValidateSchema) {
                 return new LinkedHashSet<ValidationMessage>();
             }
+
+            @Override
+            public void preloadJsonSchema() {
+                // not used in testing
+            }
         }
     }