Repository: networknt/json-schema-validator
Pull request #496 (2022-01-13 15:26:17)

Improve type validation of integrals
Instead of doing string comparison, use new jackson method to determine if a number is an integral.

The `javaSemantics` config option was added in PR #343 which partially addressed issue #334. In the notes for this PR:
> Once jackson-databind 2.12.0 is out, I'll replace my solution with a call to canConvertToExactIntegral

jackson-databind has been updated to 2.12.1 so this is available but the change has not yet been made.

PR #450 which addressed #446 missed this location which is used when calling `JsonSchemaFactory.getSchema`.

Issue #344 requested coercion of various types but the only type implemented in PR #379 was lossless narrowing, set with configuration option `losslessNarrowing`. I believe that setting is unnecessary now as this implementation of `javaSemantics` addresses the original issue, but left it for backwards compatibility. It also allows you to set `javaSemantics=false` and `losslessNarrowing=true` to achieve only this specific case rather than anything that `javaSemantics` is used for in the future. At this time, these properties do exactly the same thing.

- Change from string comparison to `canConvertToExactIntegral` for `javaSemantics` and `losslessNarrowing`
- Add missing documentation around `losslessNarrowing`
- Add more test cases around integrals

christi-square: @hkupty and @Navgeet may be interested in this change.

