Repository: networknt/json-schema-validator
Pull request #343 (2020-11-10 13:51:41)

Improve type validation of numeric values
This PR tries to address #334 . The solution is not perfect, however, it improves the current situation.

In my opinion, we should not merge this PR as is. We should wait jackson-databind's upcoming `2.12.0` release which will [introduce](https://github.com/FasterXML/jackson-databind/blob/master/release-notes/VERSION-2.x#L26-L28) `JsonNode.canConvertToExactIntegral()`, a new method that will meet our purpose.

I wanted to open the PR so that maintainers can see my approach, the way I provide config to all validators etc.

Once jackson-databind 2.12.0 is out, I'll replace my solution with a call to `canConvertToExactIntegral`


Patch:
diff --git a/src/main/java/com/networknt/schema/DateTimeValidator.java b/src/main/java/com/networknt/schema/DateTimeValidator.java
index cc090a7..939ad6a 100644
--- a/src/main/java/com/networknt/schema/DateTimeValidator.java
+++ b/src/main/java/com/networknt/schema/DateTimeValidator.java
@@ -51,7 +51,7 @@ public class DateTimeValidator extends BaseJsonValidator implements JsonValidato
 
         Set<ValidationMessage> errors = new LinkedHashSet<ValidationMessage>();
 
-        JsonType nodeType = TypeFactory.getValueNodeType(node);
+        JsonType nodeType = TypeFactory.getValueNodeType(node, super.config);
         if (nodeType != JsonType.STRING) {
             return errors;
         }
