Skip to content

Commit 054f2f6

Browse files
authored
Support handling of non-finite numbers if present (#1241)
* Support handling of non-finite numbers if present * Address review comments.
1 parent d352d6f commit 054f2f6

13 files changed

+381
-10
lines changed

src/main/java/com/networknt/schema/keyword/ConstValidator.java

Lines changed: 21 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@
2020
import com.networknt.schema.Schema;
2121
import com.networknt.schema.SchemaLocation;
2222
import com.networknt.schema.path.NodePath;
23+
import com.networknt.schema.utils.JsonNodeTypes;
2324
import com.networknt.schema.SchemaContext;
2425

2526
/**
@@ -33,11 +34,27 @@ public ConstValidator(SchemaLocation schemaLocation, JsonNode schemaNode,
3334

3435
public void validate(ExecutionContext executionContext, JsonNode node, JsonNode rootNode, NodePath instanceLocation) {
3536
if (schemaNode.isNumber() && node.isNumber()) {
36-
if (schemaNode.decimalValue().compareTo(node.decimalValue()) != 0) {
37+
boolean schemaIsNonFinite = JsonNodeTypes.isNonFiniteNumber(schemaNode);
38+
boolean nodeIsNonFinite = JsonNodeTypes.isNonFiniteNumber(node);
39+
if (schemaIsNonFinite != nodeIsNonFinite) {
3740
executionContext.addError(error().instanceNode(node).instanceLocation(instanceLocation)
38-
.evaluationPath(executionContext.getEvaluationPath()).locale(executionContext.getExecutionConfig().getLocale())
39-
.arguments(schemaNode.asString(schemaNode.toString()), node.asString())
40-
.build());
41+
.evaluationPath(executionContext.getEvaluationPath())
42+
.locale(executionContext.getExecutionConfig().getLocale())
43+
.arguments(schemaNode.asString(schemaNode.toString()), node.asString()).build());
44+
} else if (schemaIsNonFinite || nodeIsNonFinite) {
45+
// Handle the NaN, Infinity and -Infinity cases
46+
// Note that Double.compare(NaN, NaN) == 0 as this is comparing constants and not the numeric operation
47+
if (Double.compare(schemaNode.doubleValue(), node.doubleValue()) != 0) {
48+
executionContext.addError(error().instanceNode(node).instanceLocation(instanceLocation)
49+
.evaluationPath(executionContext.getEvaluationPath())
50+
.locale(executionContext.getExecutionConfig().getLocale())
51+
.arguments(schemaNode.asString(schemaNode.toString()), node.asString()).build());
52+
}
53+
} else if (schemaNode.decimalValue().compareTo(node.decimalValue()) != 0) {
54+
executionContext.addError(error().instanceNode(node).instanceLocation(instanceLocation)
55+
.evaluationPath(executionContext.getEvaluationPath())
56+
.locale(executionContext.getExecutionConfig().getLocale())
57+
.arguments(schemaNode.asString(schemaNode.toString()), node.asString()).build());
4158
}
4259
} else if (!schemaNode.equals(node)) {
4360
executionContext.addError(error().instanceNode(node).instanceLocation(instanceLocation)

src/main/java/com/networknt/schema/keyword/EnumValidator.java

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,11 +19,13 @@
1919
import tools.jackson.databind.JsonNode;
2020
import tools.jackson.databind.node.ArrayNode;
2121
import tools.jackson.databind.node.DecimalNode;
22+
import tools.jackson.databind.node.DoubleNode;
2223
import tools.jackson.databind.node.NullNode;
2324
import com.networknt.schema.ExecutionContext;
2425
import com.networknt.schema.Schema;
2526
import com.networknt.schema.SchemaLocation;
2627
import com.networknt.schema.path.NodePath;
28+
import com.networknt.schema.utils.JsonNodeTypes;
2729
import com.networknt.schema.utils.JsonType;
2830
import com.networknt.schema.utils.TypeFactory;
2931
import com.networknt.schema.SchemaContext;
@@ -129,6 +131,12 @@ private boolean isTypeLooseContainsInEnum(JsonNode node) {
129131
* @return the node
130132
*/
131133
protected JsonNode processNumberNode(JsonNode n) {
134+
if (JsonNodeTypes.isNonFiniteNumber(n)) {
135+
if (n.isDouble()) { // If it is already a DoubleNode don't create another one
136+
return n;
137+
}
138+
return DoubleNode.valueOf(n.doubleValue());
139+
}
132140
return DecimalNode.valueOf(n.decimalValue().stripTrailingZeros());
133141
}
134142

src/main/java/com/networknt/schema/keyword/MultipleOfValidator.java

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -63,7 +63,7 @@ public void validate(ExecutionContext executionContext, JsonNode node, JsonNode
6363
protected BigDecimal getDivisor(JsonNode schemaNode) {
6464
if (schemaNode.isNumber()) {
6565
double divisor = schemaNode.doubleValue();
66-
if (divisor != 0) {
66+
if (divisor != 0 && Double.isFinite(divisor)) {
6767
// convert to BigDecimal since double type is not accurate enough to do the
6868
// division and multiple
6969
return schemaNode.isBigDecimal() ? schemaNode.decimalValue().stripTrailingZeros() : BigDecimal.valueOf(divisor).stripTrailingZeros();
@@ -80,6 +80,11 @@ protected BigDecimal getDivisor(JsonNode schemaNode) {
8080
*/
8181
protected BigDecimal getDividend(JsonNode node) {
8282
if (node.isNumber()) {
83+
// Handle NaN, Infinity and -Infinity
84+
if (JsonNodeTypes.isNonFiniteNumber(node)) {
85+
// Incorrect type as NaN, Infinity and -Infinity are not valid JSON numbers so return null
86+
return null;
87+
}
8388
// convert to BigDecimal since double type is not accurate enough to do the
8489
// division and multiple
8590
return node.isBigDecimal() ? node.decimalValue() : BigDecimal.valueOf(node.doubleValue());

src/main/java/com/networknt/schema/utils/JsonNodeTypes.java

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -89,6 +89,9 @@ private static long detectVersion(SchemaContext schemaContext) {
8989
*/
9090
public static boolean isNumber(JsonNode node, SchemaRegistryConfig config) {
9191
if (node.isNumber()) {
92+
if (isNonFiniteNumber(node)) {
93+
return false;
94+
}
9295
return true;
9396
} else if (config.isTypeLoose()) {
9497
if (TypeFactory.getValueNodeType(node, config) == JsonType.STRING) {
@@ -98,6 +101,20 @@ public static boolean isNumber(JsonNode node, SchemaRegistryConfig config) {
98101
return false;
99102
}
100103

104+
/**
105+
* Check if the node is a number and is one of NaN, Infinity or -Infinity
106+
*
107+
* @param node to check
108+
* @return true if it is NaN, Infinity or -Infinity
109+
*/
110+
public static boolean isNonFiniteNumber(JsonNode node) {
111+
if (node.isFloatingPointNumber() && !node.isBigDecimal() && !node.isBigInteger()
112+
&& !Double.isFinite(node.doubleValue())) {
113+
return true;
114+
}
115+
return false;
116+
}
117+
101118
private static boolean isEnumObjectSchema(Schema jsonSchema, ExecutionContext executionContext) {
102119

103120
// There are three conditions for enum object schema

src/main/java/com/networknt/schema/utils/TypeFactory.java

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -102,6 +102,10 @@ public static JsonType getValueNodeType(JsonNode node, SchemaRegistryConfig conf
102102
} else if (config != null && config.isLosslessNarrowing() && node.canConvertToExactIntegral()) {
103103
return JsonType.INTEGER;
104104
} else {
105+
// Handle NaN, Infinity and -Infinity
106+
if (JsonNodeTypes.isNonFiniteNumber(node)) {
107+
return JsonType.UNKNOWN;
108+
}
105109
return JsonType.NUMBER;
106110
}
107111
case BOOLEAN:

src/test/java/com/networknt/schema/ConstValidatorTest.java

Lines changed: 70 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,10 @@
2424
import org.junit.jupiter.api.Test;
2525

2626
import com.networknt.schema.i18n.ResourceBundleMessageSource;
27+
import com.networknt.schema.serialization.NodeReader;
28+
29+
import tools.jackson.core.json.JsonReadFeature;
30+
import tools.jackson.databind.json.JsonMapper;
2731

2832
/**
2933
* Test for ConstValidator.
@@ -92,4 +96,70 @@ void invalidNumber() {
9296
assertFalse(messages.isEmpty());
9397
}
9498

99+
@Test
100+
void nan() {
101+
String schemaData = "{\r\n"
102+
+ " \"const\": NaN\r\n"
103+
+ "}";
104+
Schema schema = SchemaRegistry.withDefaultDialect(SpecificationVersion.DRAFT_2020_12,
105+
builder -> builder.nodeReader(NodeReader.builder()
106+
.jsonMapper(JsonMapper.builder().enable(JsonReadFeature.ALLOW_NON_NUMERIC_NUMBERS).build())
107+
.build()))
108+
.getSchema(schemaData);
109+
String inputData = "NaN";
110+
List<Error> messages = schema.validate(inputData, InputFormat.JSON);
111+
assertTrue(messages.isEmpty()); // Note that Double.compare(NaN, NaN) == 0 as this is comparing constants and
112+
// not the numeric operation
113+
}
114+
115+
@Test
116+
void infinity() {
117+
String schemaData = "{\r\n"
118+
+ " \"const\": Infinity\r\n"
119+
+ "}";
120+
Schema schema = SchemaRegistry.withDefaultDialect(SpecificationVersion.DRAFT_2020_12,
121+
builder -> builder.nodeReader(NodeReader.builder()
122+
.jsonMapper(JsonMapper.builder().enable(JsonReadFeature.ALLOW_NON_NUMERIC_NUMBERS).build())
123+
.build()))
124+
.getSchema(schemaData);
125+
String inputData = "Infinity";
126+
List<Error> messages = schema.validate(inputData, InputFormat.JSON);
127+
assertTrue(messages.isEmpty());
128+
}
129+
130+
@Test
131+
void negativeInfinity() {
132+
String schemaData = "{\r\n"
133+
+ " \"const\": -Infinity\r\n"
134+
+ "}";
135+
Schema schema = SchemaRegistry.withDefaultDialect(SpecificationVersion.DRAFT_2020_12,
136+
builder -> builder.nodeReader(NodeReader.builder()
137+
.jsonMapper(JsonMapper.builder().enable(JsonReadFeature.ALLOW_NON_NUMERIC_NUMBERS).build())
138+
.build()))
139+
.getSchema(schemaData);
140+
String inputData = "-Infinity";
141+
List<Error> messages = schema.validate(inputData, InputFormat.JSON);
142+
assertTrue(messages.isEmpty());
143+
}
144+
145+
@Test
146+
void nonFinite() {
147+
String schemaData = "{\r\n"
148+
+ " \"const\": 10\r\n"
149+
+ "}";
150+
Schema schema = SchemaRegistry.withDefaultDialect(SpecificationVersion.DRAFT_2020_12,
151+
builder -> builder.nodeReader(NodeReader.builder()
152+
.jsonMapper(JsonMapper.builder().enable(JsonReadFeature.ALLOW_NON_NUMERIC_NUMBERS).build())
153+
.build()))
154+
.getSchema(schemaData);
155+
String inputData = "-Infinity";
156+
List<Error> messages = schema.validate(inputData, InputFormat.JSON);
157+
assertFalse(messages.isEmpty());
158+
inputData = "Infinity";
159+
messages = schema.validate(inputData, InputFormat.JSON);
160+
assertFalse(messages.isEmpty());
161+
inputData = "NaN";
162+
messages = schema.validate(inputData, InputFormat.JSON);
163+
assertFalse(messages.isEmpty());
164+
}
95165
}

src/test/java/com/networknt/schema/EnumValidatorTest.java

Lines changed: 72 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,11 @@
2222

2323
import org.junit.jupiter.api.Test;
2424

25+
import com.networknt.schema.serialization.NodeReader;
26+
27+
import tools.jackson.core.json.JsonReadFeature;
28+
import tools.jackson.databind.json.JsonMapper;
29+
2530
/**
2631
* EnumValidator test.
2732
*/
@@ -108,4 +113,71 @@ void enumWithHeterogenousNodes() {
108113
Error message = messages.get(0);
109114
assertEquals(": does not have a value in the enumeration [6, \"foo\", [], true, {\"foo\":12}]", message.toString());
110115
}
116+
117+
@Test
118+
void nan() {
119+
String schemaData = "{\r\n"
120+
+ " \"enum\": [NaN]\r\n"
121+
+ "}";
122+
Schema schema = SchemaRegistry.withDefaultDialect(SpecificationVersion.DRAFT_2020_12,
123+
builder -> builder.nodeReader(NodeReader.builder()
124+
.jsonMapper(JsonMapper.builder().enable(JsonReadFeature.ALLOW_NON_NUMERIC_NUMBERS).build())
125+
.build()))
126+
.getSchema(schemaData);
127+
String inputData = "NaN";
128+
List<Error> messages = schema.validate(inputData, InputFormat.JSON);
129+
assertTrue(messages.isEmpty()); // Note that Double.compare(NaN, NaN) == 0 as this is comparing constants and
130+
// not the numeric operation
131+
}
132+
133+
@Test
134+
void infinity() {
135+
String schemaData = "{\r\n"
136+
+ " \"enum\": [Infinity]\r\n"
137+
+ "}";
138+
Schema schema = SchemaRegistry.withDefaultDialect(SpecificationVersion.DRAFT_2020_12,
139+
builder -> builder.nodeReader(NodeReader.builder()
140+
.jsonMapper(JsonMapper.builder().enable(JsonReadFeature.ALLOW_NON_NUMERIC_NUMBERS).build())
141+
.build()))
142+
.getSchema(schemaData);
143+
String inputData = "Infinity";
144+
List<Error> messages = schema.validate(inputData, InputFormat.JSON);
145+
assertTrue(messages.isEmpty());
146+
}
147+
148+
@Test
149+
void negativeInfinity() {
150+
String schemaData = "{\r\n"
151+
+ " \"enum\": [-Infinity]\r\n"
152+
+ "}";
153+
Schema schema = SchemaRegistry.withDefaultDialect(SpecificationVersion.DRAFT_2020_12,
154+
builder -> builder.nodeReader(NodeReader.builder()
155+
.jsonMapper(JsonMapper.builder().enable(JsonReadFeature.ALLOW_NON_NUMERIC_NUMBERS).build())
156+
.build()))
157+
.getSchema(schemaData);
158+
String inputData = "-Infinity";
159+
List<Error> messages = schema.validate(inputData, InputFormat.JSON);
160+
assertTrue(messages.isEmpty());
161+
}
162+
163+
@Test
164+
void nonFinite() {
165+
String schemaData = "{\r\n"
166+
+ " \"enum\": [10]\r\n"
167+
+ "}";
168+
Schema schema = SchemaRegistry.withDefaultDialect(SpecificationVersion.DRAFT_2020_12,
169+
builder -> builder.nodeReader(NodeReader.builder()
170+
.jsonMapper(JsonMapper.builder().enable(JsonReadFeature.ALLOW_NON_NUMERIC_NUMBERS).build())
171+
.build()))
172+
.getSchema(schemaData);
173+
String inputData = "-Infinity";
174+
List<Error> messages = schema.validate(inputData, InputFormat.JSON);
175+
assertFalse(messages.isEmpty());
176+
inputData = "Infinity";
177+
messages = schema.validate(inputData, InputFormat.JSON);
178+
assertFalse(messages.isEmpty());
179+
inputData = "NaN";
180+
messages = schema.validate(inputData, InputFormat.JSON);
181+
assertFalse(messages.isEmpty());
182+
}
111183
}

src/test/java/com/networknt/schema/ExclusiveMaximumValidatorTest.java

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,9 +17,15 @@
1717

1818
import static org.junit.jupiter.api.Assertions.assertEquals;
1919

20+
import java.util.List;
21+
2022
import org.junit.jupiter.api.Test;
2123

2224
import com.networknt.schema.dialect.Dialects;
25+
import com.networknt.schema.serialization.NodeReader;
26+
27+
import tools.jackson.core.json.JsonReadFeature;
28+
import tools.jackson.databind.json.JsonMapper;
2329

2430
/**
2531
* Test ExclusiveMaximumValidator validator.
@@ -37,4 +43,22 @@ void exclusiveMaximum() {
3743
final SchemaRegistry schemaRegistry = SchemaRegistry.withDialect(Dialects.getDraft7());
3844
assertEquals(1, schemaRegistry.getSchema(schemaString).validate("10", InputFormat.JSON).size());
3945
}
46+
47+
@Test
48+
void nonFinite() {
49+
String schemaData = "{\r\n"
50+
+ " \"exclusiveMaximum\": 10\r\n"
51+
+ "}";
52+
Schema schema = SchemaRegistry.withDefaultDialect(SpecificationVersion.DRAFT_4,
53+
builder -> builder.nodeReader(NodeReader.builder()
54+
.jsonMapper(JsonMapper.builder().enable(JsonReadFeature.ALLOW_NON_NUMERIC_NUMBERS).build())
55+
.build()))
56+
.getSchema(schemaData);
57+
List<Error> errors = schema.validate("NaN", InputFormat.JSON);
58+
assertEquals(0, errors.size());
59+
errors = schema.validate("Infinity", InputFormat.JSON);
60+
assertEquals(0, errors.size());
61+
errors = schema.validate("-Infinity", InputFormat.JSON);
62+
assertEquals(0, errors.size());
63+
}
4064
}

src/test/java/com/networknt/schema/ExclusiveMinimumValidatorTest.java

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,10 @@
2525
import com.networknt.schema.dialect.Dialect;
2626
import com.networknt.schema.dialect.Dialects;
2727
import com.networknt.schema.keyword.DisallowUnknownKeywordFactory;
28+
import com.networknt.schema.serialization.NodeReader;
29+
30+
import tools.jackson.core.json.JsonReadFeature;
31+
import tools.jackson.databind.json.JsonMapper;
2832

2933
/**
3034
* Test ExclusiveMinimumValidator validator.
@@ -105,4 +109,22 @@ void exclusiveMinimum() {
105109
final SchemaRegistry schemaRegistry = SchemaRegistry.withDialect(Dialects.getDraft7());
106110
assertEquals(1, schemaRegistry.getSchema(schemaString).validate("10", InputFormat.JSON).size());
107111
}
112+
113+
@Test
114+
void nonFinite() {
115+
String schemaData = "{\r\n"
116+
+ " \"exclusiveMinimum\": 10\r\n"
117+
+ "}";
118+
Schema schema = SchemaRegistry.withDefaultDialect(SpecificationVersion.DRAFT_4,
119+
builder -> builder.nodeReader(NodeReader.builder()
120+
.jsonMapper(JsonMapper.builder().enable(JsonReadFeature.ALLOW_NON_NUMERIC_NUMBERS).build())
121+
.build()))
122+
.getSchema(schemaData);
123+
List<Error> errors = schema.validate("NaN", InputFormat.JSON);
124+
assertEquals(0, errors.size());
125+
errors = schema.validate("Infinity", InputFormat.JSON);
126+
assertEquals(0, errors.size());
127+
errors = schema.validate("-Infinity", InputFormat.JSON);
128+
assertEquals(0, errors.size());
129+
}
108130
}

0 commit comments

Comments
 (0)