Repository: networknt/json-schema-validator
Pull request #361 (2020-12-21 14:26:58)

Validation of oneOf depends on schema order
Issue #295 appears to be caused when the validation of one element of a oneOf schema fails due to a failed 'required' block (setting ValidatorState.matchedNode to false), and a later schema succeeds without matching any 'properties' block (which would set ValidatorState.matchedNode to true).

I'm not familiar enough with the entire code to be sure that my fix is correct, but it fixes the reported testcase and I see no immediate problems.


Patch:
diff --git a/src/main/java/com/networknt/schema/DateTimeValidator.java b/src/main/java/com/networknt/schema/DateTimeValidator.java
index 939ad6a..ed4d01f 100644
--- a/src/main/java/com/networknt/schema/DateTimeValidator.java
+++ b/src/main/java/com/networknt/schema/DateTimeValidator.java
@@ -25,6 +25,7 @@ import java.text.SimpleDateFormat;
 import java.util.Collections;
 import java.util.LinkedHashSet;
 import java.util.Set;
+import java.util.TimeZone;
 import java.util.regex.Matcher;
 import java.util.regex.Pattern;
 
@@ -136,6 +137,7 @@ public class DateTimeValidator extends BaseJsonValidator implements JsonValidato
     private boolean validateDateTime(String dateTime, String pattern) {
         SimpleDateFormat sdf = new SimpleDateFormat(pattern);
         sdf.setLenient(false);
+        sdf.setTimeZone(TimeZone.getTimeZone("UTC"));
         return sdf.parse(dateTime, new ParsePosition(0)) != null;
     }
 }
