Skip to content

Commit c7e7ab4

Browse files
authored
Support type loose for multipleOf validator (#945)
1 parent 2795d79 commit c7e7ab4

File tree

2 files changed

+145
-16
lines changed

2 files changed

+145
-16
lines changed

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

Lines changed: 51 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,8 @@
1717
package com.networknt.schema;
1818

1919
import com.fasterxml.jackson.databind.JsonNode;
20+
import com.networknt.schema.utils.JsonNodeUtil;
21+
2022
import org.slf4j.Logger;
2123
import org.slf4j.LoggerFactory;
2224

@@ -30,33 +32,66 @@
3032
public class MultipleOfValidator extends BaseJsonValidator implements JsonValidator {
3133
private static final Logger logger = LoggerFactory.getLogger(MultipleOfValidator.class);
3234

33-
private double divisor = 0;
35+
private final BigDecimal divisor;
3436

35-
public MultipleOfValidator(SchemaLocation schemaLocation, JsonNodePath evaluationPath, JsonNode schemaNode, JsonSchema parentSchema, ValidationContext validationContext) {
37+
public MultipleOfValidator(SchemaLocation schemaLocation, JsonNodePath evaluationPath, JsonNode schemaNode,
38+
JsonSchema parentSchema, ValidationContext validationContext) {
3639
super(schemaLocation, evaluationPath, schemaNode, parentSchema, ValidatorTypeCode.MULTIPLE_OF, validationContext);
37-
if (schemaNode.isNumber()) {
38-
divisor = schemaNode.doubleValue();
39-
}
40+
this.divisor = getDivisor(schemaNode);
4041
}
4142

42-
public Set<ValidationMessage> validate(ExecutionContext executionContext, JsonNode node, JsonNode rootNode, JsonNodePath instanceLocation) {
43+
public Set<ValidationMessage> validate(ExecutionContext executionContext, JsonNode node, JsonNode rootNode,
44+
JsonNodePath instanceLocation) {
4345
debug(logger, node, rootNode, instanceLocation);
44-
45-
if (node.isNumber()) {
46-
double nodeValue = node.doubleValue();
47-
if (divisor != 0) {
48-
// convert to BigDecimal since double type is not accurate enough to do the division and multiple
49-
BigDecimal accurateDividend = node.isBigDecimal() ? node.decimalValue() : new BigDecimal(String.valueOf(nodeValue));
50-
BigDecimal accurateDivisor = new BigDecimal(String.valueOf(divisor));
51-
if (accurateDividend.divideAndRemainder(accurateDivisor)[1].abs().compareTo(BigDecimal.ZERO) > 0) {
46+
if (this.divisor != null) {
47+
BigDecimal dividend = getDividend(node);
48+
if (dividend != null) {
49+
if (dividend.divideAndRemainder(this.divisor)[1].abs().compareTo(BigDecimal.ZERO) > 0) {
5250
return Collections.singleton(message().instanceNode(node).instanceLocation(instanceLocation)
5351
.locale(executionContext.getExecutionConfig().getLocale())
54-
.failFast(executionContext.getExecutionConfig().isFailFast()).arguments(divisor).build());
52+
.failFast(executionContext.getExecutionConfig().isFailFast()).arguments(this.divisor)
53+
.build());
5554
}
5655
}
5756
}
58-
5957
return Collections.emptySet();
6058
}
6159

60+
/**
61+
* Gets the divisor to use.
62+
*
63+
* @param schemaNode the schema node
64+
* @return the divisor or null if the input is not correct
65+
*/
66+
protected BigDecimal getDivisor(JsonNode schemaNode) {
67+
if (schemaNode.isNumber()) {
68+
double divisor = schemaNode.doubleValue();
69+
if (divisor != 0) {
70+
// convert to BigDecimal since double type is not accurate enough to do the
71+
// division and multiple
72+
return schemaNode.isBigDecimal() ? schemaNode.decimalValue() : BigDecimal.valueOf(divisor);
73+
}
74+
}
75+
return null;
76+
}
77+
78+
/**
79+
* Gets the dividend to use.
80+
*
81+
* @param node the node
82+
* @return the dividend or null if the type is incorrect
83+
*/
84+
protected BigDecimal getDividend(JsonNode node) {
85+
if (node.isNumber()) {
86+
// convert to BigDecimal since double type is not accurate enough to do the
87+
// division and multiple
88+
return node.isBigDecimal() ? node.decimalValue() : BigDecimal.valueOf(node.doubleValue());
89+
} else if (this.validationContext.getConfig().isTypeLoose()
90+
&& JsonNodeUtil.isNumber(node, this.validationContext.getConfig())) {
91+
// handling for type loose
92+
return new BigDecimal(node.textValue());
93+
}
94+
return null;
95+
}
96+
6297
}
Lines changed: 94 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,94 @@
1+
/*
2+
* Copyright (c) 2024 the original author or authors.
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* http://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
package com.networknt.schema;
17+
18+
import static org.junit.jupiter.api.Assertions.assertEquals;
19+
20+
import java.util.Set;
21+
22+
import org.junit.jupiter.api.Test;
23+
24+
import com.networknt.schema.SpecVersion.VersionFlag;
25+
26+
/**
27+
* Test MultipleOfValidator validator.
28+
*/
29+
public class MultipleOfValidatorTest {
30+
String schemaData = "{" +
31+
" \"type\": \"object\"," +
32+
" \"properties\": {" +
33+
" \"value1\": {" +
34+
" \"type\": \"number\"," +
35+
" \"multipleOf\": 0.01" +
36+
" }," +
37+
" \"value2\": {" +
38+
" \"type\": \"number\"," +
39+
" \"multipleOf\": 0.01" +
40+
" }," +
41+
" \"value3\": {" +
42+
" \"type\": \"number\"," +
43+
" \"multipleOf\": 0.01" +
44+
" }" +
45+
" }" +
46+
"}";
47+
48+
@Test
49+
void test() {
50+
JsonSchemaFactory factory = JsonSchemaFactory.getInstance(VersionFlag.V202012);
51+
JsonSchema schema = factory.getSchema(schemaData);
52+
String inputData = "{\"value1\":123.892,\"value2\":123456.2934,\"value3\":123.123}";
53+
String validData = "{\"value1\":123.89,\"value2\":123456,\"value3\":123.010}";
54+
55+
Set<ValidationMessage> messages = schema.validate(inputData, InputFormat.JSON);
56+
assertEquals(3, messages.size());
57+
assertEquals(3, messages.stream().filter(m -> "multipleOf".equals(m.getType())).count());
58+
59+
messages = schema.validate(validData, InputFormat.JSON);
60+
assertEquals(0, messages.size());
61+
}
62+
63+
@Test
64+
void testTypeLoose() {
65+
JsonSchemaFactory factory = JsonSchemaFactory.getInstance(VersionFlag.V202012);
66+
JsonSchema schema = factory.getSchema(schemaData);
67+
68+
String inputData = "{\"value1\":\"123.892\",\"value2\":\"123456.2934\",\"value3\":123.123}";
69+
String validTypeLooseInputData = "{\"value1\":\"123.89\",\"value2\":\"123456.29\",\"value3\":123.12}";
70+
71+
// Without type loose this has 2 type and 1 multipleOf errors
72+
Set<ValidationMessage> messages = schema.validate(inputData, InputFormat.JSON);
73+
assertEquals(3, messages.size());
74+
assertEquals(2, messages.stream().filter(m -> "type".equals(m.getType())).count());
75+
assertEquals(1, messages.stream().filter(m -> "multipleOf".equals(m.getType())).count());
76+
77+
// 2 type errors
78+
messages = schema.validate(validTypeLooseInputData, InputFormat.JSON);
79+
assertEquals(2, messages.size());
80+
assertEquals(2, messages.stream().filter(m -> "type".equals(m.getType())).count());
81+
82+
// With type loose this has 3 multipleOf errors
83+
SchemaValidatorsConfig config = new SchemaValidatorsConfig();
84+
config.setTypeLoose(true);
85+
JsonSchema typeLoose = factory.getSchema(schemaData, config);
86+
messages = typeLoose.validate(inputData, InputFormat.JSON);
87+
assertEquals(3, messages.size());
88+
assertEquals(3, messages.stream().filter(m -> "multipleOf".equals(m.getType())).count());
89+
90+
// No errors
91+
messages = typeLoose.validate(validTypeLooseInputData, InputFormat.JSON);
92+
assertEquals(0, messages.size());
93+
}
94+
}

0 commit comments

Comments
 (0)