diff --git a/src/main/java/com/networknt/schema/EnumValidator.java b/src/main/java/com/networknt/schema/EnumValidator.java
index 01ed2ca..abd95f9 100644
--- a/src/main/java/com/networknt/schema/EnumValidator.java
+++ b/src/main/java/com/networknt/schema/EnumValidator.java
@@ -96,7 +96,7 @@ public class EnumValidator extends BaseJsonValidator implements JsonValidator {
      * @param node JsonNode to check
      */
     private boolean isTypeLooseContainsInEnum(JsonNode node) {
-        if (TypeFactory.getValueNodeType(node) == JsonType.STRING) {
+        if (TypeFactory.getValueNodeType(node, super.config) == JsonType.STRING) {
             String nodeText = node.textValue();
             for (JsonNode n : nodes) {
                 String value = n.asText();
diff --git a/src/main/java/com/networknt/schema/ExclusiveMaximumValidator.java b/src/main/java/com/networknt/schema/ExclusiveMaximumValidator.java
index 943e71f..cf2942d 100644
--- a/src/main/java/com/networknt/schema/ExclusiveMaximumValidator.java
+++ b/src/main/java/com/networknt/schema/ExclusiveMaximumValidator.java
@@ -99,7 +99,7 @@ public class ExclusiveMaximumValidator extends BaseJsonValidator implements Json
     public Set<ValidationMessage> validate(JsonNode node, JsonNode rootNode, String at) {
         debug(logger, node, rootNode, at);
 
-        if (!TypeValidator.isNumber(node, config.isTypeLoose())) {
+        if (!TypeValidator.isNumber(node, super.config)) {
             // maximum only applies to numbers
             return Collections.emptySet();
         }
diff --git a/src/main/java/com/networknt/schema/ExclusiveMinimumValidator.java b/src/main/java/com/networknt/schema/ExclusiveMinimumValidator.java
index cc8b6f6..afccbfe 100644
--- a/src/main/java/com/networknt/schema/ExclusiveMinimumValidator.java
+++ b/src/main/java/com/networknt/schema/ExclusiveMinimumValidator.java
@@ -106,7 +106,7 @@ public class ExclusiveMinimumValidator extends BaseJsonValidator implements Json
     public Set<ValidationMessage> validate(JsonNode node, JsonNode rootNode, String at) {
         debug(logger, node, rootNode, at);
 
-        if (!TypeValidator.isNumber(node, config.isTypeLoose())) {
+        if (!TypeValidator.isNumber(node, super.config)) {
             // minimum only applies to numbers
             return Collections.emptySet();
         }
diff --git a/src/main/java/com/networknt/schema/FormatValidator.java b/src/main/java/com/networknt/schema/FormatValidator.java
index 8d8aec4..6db5f01 100644
--- a/src/main/java/com/networknt/schema/FormatValidator.java
+++ b/src/main/java/com/networknt/schema/FormatValidator.java
@@ -41,7 +41,7 @@ public class FormatValidator extends BaseJsonValidator implements JsonValidator
 
         Set<ValidationMessage> errors = new LinkedHashSet<ValidationMessage>();
 
-        JsonType nodeType = TypeFactory.getValueNodeType(node);
+        JsonType nodeType = TypeFactory.getValueNodeType(node, super.config);
         if (nodeType != JsonType.STRING) {
             return errors;
         }
diff --git a/src/main/java/com/networknt/schema/MaxLengthValidator.java b/src/main/java/com/networknt/schema/MaxLengthValidator.java
index 68ed1da..e5c097f 100644
--- a/src/main/java/com/networknt/schema/MaxLengthValidator.java
+++ b/src/main/java/com/networknt/schema/MaxLengthValidator.java
@@ -41,7 +41,7 @@ public class MaxLengthValidator extends BaseJsonValidator implements JsonValidat
     public Set<ValidationMessage> validate(JsonNode node, JsonNode rootNode, String at) {
         debug(logger, node, rootNode, at);
 
-        JsonType nodeType = TypeFactory.getValueNodeType(node);
+        JsonType nodeType = TypeFactory.getValueNodeType(node, super.config);
         if (nodeType != JsonType.STRING) {
             // ignore no-string typs
             return Collections.emptySet();
diff --git a/src/main/java/com/networknt/schema/MaximumValidator.java b/src/main/java/com/networknt/schema/MaximumValidator.java
index 849e4d3..b189bc7 100644
--- a/src/main/java/com/networknt/schema/MaximumValidator.java
+++ b/src/main/java/com/networknt/schema/MaximumValidator.java
@@ -108,7 +108,7 @@ public class MaximumValidator extends BaseJsonValidator implements JsonValidator
     public Set<ValidationMessage> validate(JsonNode node, JsonNode rootNode, String at) {
         debug(logger, node, rootNode, at);
 
-        if (!TypeValidator.isNumber(node, config.isTypeLoose())) {
+        if (!TypeValidator.isNumber(node, super.config)) {
             // maximum only applies to numbers
             return Collections.emptySet();
         }
diff --git a/src/main/java/com/networknt/schema/MinLengthValidator.java b/src/main/java/com/networknt/schema/MinLengthValidator.java
index 8eddebc..cec85eb 100644
--- a/src/main/java/com/networknt/schema/MinLengthValidator.java
+++ b/src/main/java/com/networknt/schema/MinLengthValidator.java
@@ -41,7 +41,7 @@ public class MinLengthValidator extends BaseJsonValidator implements JsonValidat
     public Set<ValidationMessage> validate(JsonNode node, JsonNode rootNode, String at) {
         debug(logger, node, rootNode, at);
 
-        JsonType nodeType = TypeFactory.getValueNodeType(node);
+        JsonType nodeType = TypeFactory.getValueNodeType(node, super.config);
         if (nodeType != JsonType.STRING) {
             // ignore non-string types
             return Collections.emptySet();
diff --git a/src/main/java/com/networknt/schema/MinimumValidator.java b/src/main/java/com/networknt/schema/MinimumValidator.java
index b2f932a..f9af8f8 100644
--- a/src/main/java/com/networknt/schema/MinimumValidator.java
+++ b/src/main/java/com/networknt/schema/MinimumValidator.java
@@ -114,7 +114,7 @@ public class MinimumValidator extends BaseJsonValidator implements JsonValidator
     public Set<ValidationMessage> validate(JsonNode node, JsonNode rootNode, String at) {
         debug(logger, node, rootNode, at);
 
-        if (!TypeValidator.isNumber(node, config.isTypeLoose())) {
+        if (!TypeValidator.isNumber(node, super.config)) {
             // minimum only applies to numbers
             return Collections.emptySet();
         }
diff --git a/src/main/java/com/networknt/schema/PatternValidator.java b/src/main/java/com/networknt/schema/PatternValidator.java
index ff7bb4f..ced9064 100644
--- a/src/main/java/com/networknt/schema/PatternValidator.java
+++ b/src/main/java/com/networknt/schema/PatternValidator.java
@@ -94,7 +94,7 @@ public class PatternValidator implements JsonValidator {
         public Set<ValidationMessage> validate(JsonNode node, JsonNode rootNode, String at) {
             debug(logger, node, rootNode, at);
 
-            JsonType nodeType = TypeFactory.getValueNodeType(node);
+            JsonType nodeType = TypeFactory.getValueNodeType(node, super.config);
             if (nodeType != JsonType.STRING) {
                 return Collections.emptySet();
             }
@@ -151,7 +151,7 @@ public class PatternValidator implements JsonValidator {
         public Set<ValidationMessage> validate(JsonNode node, JsonNode rootNode, String at) {
             debug(logger, node, rootNode, at);
 
-            JsonType nodeType = TypeFactory.getValueNodeType(node);
+            JsonType nodeType = TypeFactory.getValueNodeType(node, super.config);
             if (nodeType != JsonType.STRING) {
                 return Collections.emptySet();
             }
diff --git a/src/main/java/com/networknt/schema/SchemaValidatorsConfig.java b/src/main/java/com/networknt/schema/SchemaValidatorsConfig.java
index fca2390..4a15fd6 100644
--- a/src/main/java/com/networknt/schema/SchemaValidatorsConfig.java
+++ b/src/main/java/com/networknt/schema/SchemaValidatorsConfig.java
@@ -41,6 +41,11 @@ public class SchemaValidatorsConfig {
      */
     private boolean ecma262Validator;
 
+    /**
+     * When set to true, use Java-specific semantics rather than native JavaScript semantics
+     */
+    private boolean javaSemantics;
+
     /**
      * Map of public, normally internet accessible schema URLs to alternate locations; this allows for offline
      * validation of schemas that refer to public URLs. This is merged with any mappings the {@link JsonSchemaFactory}
@@ -112,7 +117,11 @@ public class SchemaValidatorsConfig {
     public void setEcma262Validator(boolean ecma262Validator) {
         this.ecma262Validator = ecma262Validator;
     }
-    
+
+    public boolean isJavaSemantics() { return javaSemantics; }
+
+    public void setJavaSemantics(boolean javaSemantics) { this.javaSemantics = javaSemantics; }
+
     public void addKeywordWalkListener(JsonSchemaWalkListener keywordWalkListener) {
 		if (keywordWalkListenersMap.get(ALL_KEYWORD_WALK_LISTENER_KEY) == null) {
 			List<JsonSchemaWalkListener> keywordWalkListeners = new ArrayList<JsonSchemaWalkListener>();
diff --git a/src/main/java/com/networknt/schema/TypeFactory.java b/src/main/java/com/networknt/schema/TypeFactory.java
index 57049fd..f2e93a4 100644
--- a/src/main/java/com/networknt/schema/TypeFactory.java
+++ b/src/main/java/com/networknt/schema/TypeFactory.java
@@ -18,6 +18,8 @@ package com.networknt.schema;
 
 import com.fasterxml.jackson.databind.JsonNode;
 
+import javax.xml.validation.Schema;
+
 public class TypeFactory {
     public static JsonType getSchemaNodeType(JsonNode node) {
         //Single Type Definition
@@ -57,7 +59,7 @@ public class TypeFactory {
         return JsonType.UNKNOWN;
     }
 
-    public static JsonType getValueNodeType(JsonNode node) {
+    public static JsonType getValueNodeType(JsonNode node, SchemaValidatorsConfig config) {
         if (node.isContainerNode()) {
             if (node.isObject())
                 return JsonType.OBJECT;
@@ -72,7 +74,10 @@ public class TypeFactory {
             if (node.isIntegralNumber())
                 return JsonType.INTEGER;
             if (node.isNumber())
-                return JsonType.NUMBER;
+                if (config.isJavaSemantics() && node.canConvertToLong() && (node.asText().indexOf('.') == -1))
+                    return JsonType.INTEGER;
+                else
+                    return JsonType.NUMBER;
             if (node.isBoolean())
                 return JsonType.BOOLEAN;
             if (node.isNull())
diff --git a/src/main/java/com/networknt/schema/TypeValidator.java b/src/main/java/com/networknt/schema/TypeValidator.java
index 1543898..3ca57db 100644
--- a/src/main/java/com/networknt/schema/TypeValidator.java
+++ b/src/main/java/com/networknt/schema/TypeValidator.java
@@ -51,7 +51,7 @@ public class TypeValidator extends BaseJsonValidator implements JsonValidator {
     }
 
     public boolean equalsToSchemaType(JsonNode node) {
-        JsonType nodeType = TypeFactory.getValueNodeType(node);
+        JsonType nodeType = TypeFactory.getValueNodeType(node, super.config);
         // in the case that node type is not the same as schema type, try to convert node to the
         // same type of schema. In REST API, query parameters, path parameters and headers are all
         // string type and we must convert, otherwise, all schema validations will fail.
@@ -107,7 +107,7 @@ public class TypeValidator extends BaseJsonValidator implements JsonValidator {
         }
 
         if (!equalsToSchemaType(node)) {
-            JsonType nodeType = TypeFactory.getValueNodeType(node);
+            JsonType nodeType = TypeFactory.getValueNodeType(node, super.config);
             return Collections.singleton(buildValidationMessage(at, nodeType.toString(), schemaType.toString()));
         }
         return Collections.emptySet();
@@ -218,14 +218,14 @@ public class TypeValidator extends BaseJsonValidator implements JsonValidator {
      * status of typeLoose flag.
      *
      * @param node        the JsonNode to check
-     * @param isTypeLoose The flag to show whether typeLoose is enabled
+     * @param config      the SchemaValidatorsConfig to depend on
      * @return boolean to indicate if it is a number
      */
-    public static boolean isNumber(JsonNode node, boolean isTypeLoose) {
+    public static boolean isNumber(JsonNode node, SchemaValidatorsConfig config) {
         if (node.isNumber()) {
             return true;
-        } else if (isTypeLoose) {
-            if (TypeFactory.getValueNodeType(node) == JsonType.STRING) {
+        } else if (config.isTypeLoose()) {
+            if (TypeFactory.getValueNodeType(node, config) == JsonType.STRING) {
                 return isNumeric(node.textValue());
             }
         }
diff --git a/src/main/java/com/networknt/schema/UUIDValidator.java b/src/main/java/com/networknt/schema/UUIDValidator.java
index 7b5cd14..2677bef 100644
--- a/src/main/java/com/networknt/schema/UUIDValidator.java
+++ b/src/main/java/com/networknt/schema/UUIDValidator.java
@@ -43,7 +43,7 @@ public class UUIDValidator extends BaseJsonValidator implements JsonValidator {
 
         Set<ValidationMessage> errors = new LinkedHashSet<ValidationMessage>();
 
-        JsonType nodeType = TypeFactory.getValueNodeType(node);
+        JsonType nodeType = TypeFactory.getValueNodeType(node, super.config);
         if (nodeType != JsonType.STRING) {
             return errors;
         }
diff --git a/src/main/java/com/networknt/schema/UnionTypeValidator.java b/src/main/java/com/networknt/schema/UnionTypeValidator.java
index 446de01..f6f33b0 100644
--- a/src/main/java/com/networknt/schema/UnionTypeValidator.java
+++ b/src/main/java/com/networknt/schema/UnionTypeValidator.java
@@ -66,7 +66,7 @@ public class UnionTypeValidator extends BaseJsonValidator implements JsonValidat
     public Set<ValidationMessage> validate(JsonNode node, JsonNode rootNode, String at) {
         debug(logger, node, rootNode, at);
 
-        JsonType nodeType = TypeFactory.getValueNodeType(node);
+        JsonType nodeType = TypeFactory.getValueNodeType(node, super.config);
 
         boolean valid = false;
 
diff --git a/src/main/java/com/networknt/schema/format/EmailValidator.java b/src/main/java/com/networknt/schema/format/EmailValidator.java
index 994b247..57e9207 100644
--- a/src/main/java/com/networknt/schema/format/EmailValidator.java
+++ b/src/main/java/com/networknt/schema/format/EmailValidator.java
@@ -147,7 +147,7 @@ public class EmailValidator extends BaseJsonValidator implements JsonValidator {
 
         Set<ValidationMessage> errors = new LinkedHashSet<ValidationMessage>();
 
-        JsonType nodeType = TypeFactory.getValueNodeType(node);
+        JsonType nodeType = TypeFactory.getValueNodeType(node, super.config);
         if (nodeType != JsonType.STRING) {
             return errors;
         }


Test patch:
diff --git a/src/test/java/com/networknt/schema/TypeFactoryTest.java b/src/test/java/com/networknt/schema/TypeFactoryTest.java
new file mode 100755
index 0000000..adf2603
--- /dev/null
+++ b/src/test/java/com/networknt/schema/TypeFactoryTest.java
@@ -0,0 +1,53 @@
+/*
+ * Copyright (c) 2016 Network New Technologies Inc.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.networknt.schema;
+
+import com.fasterxml.jackson.databind.node.DecimalNode;
+import org.junit.Test;
+
+import java.math.BigDecimal;
+
+import static com.networknt.schema.TypeFactory.getValueNodeType;
+import static org.junit.Assert.*;
+
+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"
+    };
+
+    private final SchemaValidatorsConfig schemaValidatorsConfig = new SchemaValidatorsConfig();
+
+    @Test
+    public void testValidIntegralValuesWithJavaSemantics() {
+        schemaValidatorsConfig.setJavaSemantics(true);
+        for (String validValue : validIntegralValues) {
+            assertSame(validValue, JsonType.INTEGER,
+                    getValueNodeType(DecimalNode.valueOf(new BigDecimal(validValue)), schemaValidatorsConfig));
+        }
+    }
+
+    @Test
+    public void testValidIntegralValuesWithoutJavaSemantics() {
+        schemaValidatorsConfig.setJavaSemantics(false);
+        for (String validValue : validIntegralValues) {
+            assertSame(validValue, JsonType.NUMBER,
+                    getValueNodeType(DecimalNode.valueOf(new BigDecimal(validValue)), schemaValidatorsConfig));
+        }
+    }
+}