diff --git a/src/main/java/com/networknt/schema/OneOfValidator.java b/src/main/java/com/networknt/schema/OneOfValidator.java
index 0ba7832..a2f5276 100644
--- a/src/main/java/com/networknt/schema/OneOfValidator.java
+++ b/src/main/java/com/networknt/schema/OneOfValidator.java
@@ -145,6 +145,8 @@ public class OneOfValidator extends BaseJsonValidator implements JsonValidator {
 //        }
 
         for (ShortcutValidator validator : schemas) {
+            // Reset state in case the previous validator did not match
+            state.setMatchedNode(true);
             if (!validator.allConstantsMatch(node)) {
                 // take a shortcut: if there is any constant that does not match,
                 // we can bail out of the validation


Test patch:
diff --git a/src/test/java/com/networknt/schema/DateTimeDSTTest.java b/src/test/java/com/networknt/schema/DateTimeDSTTest.java
new file mode 100644
index 0000000..8401937
--- /dev/null
+++ b/src/test/java/com/networknt/schema/DateTimeDSTTest.java
@@ -0,0 +1,34 @@
+package com.networknt.schema;
+
+import com.fasterxml.jackson.databind.JsonNode;
+import com.fasterxml.jackson.databind.ObjectMapper;
+import org.junit.Assert;
+import org.junit.Test;
+
+import java.io.InputStream;
+import java.util.Set;
+
+public class DateTimeDSTTest {
+    protected JsonSchema getJsonSchemaFromStreamContentV7(InputStream schemaContent) {
+        JsonSchemaFactory factory = JsonSchemaFactory.getInstance(SpecVersion.VersionFlag.V7);
+        return factory.getSchema(schemaContent);
+    }
+
+    protected JsonNode getJsonNodeFromStreamContent(InputStream content) throws Exception {
+        ObjectMapper mapper = new ObjectMapper();
+        JsonNode node = mapper.readTree(content);
+        return node;
+    }
+
+    @Test
+    public void shouldWorkV7() throws Exception {
+        String schemaPath = "/schema/dateTimeArray.json";
+        String dataPath = "/data/dstTimes.json"; // Contains 2020 DST changes for various countries
+        InputStream schemaInputStream = getClass().getResourceAsStream(schemaPath);
+        JsonSchema schema = getJsonSchemaFromStreamContentV7(schemaInputStream);
+        InputStream dataInputStream = getClass().getResourceAsStream(dataPath);
+        JsonNode node = getJsonNodeFromStreamContent(dataInputStream);
+        Set<ValidationMessage> errors = schema.validate(node);
+        Assert.assertEquals(0, errors.size());
+    }
+}
diff --git a/src/test/java/com/networknt/schema/Issue295Test.java b/src/test/java/com/networknt/schema/Issue295Test.java
new file mode 100644
index 0000000..680ae2a
--- /dev/null
+++ b/src/test/java/com/networknt/schema/Issue295Test.java
@@ -0,0 +1,34 @@
+package com.networknt.schema;
+
+import com.fasterxml.jackson.databind.JsonNode;
+import com.fasterxml.jackson.databind.ObjectMapper;
+import org.junit.Assert;
+import org.junit.Test;
+
+import java.io.InputStream;
+import java.util.Set;
+
+public class Issue295Test {
+    protected JsonSchema getJsonSchemaFromStreamContentV7(InputStream schemaContent) {
+        JsonSchemaFactory factory = JsonSchemaFactory.getInstance(SpecVersion.VersionFlag.V7);
+        return factory.getSchema(schemaContent);
+    }
+
+    protected JsonNode getJsonNodeFromStreamContent(InputStream content) throws Exception {
+        ObjectMapper mapper = new ObjectMapper();
+        JsonNode node = mapper.readTree(content);
+        return node;
+    }
+
+    @Test
+    public void shouldWorkV7() throws Exception {
+        String schemaPath = "/schema/issue295-v7.json";
+        String dataPath = "/data/issue295.json";
+        InputStream schemaInputStream = getClass().getResourceAsStream(schemaPath);
+        JsonSchema schema = getJsonSchemaFromStreamContentV7(schemaInputStream);
+        InputStream dataInputStream = getClass().getResourceAsStream(dataPath);
+        JsonNode node = getJsonNodeFromStreamContent(dataInputStream);
+        Set<ValidationMessage> errors = schema.validate(node);
+        Assert.assertEquals(0, errors.size());
+    }
+}
diff --git a/src/test/resources/data/dstTimes.json b/src/test/resources/data/dstTimes.json
new file mode 100644
index 0000000..928eeb6
--- /dev/null
+++ b/src/test/resources/data/dstTimes.json
@@ -0,0 +1,8 @@
+[
+	"2020-03-29T02:00:00Z",
+	"2020-09-27T02:00:00Z",
+	"2020-03-15T02:00:00Z",
+	"2020-03-08T02:00:00Z",
+	"2020-03-27T02:00:00Z",
+	"2020-08-04T02:00:00Z"
+]
diff --git a/src/test/resources/data/issue295.json b/src/test/resources/data/issue295.json
new file mode 100644
index 0000000..314b66c
--- /dev/null
+++ b/src/test/resources/data/issue295.json
@@ -0,0 +1,1 @@
+{ "other": "stuff" }
diff --git a/src/test/resources/draft7/if-then-else.json b/src/test/resources/draft7/if-then-else.json
index 1cdeeb9..634aa06 100644
--- a/src/test/resources/draft7/if-then-else.json
+++ b/src/test/resources/draft7/if-then-else.json
@@ -184,5 +184,69 @@
         "valid": true
       }
     ]
+  },
+  {
+    "description": "conditions by properties",
+    "schema": {
+      "type": "object",
+      "properties": {
+        "street_address": {
+          "type": "string"
+        },
+        "country": {
+          "enum": ["United States of America", "Canada"]
+        }
+      },
+      "if": {
+        "properties": { 
+          "country": { 
+            "const": "United States of America" 
+          } 
+        }
+      },
+      "then": {
+        "properties": { 
+          "postal_code": { 
+            "pattern": "[0-9]{5}(-[0-9]{4})?" 
+          } 
+        }
+      },
+      "else": {
+        "properties": { 
+          "postal_code": { 
+            "pattern": "[A-Z][0-9][A-Z] [0-9][A-Z][0-9]" 
+          } 
+        }
+      }
+    },
+    "tests": [
+      {
+        "description": "valid through then",
+        "data": {
+          "street_address": "1600 Pennsylvania Avenue NW",
+          "country": "United States of America",
+          "postal_code": "20500"
+        },
+        "valid": true
+      },
+      {
+        "description": "valid through else",
+        "data": {
+          "street_address": "24 Sussex Drive",
+          "country": "Canada",
+          "postal_code": "K1M 1M4"
+        },
+        "valid": true
+      },
+      {
+        "description": "invalid",
+        "data": {
+          "street_address": "24 Sussex Drive",
+          "country": "Canada",
+          "postal_code": "10000"
+        },
+        "valid": false
+      }
+    ]
   }
 ]
diff --git a/src/test/resources/schema/dateTimeArray.json b/src/test/resources/schema/dateTimeArray.json
new file mode 100644
index 0000000..68bab5b
--- /dev/null
+++ b/src/test/resources/schema/dateTimeArray.json
@@ -0,0 +1,10 @@
+{
+  "$id": "https://example.com/dateTimeTest.schema.json",
+  "$schema": "http://json-schema.org/draft-07/schema#",
+  "type": "array",
+  "items": {
+    "type": "string",
+    "format": "date-time"
+  }
+}
+
diff --git a/src/test/resources/schema/issue295-v7.json b/src/test/resources/schema/issue295-v7.json
new file mode 100644
index 0000000..449a0e6
--- /dev/null
+++ b/src/test/resources/schema/issue295-v7.json
@@ -0,0 +1,24 @@
+{
+  "$id": "https://example.com/properties.schema.json",
+  "$schema": "http://json-schema.org/draft-07/schema#",
+  "oneOf": [
+    {
+      "required": [
+        "requiredprop"
+      ],
+      "properties": {
+        "requiredprop": {
+          "type": "string"
+        }
+      }
+    },
+    {
+      "properties": {
+        "forbiddenprop": {
+          "type": "null"
+        }
+      }
+    }
+  ]
+}
+