Patch:
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 73ecc17..69c9d9f 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -10,6 +10,8 @@ and this project adheres to [Semantic Versioning](http://semver.org/).
 
 ### Changed
 
+- fixes #446 maxLength and minLength do not work when a number with a zero decimal is used
+
 ## 1.0.65 - 2022-01-07
 
 ### Changed
diff --git a/doc/config.md b/doc/config.md
index be7d66a..d03544b 100644
--- a/doc/config.md
+++ b/doc/config.md
@@ -55,3 +55,11 @@ When set to true, use Java-specific semantics rather than native JavaScript sema
 For example, if the node type is `number` per JS semantics where the value can be losslesly interpreted as `java.lang.Long`, the validator would use `integer` as the node type instead of `number`. This is useful when schema type is `integer`, since validation would fail otherwise.
 
 For more details, please refer to this [issue](https://github.com/networknt/json-schema-validator/issues/334).
+
+* losslessNarrowing
+
+When set to true, can interpret round doubles as integers.
+
+Note that setting `javaSemantics = true` will achieve the same functionality at this time.
+
+For more details, please refer to this [issue](https://github.com/networknt/json-schema-validator/issues/344).
\ No newline at end of file
diff --git a/src/main/java/com/networknt/schema/ItemsValidator.java b/src/main/java/com/networknt/schema/ItemsValidator.java
index 80c7042..0785efe 100644
--- a/src/main/java/com/networknt/schema/ItemsValidator.java
+++ b/src/main/java/com/networknt/schema/ItemsValidator.java
@@ -58,7 +58,7 @@ public class ItemsValidator extends BaseJsonValidator implements JsonValidator {
                 if (addItemNode.isBoolean()) {
                     additionalItems = addItemNode.asBoolean();
                 } else if (addItemNode.isObject()) {
-                    foundAdditionalSchema = new JsonSchema(validationContext, parentSchema.getCurrentUri(), addItemNode);
+                    foundAdditionalSchema = new JsonSchema(validationContext, "#", parentSchema.getCurrentUri(), addItemNode, parentSchema);
                 }
             }
         }
@@ -197,4 +197,4 @@ public class ItemsValidator extends BaseJsonValidator implements JsonValidator {
             additionalSchema.initializeValidators();
         }
     }
-}
\ No newline at end of file
+}
diff --git a/src/main/java/com/networknt/schema/TypeFactory.java b/src/main/java/com/networknt/schema/TypeFactory.java
index 71bb8ea..d44ff9c 100644
--- a/src/main/java/com/networknt/schema/TypeFactory.java
+++ b/src/main/java/com/networknt/schema/TypeFactory.java
@@ -72,9 +72,9 @@ public class TypeFactory {
             if (node.isIntegralNumber())
                 return JsonType.INTEGER;
             if (node.isNumber())
-                if (config != null && config.isJavaSemantics() && node.canConvertToLong() && (node.asText().indexOf('.') == -1))
+                if (config != null && config.isJavaSemantics() && node.canConvertToExactIntegral())
                     return JsonType.INTEGER;
-                else if (config!= null && config.isLosslessNarrowing() && node.asText().endsWith(".0"))
+                else if (config != null && config.isLosslessNarrowing() && node.canConvertToExactIntegral())
                     return JsonType.INTEGER;
                 else
                     return JsonType.NUMBER;


Test patch:
diff --git a/src/test/java/com/networknt/schema/Issue493Test.java b/src/test/java/com/networknt/schema/Issue493Test.java
new file mode 100644
index 0000000..6bc65d6
--- /dev/null
+++ b/src/test/java/com/networknt/schema/Issue493Test.java
@@ -0,0 +1,94 @@
+package com.networknt.schema;
+
+import static org.hamcrest.MatcherAssert.assertThat;
+
+import java.io.InputStream;
+import java.util.HashSet;
+import java.util.Set;
+
+import org.hamcrest.Matchers;
+import org.junit.jupiter.api.Assertions;
+import org.junit.jupiter.api.DisplayName;
+import org.junit.jupiter.api.Test;
+
+import com.fasterxml.jackson.databind.JsonNode;
+import com.fasterxml.jackson.databind.ObjectMapper;
+
+class Issue493Test
+{
+
+    private static JsonSchemaFactory factory = JsonSchemaFactory.getInstance(SpecVersion.VersionFlag.V201909);
+    private static String schemaPath1 = "/schema/issue493.json";
+
+    private JsonNode getJsonNodeFromJsonData (String jsonFilePath)
+            throws Exception
+    {
+        InputStream content = getClass().getResourceAsStream(jsonFilePath);
+        ObjectMapper mapper = new ObjectMapper();
+        return mapper.readTree(content);
+    }
+
+    @Test
+    @DisplayName("Test valid with required item only")
+    void testValidJson1 ()
+            throws Exception
+    {
+        InputStream schemaInputStream = Issue493Test.class.getResourceAsStream(schemaPath1);
+        JsonSchema schema = factory.getSchema(schemaInputStream);
+        JsonNode node = getJsonNodeFromJsonData("/data/issue493-valid-1.json");
+        Set<ValidationMessage> errors = schema.validate(node);
+        Assertions.assertTrue(errors.isEmpty());
+    }
+
+    @Test
+    @DisplayName("Test valid with optional item")
+    void testValidJson2 ()
+            throws Exception
+    {
+        InputStream schemaInputStream = Issue493Test.class.getResourceAsStream(schemaPath1);
+        JsonSchema schema = factory.getSchema(schemaInputStream);
+        JsonNode node = getJsonNodeFromJsonData("/data/issue493-valid-2.json");
+        Set<ValidationMessage> errors = schema.validate(node);
+        Assertions.assertTrue(errors.isEmpty());
+    }
+
+    @Test
+    @DisplayName("Test invalid with required item but wrong type")
+    void testInvalidJson1 ()
+            throws Exception
+    {
+        InputStream schemaInputStream = Issue491Test.class.getResourceAsStream(schemaPath1);
+        JsonSchema schema = factory.getSchema(schemaInputStream);
+        JsonNode node = getJsonNodeFromJsonData("/data/issue493-invalid-1.json");
+        Set<ValidationMessage> errors = schema.validate(node);
+        Assertions.assertEquals(2, errors.size());
+
+        Set<String> allErrorMessages = new HashSet<>();
+        errors.forEach(vm -> {
+            allErrorMessages.add(vm.getMessage());
+        });
+        assertThat(allErrorMessages,
+                   Matchers.containsInAnyOrder("$.parameters[0].value: string found, integer expected",
+                                               "$.parameters[0].value: does not match the regex pattern ^\\{\\{.+\\}\\}$"));
+    }
+
+    @Test
+    @DisplayName("Test invalid with optional item but wrong type")
+    void testInvalidJson2 ()
+            throws Exception
+    {
+        InputStream schemaInputStream = Issue491Test.class.getResourceAsStream(schemaPath1);
+        JsonSchema schema = factory.getSchema(schemaInputStream);
+        JsonNode node = getJsonNodeFromJsonData("/data/issue493-invalid-2.json");
+        Set<ValidationMessage> errors = schema.validate(node);
+        Assertions.assertEquals(2, errors.size());
+
+        Set<String> allErrorMessages = new HashSet<>();
+        errors.forEach(vm -> {
+            allErrorMessages.add(vm.getMessage());
+        });
+        assertThat(allErrorMessages,
+                   Matchers.containsInAnyOrder("$.parameters[1].value: string found, integer expected",
+                                               "$.parameters[1].value: does not match the regex pattern ^\\{\\{.+\\}\\}$"));
+    }
+}
diff --git a/src/test/java/com/networknt/schema/TypeFactoryTest.java b/src/test/java/com/networknt/schema/TypeFactoryTest.java
index b144527..5377bc4 100755
--- a/src/test/java/com/networknt/schema/TypeFactoryTest.java
+++ b/src/test/java/com/networknt/schema/TypeFactoryTest.java
@@ -28,29 +28,43 @@ public class TypeFactoryTest {
 
     private static final String[] validIntegralValues = {
             "1", "-1", "0E+1", "0E1", "-0E+1", "-0E1", "10.1E+1", "10.1E1", "-10.1E+1", "-10.1E1", "1E+0", "1E-0",
-            "1E0", "1E18", "9223372036854775807", "-9223372036854775808"
+            "1E0", "1E18", "9223372036854775807", "-9223372036854775808", "1.0", "1.00", "-1.0", "-1.00"
+    };
+
+    private static final String[] validNonIntegralNumberValues = {
+        "1.1", "-1.1", "1.10"
     };
 
     private final SchemaValidatorsConfig schemaValidatorsConfig = new SchemaValidatorsConfig();
 
     @Test
-    public void testValidIntegralValuesWithJavaSemantics() {
+    public void testIntegralValuesWithJavaSemantics() {
         schemaValidatorsConfig.setJavaSemantics(true);
         for (String validValue : validIntegralValues) {
             assertSame(JsonType.INTEGER,
                     getValueNodeType(DecimalNode.valueOf(new BigDecimal(validValue)), schemaValidatorsConfig),
                     validValue);
         }
+        for (String validValue : validNonIntegralNumberValues) {
+            assertSame(JsonType.NUMBER,
+                    getValueNodeType(DecimalNode.valueOf(new BigDecimal(validValue)), schemaValidatorsConfig),
+                    validValue);
+        }
     }
 
     @Test
-    public void testValidIntegralValuesWithoutJavaSemantics() {
+    public void testIntegralValuesWithoutJavaSemantics() {
         schemaValidatorsConfig.setJavaSemantics(false);
         for (String validValue : validIntegralValues) {
             assertSame(JsonType.NUMBER,
                     getValueNodeType(DecimalNode.valueOf(new BigDecimal(validValue)), schemaValidatorsConfig),
                     validValue);
         }
+        for (String validValue : validNonIntegralNumberValues) {
+            assertSame(JsonType.NUMBER,
+                    getValueNodeType(DecimalNode.valueOf(new BigDecimal(validValue)), schemaValidatorsConfig),
+                    validValue);
+        }
     }
 
 
@@ -82,4 +96,4 @@ public class TypeFactoryTest {
         }
 
     }
-}
\ No newline at end of file
+}
diff --git a/src/test/resources/data/issue493-invalid-1.json b/src/test/resources/data/issue493-invalid-1.json
new file mode 100644
index 0000000..3ad7b7d
--- /dev/null
+++ b/src/test/resources/data/issue493-invalid-1.json
@@ -0,0 +1,8 @@
+{
+  "parameters": [
+    {
+      "name": "param-required",
+      "value": "wrongtype"
+    }
+  ]
+}
\ No newline at end of file
diff --git a/src/test/resources/data/issue493-invalid-2.json b/src/test/resources/data/issue493-invalid-2.json
new file mode 100644
index 0000000..970f4f0
--- /dev/null
+++ b/src/test/resources/data/issue493-invalid-2.json
@@ -0,0 +1,12 @@
+{
+  "parameters": [
+    {
+      "name": "param-required",
+      "value": 123
+    },
+    {
+      "name": "param-optional",
+      "value": "wrongtype"
+    }
+  ]
+}
\ No newline at end of file
diff --git a/src/test/resources/data/issue493-valid-1.json b/src/test/resources/data/issue493-valid-1.json
new file mode 100644
index 0000000..6f852db
--- /dev/null
+++ b/src/test/resources/data/issue493-valid-1.json
@@ -0,0 +1,12 @@
+{
+  "parameters": [
+    {
+      "name": "param-required",
+      "value": 123
+    },
+    {
+      "name": "param-optional",
+      "value": "{{test}}"
+    }
+  ]
+}
\ No newline at end of file
diff --git a/src/test/resources/data/issue493-valid-2.json b/src/test/resources/data/issue493-valid-2.json
new file mode 100644
index 0000000..6f852db
--- /dev/null
+++ b/src/test/resources/data/issue493-valid-2.json
@@ -0,0 +1,12 @@
+{
+  "parameters": [
+    {
+      "name": "param-required",
+      "value": 123
+    },
+    {
+      "name": "param-optional",
+      "value": "{{test}}"
+    }
+  ]
+}
\ No newline at end of file
diff --git a/src/test/resources/schema/issue493.json b/src/test/resources/schema/issue493.json
new file mode 100644
index 0000000..d24a0b1
--- /dev/null
+++ b/src/test/resources/schema/issue493.json
@@ -0,0 +1,72 @@
+{
+  "$schema": "https://json-schema.org/draft/2019-09/schema",
+  "type": "object",
+  "additionalProperties": false,
+  "properties": {
+    "parameters": {
+      "type": "array",
+      "uniqueItems": true,
+      "items": [
+        {
+          "type": "object",
+          "required": [
+            "name",
+            "value"
+          ],
+          "properties": {
+            "name": {
+              "const": "param-required"
+            },
+            "value": {
+              "anyOf": [
+                {
+                  "$ref": "#/$defs/test-ref1"
+                },
+                {
+                  "$ref": "#/$defs/test-ref2"
+                }
+              ]
+            }
+          }
+        }
+      ],
+      "additionalItems": {
+        "oneOf": [
+          {
+            "type": "object",
+            "required": [
+              "name",
+              "value"
+            ],
+            "properties": {
+              "name": {
+                "const": "param-optional"
+              },
+              "value": {
+                "anyOf": [
+                  {
+                    "$ref": "#/$defs/test-ref1"
+                  },
+                  {
+                    "$ref": "#/$defs/test-ref2"
+                  }
+                ]
+              }
+            }
+          }
+        ]
+      },
+      "minItems": 1,
+      "maxItems": 2
+    }
+  },
+  "$defs": {
+    "test-ref1": {
+      "type": "string",
+      "pattern": "^\\{\\{.+\\}\\}$"
+    },
+    "test-ref2": {
+      "type": "integer"
+    }
+  }
+}
\ No newline at end of file