Repository: networknt/json-schema-validator
Pull request #36 (2017-06-17 11:55:09)

Recursive load fix
Hi

I have found that loading JsonSchema by URL creates an infinite loop where the schema uses its `id` property to reference itself. 

I am using the "id" property of the schema as an explicit URL reference to the schema.

Example:

using `test/resource/self_ref/selfRef.json` as 
```
{
  "id": "http://localhost:1234/self_ref/selfRef.json",
  "description": "Schema with ID set to its own URL"
}
```

and creating a test case in `JsonSchemaTest.java` 

```
    @Test(expected = java.lang.StackOverflowError.class)
    public void testLoadingWithId() throws Exception {
        URL url = new URL("http://localhost:1234/self_ref/selfRef.json");
        JsonNode schemaJson = mapper.readTree(url);
        JsonSchemaFactory factory = new JsonSchemaFactory();
        JsonSchema schema = factory.getSchema(schemaJson);
    }
```

will result in a stackoverflow error from `BaseJsonValidator.ontainSubSchemaNode()`

I have added a fix in this patch

- added Constructor to BaseJsonValidator to accept an explicit subSchema
- added Constructor to JsonSchema to accept an explicit subSchema 
- in `JsonSchemaFactory.getSchema(URL url)` check for an `id` node and if that matches the requested id.  If the id matches the requested URL, then create a JsonSchema node with a `null` subSchema.  
- Added the tests above 
- tidied duplicate constructor code in `JsonSchema` with an `init()` function




Patch:
diff --git a/src/main/java/com/networknt/schema/BaseJsonValidator.java b/src/main/java/com/networknt/schema/BaseJsonValidator.java
index 840c723..e9af87a 100644
--- a/src/main/java/com/networknt/schema/BaseJsonValidator.java
+++ b/src/main/java/com/networknt/schema/BaseJsonValidator.java
@@ -41,6 +41,15 @@ public abstract class BaseJsonValidator implements JsonValidator {
         this.subSchema = obainSubSchemaNode(schemaNode);
     }
 
+    public BaseJsonValidator(String schemaPath, JsonNode schemaNode, JsonSchema parentSchema,
+                             ValidatorTypeCode validatorType, JsonSchema subSchema) {
+        this.schemaPath = schemaPath;
+        this.schemaNode = schemaNode;
+        this.parentSchema = parentSchema;
+        this.validatorType = validatorType;
+        this.subSchema = subSchema;
+    }
+
     protected String getSchemaPath() {
         return schemaPath;
     }
diff --git a/src/main/java/com/networknt/schema/JsonSchema.java b/src/main/java/com/networknt/schema/JsonSchema.java
index 7136e05..9447b0c 100644
--- a/src/main/java/com/networknt/schema/JsonSchema.java
+++ b/src/main/java/com/networknt/schema/JsonSchema.java
@@ -47,11 +47,23 @@ public class JsonSchema extends BaseJsonValidator {
     JsonSchema(ObjectMapper mapper, String schemaPath, JsonNode schemaNode,
                JsonSchema parent) {
         super(schemaPath, schemaNode, parent, null);
-        this.mapper = mapper;
+        this.init(mapper, schemaNode);
+    }
 
-        validators = new LinkedHashMap<String, JsonValidator>();
+    JsonSchema(ObjectMapper mapper, String schemaPath, JsonNode schemaNode,
+               JsonSchema parent, JsonSchema subSchema) {
+        super(schemaPath, schemaNode, parent, null, subSchema);
+        this.init(mapper, schemaNode);
+    }
 
-        read(schemaNode);
+    public JsonSchema(ObjectMapper mapper, JsonNode schemaNode, JsonSchema subSchema) {
+        this(mapper, "#", schemaNode, null, subSchema);
+    }
+
+    private void init(ObjectMapper mapper, JsonNode schemaNode) {
+        this.mapper = mapper;
+        this.validators = new LinkedHashMap<String, JsonValidator>();
+        this.read(schemaNode);
     }
 
     /**
diff --git a/src/main/java/com/networknt/schema/JsonSchemaFactory.java b/src/main/java/com/networknt/schema/JsonSchemaFactory.java
index 97609d8..3e53e36 100644
--- a/src/main/java/com/networknt/schema/JsonSchemaFactory.java
+++ b/src/main/java/com/networknt/schema/JsonSchemaFactory.java
@@ -26,6 +26,10 @@ import java.io.InputStream;
 import java.net.URL;
 
 public class JsonSchemaFactory {
+
+    // Draft 6 uses "$id"
+    private static final String DRAFT_4_ID = "id";
+
     private static final Logger logger = LoggerFactory
             .getLogger(JsonSchemaFactory.class);
     private ObjectMapper mapper;
@@ -60,8 +64,15 @@ public class JsonSchemaFactory {
 
     public JsonSchema getSchema(URL schemaURL) {
         try {
+
             JsonNode schemaNode = mapper.readTree(schemaURL.openStream());
+
+            if (this.idMatchesSourceUrl(schemaNode, schemaURL)) {
+                return new JsonSchema(mapper, schemaNode, null);
+            }
+
             return new JsonSchema(mapper, schemaNode);
+
         } catch (IOException ioe) {
             logger.error("Failed to load json schema!", ioe);
             throw new JsonSchemaException(ioe);
@@ -72,4 +83,18 @@ public class JsonSchemaFactory {
         return new JsonSchema(mapper, jsonNode);
     }
 
+    private boolean idMatchesSourceUrl(JsonNode schema, URL schemaUrl) {
+
+        JsonNode idNode = schema.get(DRAFT_4_ID);
+
+        if (idNode == null) {
+            return false;
+        }
+
+        String id = idNode.asText();
+        logger.info("Matching " + id + " to " + schemaUrl.toString());
+        return id.equals(schemaUrl.toString());
+
+    }
+
 }


Test patch:
diff --git a/src/test/java/com/networknt/schema/JsonSchemaTest.java b/src/test/java/com/networknt/schema/JsonSchemaTest.java
index adb4b4c..055f852 100644
--- a/src/test/java/com/networknt/schema/JsonSchemaTest.java
+++ b/src/test/java/com/networknt/schema/JsonSchemaTest.java
@@ -30,6 +30,7 @@ import org.junit.Test;
 
 import java.io.File;
 import java.io.InputStream;
+import java.net.URL;
 import java.util.ArrayList;
 import java.util.List;
 
@@ -105,6 +106,14 @@ public class JsonSchemaTest {
         }
     }
 
+    @Test(/*expected = java.lang.StackOverflowError.class*/)
+    public void testLoadingWithId() throws Exception {
+        URL url = new URL("http://localhost:1234/self_ref/selfRef.json");
+        JsonNode schemaJson = mapper.readTree(url);
+        JsonSchemaFactory factory = new JsonSchemaFactory();
+        JsonSchema schema = factory.getSchema(schemaJson);
+    }
+
     @Test
     public void testBignumValidator() throws Exception {
         runTestFile("tests/optional/bignum.json");
diff --git a/src/test/resources/tests/self_ref/selfRef.json b/src/test/resources/tests/self_ref/selfRef.json
new file mode 100644
index 0000000..10b45dd
--- /dev/null
+++ b/src/test/resources/tests/self_ref/selfRef.json
@@ -0,0 +1,4 @@
+{
+  "id": "http://localhost:1234/self_ref/selfRef.json",
+  "description": "Schema with ID set to its own URL"
+}