Repository: networknt/json-schema-validator
Pull request #141 (2019-05-03 23:49:50)

Improved Ref Validator
This pull request's aim is to eliminate some of the limitations with the RefValidator class. 

Here are list of some of the limitations:
- Relative $ref required an absolute id to exist.
- The RefValidator#isRelativePath() check only took into account the 'http' protocol. (Relevant to #98 #26 #37).
- A $ref was only able to build off of a single id.
- A relative domain $ref only worked for absolute urls that followed a <protocol>:<host> pattern. (Relevant to #86).
- An id previously was required to have at least one "/" character. (Relevant to #55).

In order to solve these issues, I made it so that JsonSchemas are able to optionally store a 'currentUrl'. This currentUrl is effected by 'id' keywords, urls that are provided to the JsonSchemaFactory. The JsonSchemaFactory is operated by the user of this library as well as RefValidators that point to a remote schema. 

All of this then allows the RefValidator to easily deal with a relative $ref. We deal with the relative $ref by simply using the constructor of the URL class. The URL constructor is able to automatically handle domain and subdirectory relative references. The URL constructor just requires a parent URL (which is provided by the JsonSchema's currentUrl) and some relative/absolute string path. If the parent URL is null, then the constructor requires the string path to be an absolute url. If the parent URL is nonnull, then the string path can be either an absolute url or a relative path (absolute urls in the string path are able to override the parent URL). 

Looking at my changes though, the URL constructor isn't actually being used directly. I chose to use the existing URLFactory class in this project and added to it so that it can combine a parent URL with some string path. Doing this allows the RefValidator to support the 'classpath:' and 'resource:' protocols that were already working with this library.

Another thing to note is that I updated the interface for the JsonSchemaFactory. I essentially just duplicated the existing factory methods so that users can optionally provide the URL of the schema that they're dealing with. The JsonSchemaFactory is still backwards compatible and doesn't require the URL of the schema, but it can be supplied so that '$ref' and 'id' values are able to be relative to the schema's URL.

So in short, I basically updated the library so that it can rely on the URL constructor for the complicated logic of combining 'id' and '$ref' values with the url of a schema. The URL constructor basically handles all of what this validator needs to do (we even are able to add support for custom protocols) and that bit of code is builtin to Java, so I think this creates a robust and flexible solution.

Note1: I saw that unit tests (in the JsonSchemaTest file) with invalid schemas were succeeding, so I ended up making it so that those tests fail. There was one existing unit test that was silently failing and it has probably been silently failing in this project for a while. It had a reference to the src/test/resources/tests/folder/folderInteger.json file (which didn't exist). I figure that if a test schema isn't able to be loaded, then the unit test or the project probably needs to be fixed.

Note2: I fixed a unit test relating to issue #12 (i.e. SelfRefTest.java). It was loading a schema as a JsonNode (which was succeeding), but the test seemed like it should have loaded it as a JsonSchema. Since I fixed this unit test and it now correctly reproduces the problem in issue #12, the unit test now fails (as expected with a StackOverflow). I just added an @Ignore annotation until someone provides a fix.


Patch:
diff --git a/src/main/java/com/networknt/schema/AdditionalPropertiesValidator.java b/src/main/java/com/networknt/schema/AdditionalPropertiesValidator.java
index 67fb6cc..2384f0a 100644
--- a/src/main/java/com/networknt/schema/AdditionalPropertiesValidator.java
+++ b/src/main/java/com/networknt/schema/AdditionalPropertiesValidator.java
@@ -47,7 +47,7 @@ public class AdditionalPropertiesValidator extends BaseJsonValidator implements
             additionalPropertiesSchema = null;
         } else if (schemaNode.isObject()) {
             allowAdditionalProperties = true;
-            additionalPropertiesSchema = new JsonSchema(validationContext, getValidatorType().getValue(), schemaNode, parentSchema);
+            additionalPropertiesSchema = new JsonSchema(validationContext, getValidatorType().getValue(), parentSchema.getCurrentUrl(), schemaNode, parentSchema);
         } else {
             allowAdditionalProperties = false;
             additionalPropertiesSchema = null;
diff --git a/src/main/java/com/networknt/schema/AllOfValidator.java b/src/main/java/com/networknt/schema/AllOfValidator.java
index b95d312..52c2382 100644
--- a/src/main/java/com/networknt/schema/AllOfValidator.java
+++ b/src/main/java/com/networknt/schema/AllOfValidator.java
@@ -35,7 +35,7 @@ public class AllOfValidator extends BaseJsonValidator implements JsonValidator {
         super(schemaPath, schemaNode, parentSchema, ValidatorTypeCode.ALL_OF, validationContext);
         int size = schemaNode.size();
         for (int i = 0; i < size; i++) {
-            schemas.add(new JsonSchema(validationContext, getValidatorType().getValue(), schemaNode.get(i), parentSchema));
+            schemas.add(new JsonSchema(validationContext, getValidatorType().getValue(), parentSchema.getCurrentUrl(), schemaNode.get(i), parentSchema));
         }
     }
 
diff --git a/src/main/java/com/networknt/schema/AnyOfValidator.java b/src/main/java/com/networknt/schema/AnyOfValidator.java
index 7cda034..1d1a954 100644
--- a/src/main/java/com/networknt/schema/AnyOfValidator.java
+++ b/src/main/java/com/networknt/schema/AnyOfValidator.java
@@ -36,7 +36,7 @@ public class AnyOfValidator extends BaseJsonValidator implements JsonValidator {
         super(schemaPath, schemaNode, parentSchema, ValidatorTypeCode.ANY_OF, validationContext);
         int size = schemaNode.size();
         for (int i = 0; i < size; i++) {
-            schemas.add(new JsonSchema(validationContext, getValidatorType().getValue(), schemaNode.get(i), parentSchema));
+            schemas.add(new JsonSchema(validationContext, getValidatorType().getValue(), parentSchema.getCurrentUrl(), schemaNode.get(i), parentSchema));
         }
     }
 
diff --git a/src/main/java/com/networknt/schema/DependenciesValidator.java b/src/main/java/com/networknt/schema/DependenciesValidator.java
index ccb85d5..1eb0168 100644
--- a/src/main/java/com/networknt/schema/DependenciesValidator.java
+++ b/src/main/java/com/networknt/schema/DependenciesValidator.java
@@ -44,7 +44,7 @@ public class DependenciesValidator extends BaseJsonValidator implements JsonVali
                     depsProps.add(pvalue.get(i).asText());
                 }
             } else if (pvalue.isObject()) {
-                schemaDeps.put(pname, new JsonSchema(validationContext, pname, pvalue, parentSchema));
+                schemaDeps.put(pname, new JsonSchema(validationContext, pname, parentSchema.getCurrentUrl(), pvalue, parentSchema));
             }
         }
 
diff --git a/src/main/java/com/networknt/schema/ItemsValidator.java b/src/main/java/com/networknt/schema/ItemsValidator.java
index 7c3b148..b164c58 100644
--- a/src/main/java/com/networknt/schema/ItemsValidator.java
+++ b/src/main/java/com/networknt/schema/ItemsValidator.java
@@ -38,11 +38,11 @@ public class ItemsValidator extends BaseJsonValidator implements JsonValidator {
     public ItemsValidator(String schemaPath, JsonNode schemaNode, JsonSchema parentSchema, ValidationContext validationContext) {
         super(schemaPath, schemaNode, parentSchema, ValidatorTypeCode.ITEMS, validationContext);
         if (schemaNode.isObject()) {
-            schema = new JsonSchema(validationContext, getValidatorType().getValue(), schemaNode, parentSchema);
+            schema = new JsonSchema(validationContext, getValidatorType().getValue(), parentSchema.getCurrentUrl(), schemaNode, parentSchema);
         } else {
             tupleSchema = new ArrayList<JsonSchema>();
             for (JsonNode s : schemaNode) {
-                tupleSchema.add(new JsonSchema(validationContext, getValidatorType().getValue(), s, parentSchema));
+                tupleSchema.add(new JsonSchema(validationContext, getValidatorType().getValue(), parentSchema.getCurrentUrl(), s, parentSchema));
             }
 
             JsonNode addItemNode = getParentSchema().getSchemaNode().get(PROPERTY_ADDITIONAL_ITEMS);
@@ -50,7 +50,7 @@ public class ItemsValidator extends BaseJsonValidator implements JsonValidator {
                 if (addItemNode.isBoolean()) {
                     additionalItems = addItemNode.asBoolean();
                 } else if (addItemNode.isObject()) {
-                    additionalSchema = new JsonSchema(validationContext, addItemNode);
+                    additionalSchema = new JsonSchema(validationContext, parentSchema.getCurrentUrl(), addItemNode);
                 }
             }
         }
diff --git a/src/main/java/com/networknt/schema/JsonSchema.java b/src/main/java/com/networknt/schema/JsonSchema.java
index 63c409a..cafd6b7 100644
--- a/src/main/java/com/networknt/schema/JsonSchema.java
+++ b/src/main/java/com/networknt/schema/JsonSchema.java
@@ -16,14 +16,22 @@
 
 package com.networknt.schema;
 
-import com.fasterxml.jackson.databind.JsonNode;
-
 import java.io.UnsupportedEncodingException;
+import java.net.MalformedURLException;
+import java.net.URL;
 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.Set;
 import java.util.regex.Matcher;
 import java.util.regex.Pattern;
 
+import com.fasterxml.jackson.databind.JsonNode;
+import com.networknt.schema.url.URLFactory;
+
 /**
  * This is the core of json constraint implementation. It parses json constraint
  * file and generates JsonValidators. The class is thread safe, once it is
@@ -34,27 +42,60 @@ public class JsonSchema extends BaseJsonValidator {
     protected final Map<String, JsonValidator> validators;
     private final ValidationContext validationContext;
     
+    /**
+     * This is the current url of this schema. This url could refer to the url of this schema's file
+     * or it could potentially be a url that has been altered by an id. An 'id' is able to completely overwrite
+     * the current url or add onto it. This is necessary so that '$ref's are able to be relative to a
+     * combination of the current schema file's url and 'id' urls visible to this schema.
+     * 
+     * This can be null. If it is null, then the creation of relative urls will fail. However, an absolute
+     * 'id' would still be able to specify an absolute url.
+     */
+    private final URL currentUrl;
+    
     private JsonValidator requiredValidator = null;
 
-    public JsonSchema(ValidationContext validationContext,  JsonNode schemaNode) {
-        this(validationContext,  "#", schemaNode, null);
+    public JsonSchema(ValidationContext validationContext, URL baseUrl, JsonNode schemaNode) {
+        this(validationContext, "#", baseUrl, schemaNode, null);
     }
 
-    public JsonSchema(ValidationContext validationContext,  String schemaPath, JsonNode schemaNode,
+    public JsonSchema(ValidationContext validationContext, String schemaPath, URL currentUrl, JsonNode schemaNode,
                JsonSchema parent) {
-        this(validationContext,  schemaPath, schemaNode, parent, false);
+        this(validationContext,  schemaPath, currentUrl, schemaNode, parent, false);
     }
 
-    public JsonSchema(ValidationContext validationContext,  String schemaPath, JsonNode schemaNode,
+    public JsonSchema(ValidationContext validationContext, URL baseUrl, JsonNode schemaNode, boolean suppressSubSchemaRetrieval) {
+        this(validationContext, "#", baseUrl, schemaNode, null, suppressSubSchemaRetrieval);
+    }
+
+    private JsonSchema(ValidationContext validationContext,  String schemaPath, URL currentUrl, JsonNode schemaNode,
                JsonSchema parent, boolean suppressSubSchemaRetrieval) {
         super(schemaPath, schemaNode, parent, null, suppressSubSchemaRetrieval);
         this.validationContext = validationContext;
         this.config = validationContext.getConfig();
+        this.currentUrl = this.combineCurrentUrlWithIds(currentUrl, schemaNode);
         this.validators = Collections.unmodifiableMap(this.read(schemaNode));
     }
-
-    public JsonSchema(ValidationContext validationContext,  JsonNode schemaNode, boolean suppressSubSchemaRetrieval) {
-        this(validationContext, "#", schemaNode, null, suppressSubSchemaRetrieval);
+    
+    private URL combineCurrentUrlWithIds(URL currentUrl, JsonNode schemaNode) {
+      final JsonNode idNode = schemaNode.get("id");
+      if (idNode == null) {
+        return currentUrl;
+      } else {
+        try
+        {
+          return URLFactory.toURL(currentUrl, idNode.asText());
+        }
+        catch (MalformedURLException e)
+        {
+          throw new IllegalArgumentException(String.format("Invalid 'id' in schema: %s", schemaNode), e);
+        }
+      }
+    }
+    
+    public URL getCurrentUrl()
+    {
+      return this.currentUrl;
     }
 
     /**
diff --git a/src/main/java/com/networknt/schema/JsonSchemaFactory.java b/src/main/java/com/networknt/schema/JsonSchemaFactory.java
index dcda5bd..85a1963 100644
--- a/src/main/java/com/networknt/schema/JsonSchemaFactory.java
+++ b/src/main/java/com/networknt/schema/JsonSchemaFactory.java
@@ -94,7 +94,12 @@ public class JsonSchemaFactory {
     private final Map<String, JsonMetaSchema> jsonMetaSchemas;
     private final Map<String, String> urlMap;
 
-    private JsonSchemaFactory(ObjectMapper mapper, URLFetcher urlFetcher, String defaultMetaSchemaURI, Map<String, JsonMetaSchema> jsonMetaSchemas, Map<String, String> urlMap) {
+    private JsonSchemaFactory(
+            ObjectMapper mapper, 
+            URLFetcher urlFetcher, 
+            String defaultMetaSchemaURI, 
+            Map<String, JsonMetaSchema> jsonMetaSchemas, 
+            Map<String, String> urlMap) {
         if (mapper == null) {
             throw new IllegalArgumentException("ObjectMapper must not be null");
         }
@@ -152,10 +157,10 @@ public class JsonSchemaFactory {
                 .addUrlMappings(blueprint.urlMap);
     }
     
-    private JsonSchema newJsonSchema(JsonNode schemaNode, SchemaValidatorsConfig config) {
+    private JsonSchema newJsonSchema(URL schemaUrl, JsonNode schemaNode, SchemaValidatorsConfig config) {
         final ValidationContext validationContext = createValidationContext(schemaNode);
         validationContext.setConfig(config);
-        JsonSchema jsonSchema = new JsonSchema(validationContext, schemaNode);
+        JsonSchema jsonSchema = new JsonSchema(validationContext, schemaUrl, schemaNode);
         return jsonSchema;
     }
 
@@ -173,11 +178,11 @@ public class JsonSchemaFactory {
         }
         return jsonMetaSchema;
     }
-
+    
     public JsonSchema getSchema(String schema, SchemaValidatorsConfig config) {
         try {
             final JsonNode schemaNode = mapper.readTree(schema);
-            return newJsonSchema(schemaNode, config);
+            return newJsonSchema(null, schemaNode, config);
         } catch (IOException ioe) {
             logger.error("Failed to load json schema!", ioe);
             throw new JsonSchemaException(ioe);
@@ -187,11 +192,11 @@ public class JsonSchemaFactory {
     public JsonSchema getSchema(String schema) {
         return getSchema(schema, null);
     }
-
+    
     public JsonSchema getSchema(InputStream schemaStream, SchemaValidatorsConfig config) {
         try {
             final JsonNode schemaNode = mapper.readTree(schemaStream);
-            return newJsonSchema(schemaNode, config);
+            return newJsonSchema(null, schemaNode, config);
         } catch (IOException ioe) {
             logger.error("Failed to load json schema!", ioe);
             throw new JsonSchemaException(ioe);
@@ -201,7 +206,7 @@ public class JsonSchemaFactory {
     public JsonSchema getSchema(InputStream schemaStream) {
         return getSchema(schemaStream, null);
     }
-
+    
     public JsonSchema getSchema(URL schemaURL, SchemaValidatorsConfig config) {
         try {
             InputStream inputStream = null;
@@ -214,11 +219,10 @@ public class JsonSchemaFactory {
                 final JsonMetaSchema jsonMetaSchema = findMetaSchemaForSchema(schemaNode);
 
                 if (idMatchesSourceUrl(jsonMetaSchema, schemaNode, schemaURL)) {
-                    
-                    return new JsonSchema(new ValidationContext(jsonMetaSchema, this), schemaNode, true /*retrieved via id, resolving will not change anything*/);
+                    return new JsonSchema(new ValidationContext(jsonMetaSchema, this), mappedURL, schemaNode, true /*retrieved via id, resolving will not change anything*/);
                 }
 
-                return newJsonSchema(schemaNode, config);
+                return newJsonSchema(mappedURL, schemaNode, config);
             } finally {
                 if (inputStream != null) {
                     inputStream.close();
@@ -231,15 +235,24 @@ public class JsonSchemaFactory {
     }
 
     public JsonSchema getSchema(URL schemaURL) {
-        return getSchema(schemaURL, null);
+        return getSchema(schemaURL, new SchemaValidatorsConfig());
     }
 
+    public JsonSchema getSchema(URL schemaUrl, JsonNode jsonNode, SchemaValidatorsConfig config) {
+        return newJsonSchema(schemaUrl, jsonNode, config);
+    }
+    
+
     public JsonSchema getSchema(JsonNode jsonNode, SchemaValidatorsConfig config) {
-        return newJsonSchema(jsonNode, config);
+        return newJsonSchema(null, jsonNode, config);
     }
 
+    public JsonSchema getSchema(URL schemaUrl, JsonNode jsonNode) {
+        return newJsonSchema(schemaUrl, jsonNode, null);
+    }
+    
     public JsonSchema getSchema(JsonNode jsonNode) {
-        return newJsonSchema(jsonNode, null);
+        return newJsonSchema(null, jsonNode, null);
     }
 
     private boolean idMatchesSourceUrl(JsonMetaSchema metaSchema, JsonNode schema, URL schemaUrl) {
diff --git a/src/main/java/com/networknt/schema/NotValidator.java b/src/main/java/com/networknt/schema/NotValidator.java
index 7917f36..564d833 100644
--- a/src/main/java/com/networknt/schema/NotValidator.java
+++ b/src/main/java/com/networknt/schema/NotValidator.java
@@ -30,7 +30,7 @@ public class NotValidator extends BaseJsonValidator implements JsonValidator {
 
     public NotValidator(String schemaPath, JsonNode schemaNode, JsonSchema parentSchema, ValidationContext validationContext) {
         super(schemaPath, schemaNode, parentSchema, ValidatorTypeCode.NOT, validationContext);
-        schema = new JsonSchema(validationContext, getValidatorType().getValue(), schemaNode, parentSchema);
+        schema = new JsonSchema(validationContext, getValidatorType().getValue(), parentSchema.getCurrentUrl(), schemaNode, parentSchema);
 
         parseErrorCode(getValidatorType().getErrorCodeKey());
     }
diff --git a/src/main/java/com/networknt/schema/OneOfValidator.java b/src/main/java/com/networknt/schema/OneOfValidator.java
index 777cef4..e1905e1 100644
--- a/src/main/java/com/networknt/schema/OneOfValidator.java
+++ b/src/main/java/com/networknt/schema/OneOfValidator.java
@@ -121,7 +121,7 @@ public class OneOfValidator extends BaseJsonValidator implements JsonValidator {
         int size = schemaNode.size();
         for (int i = 0; i < size; i++) {
             JsonNode childNode = schemaNode.get(i);
-            JsonSchema childSchema = new JsonSchema(validationContext, getValidatorType().getValue(), childNode, parentSchema);
+            JsonSchema childSchema = new JsonSchema(validationContext, getValidatorType().getValue(), parentSchema.getCurrentUrl(), childNode, parentSchema);
             schemas.add(new ShortcutValidator(childNode, parentSchema, validationContext, childSchema));
         }
 
diff --git a/src/main/java/com/networknt/schema/PatternPropertiesValidator.java b/src/main/java/com/networknt/schema/PatternPropertiesValidator.java
index 2efe4dc..9feb4aa 100644
--- a/src/main/java/com/networknt/schema/PatternPropertiesValidator.java
+++ b/src/main/java/com/networknt/schema/PatternPropertiesValidator.java
@@ -38,7 +38,7 @@ public class PatternPropertiesValidator extends BaseJsonValidator implements Jso
         Iterator<String> names = schemaNode.fieldNames();
         while (names.hasNext()) {
             String name = names.next();
-            schemas.put(Pattern.compile(name), new JsonSchema(validationContext, name, schemaNode.get(name), parentSchema));
+            schemas.put(Pattern.compile(name), new JsonSchema(validationContext, name, parentSchema.getCurrentUrl(), schemaNode.get(name), parentSchema));
         }
     }
 
diff --git a/src/main/java/com/networknt/schema/PropertiesValidator.java b/src/main/java/com/networknt/schema/PropertiesValidator.java
index 4a4f9e7..3daee20 100644
--- a/src/main/java/com/networknt/schema/PropertiesValidator.java
+++ b/src/main/java/com/networknt/schema/PropertiesValidator.java
@@ -32,7 +32,7 @@ public class PropertiesValidator extends BaseJsonValidator implements JsonValida
         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, schemaNode.get(pname), parentSchema));
+            schemas.put(pname, new JsonSchema(validationContext, schemaPath + "/" + pname, parentSchema.getCurrentUrl(), schemaNode.get(pname), parentSchema));
         }
     }
 
diff --git a/src/main/java/com/networknt/schema/RefValidator.java b/src/main/java/com/networknt/schema/RefValidator.java
index b1de2ca..4d1c7af 100644
--- a/src/main/java/com/networknt/schema/RefValidator.java
+++ b/src/main/java/com/networknt/schema/RefValidator.java
@@ -16,7 +16,6 @@
 
 package com.networknt.schema;
 
-import java.io.InputStream;
 import java.net.MalformedURLException;
 import java.net.URL;
 import java.text.MessageFormat;
@@ -34,9 +33,7 @@ public class RefValidator extends BaseJsonValidator implements JsonValidator {
 
     protected JsonSchema schema;
     
-    private static final String REF_DOMAIN = "/";
     private static final String REF_CURRENT = "#";
-    private static final String REF_RELATIVE = "../";
 
     public RefValidator(String schemaPath, JsonNode schemaNode, JsonSchema parentSchema, ValidationContext validationContext) {
 
@@ -50,23 +47,25 @@ public class RefValidator extends BaseJsonValidator implements JsonValidator {
 
     static JsonSchema getRefSchema(JsonSchema parentSchema, ValidationContext validationContext, String refValue) {
         if (!refValue.startsWith(REF_CURRENT)) {
-            // handle remote ref
-            String schemaUrl = refValue;
-        	    int index = refValue.indexOf(REF_CURRENT);
+            // This will be the url extracted from the refValue (this may be a relative or absolute Url).
+            final String refUrl;
+            final int index = refValue.indexOf(REF_CURRENT);
             if (index > 0) {
-                schemaUrl = schemaUrl.substring(0, index);
-            }
-            if(isRelativePath(schemaUrl)){
-                schemaUrl = obtainAbsolutePath(parentSchema, schemaUrl);
+                refUrl = refValue.substring(0, index);
+            } else {
+                refUrl = refValue;
             }
             
-            try {
-                URL url = URLFactory.toURL(schemaUrl);
-                parentSchema = validationContext.getJsonSchemaFactory().getSchema(url, validationContext.getConfig());
-            } catch (MalformedURLException e) {
-                InputStream is = Thread.currentThread().getContextClassLoader().getResourceAsStream(schemaUrl);
-                parentSchema = validationContext.getJsonSchemaFactory().getSchema(is);
+            // This will determine the correct absolute url for the refUrl. This decision will take into
+            // account the current url of the parent schema.
+            URL schemaUrl = determineSchemaUrl(parentSchema, refUrl);
+            if (schemaUrl == null) {
+              return null;
             }
+            
+            // This should retrieve schemas regardless of the protocol that is in the url.
+            parentSchema = validationContext.getJsonSchemaFactory().getSchema(schemaUrl, validationContext.getConfig());
+            
             if (index < 0) {
                 return parentSchema.findAncestor();
             } else {
@@ -78,41 +77,28 @@ public class RefValidator extends BaseJsonValidator implements JsonValidator {
         } else {
             JsonNode node = parentSchema.getRefSchemaNode(refValue);
             if (node != null) {
-                return new JsonSchema(validationContext, refValue, node, parentSchema);
+                return new JsonSchema(validationContext, refValue, parentSchema.getCurrentUrl(), node, parentSchema);
             }
         }
         return null;
     }
-    
-    private static boolean isRelativePath(String schemaUrl) {
-    	return !schemaUrl.startsWith("http");
-    }
-    
-    private static String obtainAbsolutePath(JsonSchema parentSchema, String schemaUrl) {
-    	String baseSchemaUrl = parentSchema.findAncestor().getSchemaNode().get("id").textValue();
-		int index = baseSchemaUrl.lastIndexOf("/");
-		baseSchemaUrl = baseSchemaUrl.substring(0, index);
-		
-		String schemaRef = schemaUrl;
-		
-		if(schemaRef.startsWith(REF_DOMAIN)){
-			// from domain add ref
-			try {
-				URL url = URLFactory.toURL(baseSchemaUrl);
-				baseSchemaUrl = url.getProtocol()+"//"+url.getHost();
-			} catch (MalformedURLException e) {
-				e.printStackTrace();
-			}
-		}else if(schemaRef.startsWith(REF_RELATIVE)){
-			// relative from schema
-			while(schemaRef.startsWith(REF_RELATIVE)){
-				index = baseSchemaUrl.lastIndexOf("/");
-				baseSchemaUrl = baseSchemaUrl.substring(0, index);
-				schemaRef = schemaRef.replaceFirst(REF_RELATIVE, "");
-			}
-		}
-		schemaRef = baseSchemaUrl +"/"+ schemaRef;
-		return schemaRef;
+
+    private static URL determineSchemaUrl(JsonSchema parentSchema, String refUrl) {
+        URL schemaUrl;
+        try {
+            // If the refUrl is an absolute url, then this will succeed.
+            schemaUrl = URLFactory.toURL(refUrl);
+        } catch (MalformedURLException e) {
+            try {
+                // If the refUrl is a valid relative url in the context of the parent schema's url,
+                // then this will succeed.
+                schemaUrl = URLFactory.toURL(parentSchema.getCurrentUrl(), refUrl);
+            } catch (MalformedURLException e2) {
+                // We are unable to resolve the reference at this point.
+                schemaUrl = null;
+            }
+        }
+        return schemaUrl;
     }
 
     public Set<ValidationMessage> validate(JsonNode node, JsonNode rootNode, String at) {
diff --git a/src/main/java/com/networknt/schema/SchemaValidatorsConfig.java b/src/main/java/com/networknt/schema/SchemaValidatorsConfig.java
index 3aad2a1..cec3135 100644
--- a/src/main/java/com/networknt/schema/SchemaValidatorsConfig.java
+++ b/src/main/java/com/networknt/schema/SchemaValidatorsConfig.java
@@ -16,6 +16,7 @@
 
 package com.networknt.schema;
 
+import java.net.URL;
 import java.util.HashMap;
 import java.util.Map;
 
@@ -60,7 +61,7 @@ public class SchemaValidatorsConfig {
     public void setUrlMappings(Map<String, String> urlMappings) {
         this.urlMappings = urlMappings;
     }
-
+    
     public boolean isMissingNodeAsError() {
     	return missingNodeAsError;
     }
diff --git a/src/main/java/com/networknt/schema/UnionTypeValidator.java b/src/main/java/com/networknt/schema/UnionTypeValidator.java
index 159f364..5d6d977 100644
--- a/src/main/java/com/networknt/schema/UnionTypeValidator.java
+++ b/src/main/java/com/networknt/schema/UnionTypeValidator.java
@@ -50,7 +50,7 @@ public class UnionTypeValidator extends BaseJsonValidator implements JsonValidat
             sep = ", ";
 
             if (n.isObject())
-                schemas.add(new JsonSchema(validationContext, ValidatorTypeCode.TYPE.getValue(), n, parentSchema));
+                schemas.add(new JsonSchema(validationContext, ValidatorTypeCode.TYPE.getValue(), parentSchema.getCurrentUrl(), n, parentSchema));
             else
                 schemas.add(new TypeValidator(schemaPath + "/" + i, n, parentSchema, validationContext));
 
diff --git a/src/main/java/com/networknt/schema/url/URLFactory.java b/src/main/java/com/networknt/schema/url/URLFactory.java
index d78deec..430980c 100644
--- a/src/main/java/com/networknt/schema/url/URLFactory.java
+++ b/src/main/java/com/networknt/schema/url/URLFactory.java
@@ -32,13 +32,24 @@ import java.net.URL;
 public class URLFactory {
   private static final ClasspathURLStreamHandler sClasspathURLStreamHandler = new ClasspathURLStreamHandler();
 
+  /**
+   * Creates an {@link URL} based on the provided string and parent url.
+   * @param parentUrl the parent URL of the given url (if no parent exists, then this can be null).
+   * @param url the url
+   * @return a {@link URL}
+   * @throws MalformedURLException if the url is not a proper URL
+   */
+  public static URL toURL(final URL parentUrl, final String url) throws MalformedURLException {
+    return new URL(parentUrl, url, sClasspathURLStreamHandler.supports(url) ? sClasspathURLStreamHandler : null);
+  }
+  
   /**
    * Creates an {@link URL} based on the provided string
-   * @param pURL the url
+   * @param url the url
    * @return a {@link URL}
    * @throws MalformedURLException if the url is not a proper URL
    */
-  public static URL toURL(final String pURL) throws MalformedURLException {
-    return new URL(null, pURL, sClasspathURLStreamHandler.supports(pURL) ? sClasspathURLStreamHandler : null);
+  public static URL toURL(final String url) throws MalformedURLException {
+    return new URL(null, url, sClasspathURLStreamHandler.supports(url) ? sClasspathURLStreamHandler : null);
   }
 }
\ No newline at end of file


Test patch:
diff --git a/src/test/java/com/networknt/schema/JsonSchemaTest.java b/src/test/java/com/networknt/schema/JsonSchemaTest.java
index 711d524..e743e87 100644
--- a/src/test/java/com/networknt/schema/JsonSchemaTest.java
+++ b/src/test/java/com/networknt/schema/JsonSchemaTest.java
@@ -16,15 +16,7 @@
 
 package com.networknt.schema;
 
-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.Assert;
-import org.junit.BeforeClass;
-import org.junit.Test;
+import static io.undertow.Handlers.resource;
 
 import java.io.File;
 import java.io.InputStream;
@@ -32,7 +24,18 @@ import java.net.URL;
 import java.util.ArrayList;
 import java.util.List;
 
-import static io.undertow.Handlers.resource;
+import org.junit.AfterClass;
+import org.junit.Assert;
+import org.junit.BeforeClass;
+import org.junit.Test;
+
+import com.fasterxml.jackson.databind.JsonNode;
+import com.fasterxml.jackson.databind.ObjectMapper;
+import com.fasterxml.jackson.databind.node.ArrayNode;
+import com.networknt.schema.url.URLFactory;
+
+import io.undertow.Undertow;
+import io.undertow.server.handlers.resource.FileResourceManager;
 
 public class JsonSchemaTest {
     protected ObjectMapper mapper = new ObjectMapper();
@@ -67,6 +70,7 @@ public class JsonSchemaTest {
     }
 
     private void runTestFile(String testCaseFile) throws Exception {
+        final URL testCaseFileUrl = URLFactory.toURL("classpath:" + testCaseFile);
         InputStream in = Thread.currentThread().getContextClassLoader()
                 .getResourceAsStream(testCaseFile);
         ArrayNode testCases = (ArrayNode) mapper.readTree(in);
@@ -83,8 +87,8 @@ public class JsonSchemaTest {
                     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) ? false : typeLooseNode.asBoolean());
-                    JsonSchema schema = validatorFactory.getSchema(testCase.get("schema"), config);
+                    config.setTypeLoose((typeLooseNode == null) ? true : typeLooseNode.asBoolean());
+                    JsonSchema schema = validatorFactory.getSchema(testCaseFileUrl, testCase.get("schema"), config);
                     List<ValidationMessage> errors = new ArrayList<ValidationMessage>();
 
                     errors.addAll(schema.validate(node));
@@ -106,7 +110,7 @@ public class JsonSchemaTest {
                     }
                 }
             } catch (JsonSchemaException e) {
-                System.out.println("Bypass validation due to invalid schema: " + e.getMessage());
+                throw new IllegalStateException(String.format("Current schema should not be invalid: %s", testCaseFile), e);
             }
         }
     }
@@ -265,6 +269,11 @@ public class JsonSchemaTest {
         runTestFile("tests/refRemote.json");
     }
 
+    @Test
+    public void testRelativeRefRemoteValidator() throws Exception {
+        runTestFile("tests/relativeRefRemote.json");
+    }
+
     @Test
     public void testRequiredValidator() throws Exception {
         runTestFile("tests/required.json");
diff --git a/src/test/java/com/networknt/schema/SelfRefTest.java b/src/test/java/com/networknt/schema/SelfRefTest.java
index 70f6e27..b116555 100644
--- a/src/test/java/com/networknt/schema/SelfRefTest.java
+++ b/src/test/java/com/networknt/schema/SelfRefTest.java
@@ -16,16 +16,17 @@
 
 package com.networknt.schema;
 
-import com.fasterxml.jackson.databind.JsonNode;
+import org.junit.Ignore;
 import org.junit.Test;
 
 /**
  * Created by stevehu on 2016-12-20.
  */
 public class SelfRefTest extends BaseJsonSchemaValidatorTest {
-    @Test
+    @Ignore("This test currently is failing because of a StackOverflow caused by a recursive $ref.")
+    @Test()
     public void testSelfRef() throws Exception {
-        JsonNode node = getJsonNodeFromClasspath("selfref.json");
+        JsonSchema node = getJsonSchemaFromClasspath("selfRef.json");
         System.out.println("node = " + node);
     }
-}
+}
\ No newline at end of file
diff --git a/src/test/resources/selfref.json b/src/test/resources/selfRef.json
similarity index 99%
rename from src/test/resources/selfref.json
rename to src/test/resources/selfRef.json
index edd98ed..385c25d 100644
--- a/src/test/resources/selfref.json
+++ b/src/test/resources/selfRef.json
@@ -28,4 +28,4 @@
       ]
     }
   }
-}
+}
\ No newline at end of file
diff --git a/src/test/resources/tests/folder/folderInteger.json b/src/test/resources/tests/folder/folderInteger.json
new file mode 100644
index 0000000..85d2db6
--- /dev/null
+++ b/src/test/resources/tests/folder/folderInteger.json
@@ -0,0 +1,3 @@
+{
+	"type": "integer"
+}
\ No newline at end of file
diff --git a/src/test/resources/tests/refRemoteSchema.json b/src/test/resources/tests/refRemoteSchema.json
new file mode 100644
index 0000000..7dc6b75
--- /dev/null
+++ b/src/test/resources/tests/refRemoteSchema.json
@@ -0,0 +1,3 @@
+{
+  "$ref" : "subSchemas.json#/integer"
+}
\ No newline at end of file
diff --git a/src/test/resources/tests/relativeRefRemote.json b/src/test/resources/tests/relativeRefRemote.json
new file mode 100644
index 0000000..81277d8
--- /dev/null
+++ b/src/test/resources/tests/relativeRefRemote.json
@@ -0,0 +1,105 @@
+[
+    {
+        "description": "remote ref",
+        "schema": {"$ref": "integer.json"},
+        "tests": [
+            {
+                "description": "remote ref valid",
+                "data": 1,
+                "valid": true
+            },
+            {
+                "description": "remote ref invalid",
+                "data": "a",
+                "valid": false
+            }
+        ]
+    },
+    {
+        "description": "fragment within remote ref",
+        "schema": {"$ref": "subSchemas.json#/integer"},
+        "tests": [
+            {
+                "description": "remote fragment valid",
+                "data": 1,
+                "valid": true
+            },
+            {
+                "description": "remote fragment invalid",
+                "data": "a",
+                "valid": false
+            }
+        ]
+    },
+    {
+        "description": "ref within remote ref",
+        "schema": {
+            "$ref": "refRemoteSchema.json"
+        },
+        "tests": [
+            {
+                "description": "ref within ref valid",
+                "data": 1,
+                "valid": true
+            },
+            {
+                "description": "ref within ref invalid",
+                "data": "a",
+                "valid": false
+            }
+        ]
+    },
+    {
+        "description": "change resolution scope",
+        "schema": {
+            "items": {
+                "id": "folder/",
+                "items": {"$ref": "folderInteger.json"}
+            }
+        },
+        "tests": [
+            {
+                "description": "changed scope ref valid",
+                "data": [[1]],
+                "valid": true
+            },
+            {
+                "description": "changed scope ref invalid",
+                "data": [["a"]],
+                "valid": false
+            }
+        ]
+    },
+    {
+        "description": "sub directory reference with ref",
+        "schema": {"$ref": "self_ref/../subSchemas.json#/integer"},
+        "tests": [
+            {
+                "description": "remote fragment valid",
+                "data": 1,
+                "valid": true
+            },
+            {
+                "description": "remote fragment invalid",
+                "data": "a",
+                "valid": false
+            }
+        ]
+    },
+    {
+        "description": "domain reference with ref",
+        "schema": {"$ref": "/tests/integer.json"},
+        "tests": [
+            {
+                "description": "remote ref valid",
+                "data": 1,
+                "valid": true
+            },
+            {
+                "description": "remote ref invalid",
+                "data": "a",
+                "valid": false
+            }
+        ]
+    }
+]