Skip to content

Commit 84d8546

Browse files
authored
Fix issue #769 - Add minContains / maxContains correct keywords (#773)
* Fix issue #769 - Add minContains / maxContains correct keywords * Fix #769 - Remove useless import
1 parent 19011c3 commit 84d8546

File tree

12 files changed

+222
-27
lines changed

12 files changed

+222
-27
lines changed

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

Lines changed: 14 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -31,7 +31,7 @@ public abstract class BaseJsonValidator implements JsonValidator {
3131
protected JsonNode schemaNode;
3232
protected JsonSchema parentSchema;
3333
private final boolean suppressSubSchemaRetrieval;
34-
private final ValidatorTypeCode validatorType;
34+
private ValidatorTypeCode validatorType;
3535
private ErrorMessageType errorMessageType;
3636
protected ValidationContext validationContext;
3737
protected final boolean failFast;
@@ -134,8 +134,8 @@ protected void parseErrorCode(String errorCodeKey) {
134134
}
135135

136136
protected ValidationMessage buildValidationMessage(String at, String... arguments) {
137-
MessageFormat messageFormat = new MessageFormat(resourceBundle.getString(errorMessageType.getErrorCodeValue()));
138-
final ValidationMessage message = ValidationMessage.ofWithCustom(getValidatorType().getValue(), errorMessageType, messageFormat, customMessage, at, schemaPath, arguments);
137+
MessageFormat messageFormat = new MessageFormat(resourceBundle.getString(getErrorMessageType().getErrorCodeValue()));
138+
final ValidationMessage message = ValidationMessage.ofWithCustom(getValidatorType().getValue(), getErrorMessageType(), messageFormat, customMessage, at, schemaPath, arguments);
139139
if (failFast && !isPartOfOneOfMultipleType()) {
140140
throw new JsonSchemaException(message);
141141
}
@@ -145,7 +145,7 @@ protected ValidationMessage buildValidationMessage(String at, String... argument
145145
protected ValidationMessage constructValidationMessage(String messageKey, String at, String... arguments) {
146146
MessageFormat messageFormat = new MessageFormat(resourceBundle.getString(messageKey));
147147
final ValidationMessage message = new ValidationMessage.Builder()
148-
.code(errorMessageType.getErrorCode())
148+
.code(getErrorMessageType().getErrorCode())
149149
.path(at)
150150
.schemaPath(schemaPath)
151151
.arguments(arguments)
@@ -167,6 +167,16 @@ protected ValidatorTypeCode getValidatorType() {
167167
return validatorType;
168168
}
169169

170+
protected ErrorMessageType getErrorMessageType() {
171+
return errorMessageType;
172+
}
173+
174+
protected void updateValidatorType(ValidatorTypeCode validatorTypeCode) {
175+
validatorType = validatorTypeCode;
176+
errorMessageType = validatorTypeCode;
177+
parseErrorCode(validatorTypeCode.getErrorCodeKey());
178+
}
179+
170180
protected String getNodeFieldType() {
171181
JsonNode typeField = this.getParentSchema().getSchemaNode().get("type");
172182
if (typeField != null) {

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

Lines changed: 30 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -18,44 +18,47 @@
1818

1919
import com.fasterxml.jackson.databind.JsonNode;
2020
import com.networknt.schema.SpecVersion.VersionFlag;
21-
2221
import org.slf4j.Logger;
2322
import org.slf4j.LoggerFactory;
2423

2524
import java.util.Collection;
2625
import java.util.Collections;
26+
import java.util.Optional;
2727
import java.util.Set;
2828

29+
import static com.networknt.schema.VersionCode.MinV201909;
30+
2931
public class ContainsValidator extends BaseJsonValidator {
3032
private static final Logger logger = LoggerFactory.getLogger(ContainsValidator.class);
33+
private static final String CONTAINS_MAX = "contains.max";
34+
private static final String CONTAINS_MIN = "contains.min";
35+
private static final VersionFlag DEFAULT_VERSION = VersionFlag.V6;
36+
37+
private final JsonSchema schema;
38+
private final boolean isMinV201909;
3139

3240
private int min = 1;
3341
private int max = Integer.MAX_VALUE;
34-
private final JsonSchema schema;
35-
private final String messageKeyMax = "contains.max";
36-
private final String messageKeyMin;
3742

3843
public ContainsValidator(String schemaPath, JsonNode schemaNode, JsonSchema parentSchema, ValidationContext validationContext) {
3944
super(schemaPath, schemaNode, parentSchema, ValidatorTypeCode.CONTAINS, validationContext);
4045

4146
// Draft 6 added the contains keyword but maxContains and minContains first
4247
// appeared in Draft 2019-09 so the semantics of the validation changes
4348
// slightly.
44-
VersionFlag version = SpecVersionDetector.detectOptionalVersion(parentSchema.getSchemaNode()).orElse(VersionFlag.V6);
45-
this.messageKeyMin = VersionFlag.V6 == version || VersionFlag.V7 == version ? "contains" : "contains.min";
49+
isMinV201909 = MinV201909.getVersions().contains(SpecVersionDetector.detectOptionalVersion(validationContext.getMetaSchema().getUri()).orElse(DEFAULT_VERSION));
4650

4751
if (schemaNode.isObject() || schemaNode.isBoolean()) {
4852
this.schema = new JsonSchema(validationContext, getValidatorType().getValue(), parentSchema.getCurrentUri(), schemaNode, parentSchema);
4953

50-
JsonNode maxNode = parentSchema.getSchemaNode().get("maxContains");
51-
if (null != maxNode && maxNode.canConvertToExactIntegral()) {
52-
this.max = maxNode.intValue();
53-
}
54+
JsonNode parentSchemaNode = parentSchema.getSchemaNode();
55+
Optional.ofNullable(parentSchemaNode.get(ValidatorTypeCode.MAX_CONTAINS.getValue()))
56+
.filter(JsonNode::canConvertToExactIntegral)
57+
.ifPresent(node -> this.max = node.intValue());
5458

55-
JsonNode minNode = parentSchema.getSchemaNode().get("minContains");
56-
if (null != minNode && minNode.canConvertToExactIntegral()) {
57-
this.min = minNode.intValue();
58-
}
59+
Optional.ofNullable(parentSchemaNode.get(ValidatorTypeCode.MIN_CONTAINS.getValue()))
60+
.filter(JsonNode::canConvertToExactIntegral)
61+
.ifPresent(node -> this.min = node.intValue());
5962
} else {
6063
this.schema = null;
6164
}
@@ -83,25 +86,29 @@ public Set<ValidationMessage> validate(JsonNode node, JsonNode rootNode, String
8386
}
8487

8588
if (actual < this.min) {
86-
return boundsViolated(this.messageKeyMin, at, this.min);
89+
if(isMinV201909) {
90+
updateValidatorType(ValidatorTypeCode.MIN_CONTAINS);
91+
}
92+
return boundsViolated(isMinV201909 ? CONTAINS_MIN : ValidatorTypeCode.CONTAINS.getValue(), at, this.min);
8793
}
8894

8995
if (actual > this.max) {
90-
return boundsViolated(this.messageKeyMax, at, this.max);
96+
if(isMinV201909) {
97+
updateValidatorType(ValidatorTypeCode.MAX_CONTAINS);
98+
}
99+
return boundsViolated(isMinV201909 ? CONTAINS_MAX : ValidatorTypeCode.CONTAINS.getValue(), at, this.max);
91100
}
92101
}
93102

94103
return Collections.emptySet();
95104
}
96105

97-
private Set<ValidationMessage> boundsViolated(String messageKey, String at, int bounds) {
98-
return Collections.singleton(constructValidationMessage(messageKey, at, "" + bounds, this.schema.getSchemaNode().toString()));
99-
}
100-
101106
@Override
102107
public void preloadJsonSchema() {
103-
if (null != this.schema) {
104-
this.schema.initializeValidators();
105-
}
108+
Optional.ofNullable(this.schema).ifPresent(JsonSchema::initializeValidators);
109+
}
110+
111+
private Set<ValidationMessage> boundsViolated(String messageKey, String at, int bounds) {
112+
return Collections.singleton(constructValidationMessage(messageKey, at, String.valueOf(bounds), this.schema.getSchemaNode().toString()));
106113
}
107114
}
Lines changed: 69 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,69 @@
1+
package com.networknt.schema;
2+
3+
import com.fasterxml.jackson.databind.JsonNode;
4+
import com.fasterxml.jackson.databind.ObjectMapper;
5+
6+
import java.io.IOException;
7+
import java.io.InputStream;
8+
import java.text.MessageFormat;
9+
import java.util.Optional;
10+
import java.util.Set;
11+
12+
import static org.junit.jupiter.api.Assertions.assertTrue;
13+
14+
/**
15+
* Abstract class to use if the data JSON has a declared schema node at root level
16+
*
17+
* @see Issue769ContainsTest
18+
* @author vwuilbea
19+
*/
20+
public abstract class AbstractJsonSchemaTest {
21+
22+
private static final String SCHEMA = "$schema";
23+
private static final SpecVersion.VersionFlag DEFAULT_VERSION_FLAG = SpecVersion.VersionFlag.V202012;
24+
private static final String ASSERT_MSG_ERROR_CODE = "Validation result should contain {} error code";
25+
private static final String ASSERT_MSG_TYPE = "Validation result should contain {} type";
26+
27+
protected Set<ValidationMessage> validate(String dataPath) {
28+
JsonNode dataNode = getJsonNodeFromPath(dataPath);
29+
return getJsonSchemaFromDataNode(dataNode).validate(dataNode);
30+
}
31+
32+
protected void assertValidatorType(String filename, ValidatorTypeCode validatorTypeCode) {
33+
Set<ValidationMessage> validationMessages = validate(getDataTestFolder() + filename);
34+
35+
assertTrue(
36+
validationMessages.stream().anyMatch(vm -> validatorTypeCode.getErrorCode().equals(vm.getCode())),
37+
() -> MessageFormat.format(ASSERT_MSG_ERROR_CODE, validatorTypeCode.getErrorCode()));
38+
assertTrue(
39+
validationMessages.stream().anyMatch(vm -> validatorTypeCode.getValue().equals(vm.getType())),
40+
() -> MessageFormat.format(ASSERT_MSG_TYPE, validatorTypeCode.getValue()));
41+
}
42+
43+
protected abstract String getDataTestFolder();
44+
45+
private JsonSchema getJsonSchemaFromDataNode(JsonNode dataNode) {
46+
return Optional.ofNullable(dataNode.get(SCHEMA))
47+
.map(JsonNode::textValue)
48+
.map(this::getJsonNodeFromPath)
49+
.map(this::getJsonSchema)
50+
.orElseThrow(() -> new IllegalArgumentException("No schema found on document to test"));
51+
}
52+
53+
private JsonNode getJsonNodeFromPath(String dataPath) {
54+
InputStream dataInputStream = getClass().getResourceAsStream(dataPath);
55+
ObjectMapper mapper = new ObjectMapper();
56+
try {
57+
return mapper.readTree(dataInputStream);
58+
} catch(IOException e) {
59+
throw new RuntimeException(e);
60+
}
61+
}
62+
63+
private JsonSchema getJsonSchema(JsonNode schemaNode) {
64+
return JsonSchemaFactory
65+
.getInstance(SpecVersionDetector.detectOptionalVersion(schemaNode).orElse(DEFAULT_VERSION_FLAG))
66+
.getSchema(schemaNode);
67+
}
68+
69+
}
Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,39 @@
1+
package com.networknt.schema;
2+
3+
import org.junit.jupiter.api.Test;
4+
5+
/**
6+
* <p>Test class for issue <a href="https://github.com/networknt/json-schema-validator/issues/769">#769</a></p>
7+
* <p>This test class asserts that correct messages are returned for contains, minContains et maxContains keywords</p>
8+
* <p>Tested class: {@link ContainsValidator}</p>
9+
*
10+
* @author vwuilbea
11+
*/
12+
class Issue769ContainsTest extends AbstractJsonSchemaTest {
13+
14+
@Override
15+
protected String getDataTestFolder() {
16+
return "/data/contains/issue769/";
17+
}
18+
19+
@Test
20+
void shouldReturnMinContainsKeyword() {
21+
assertValidatorType("min-contains.json", ValidatorTypeCode.MIN_CONTAINS);
22+
}
23+
24+
@Test
25+
void shouldReturnContainsKeywordForMinContainsV7() {
26+
assertValidatorType("min-contains-v7.json", ValidatorTypeCode.CONTAINS);
27+
}
28+
29+
@Test
30+
void shouldReturnMaxContainsKeyword() {
31+
assertValidatorType("max-contains.json", ValidatorTypeCode.MAX_CONTAINS);
32+
}
33+
34+
@Test
35+
void shouldReturnContainsKeywordForMaxContainsV7() {
36+
assertValidatorType("max-contains-v7.json", ValidatorTypeCode.CONTAINS);
37+
}
38+
39+
}
Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
{
2+
"$schema": "/schema/contains/issue769/max-contains-v7.json",
3+
"myArray": [
4+
{"itemType": "type A"},
5+
{"itemType": "type A"}
6+
]
7+
}
Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
{
2+
"$schema": "/schema/contains/issue769/max-contains.json",
3+
"myArray": [
4+
{"itemType": "type A"},
5+
{"itemType": "type A"}
6+
]
7+
}
Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
{
2+
"$schema": "/schema/contains/issue769/min-contains-v7.json",
3+
"myArray": [{"itemType": "type A"}]
4+
}
Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
{
2+
"$schema": "/schema/contains/issue769/min-contains.json",
3+
"myArray": [{"itemType": "type A"}]
4+
}
Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
{
2+
"$schema": "http://json-schema.org/draft-07/schema#",
3+
"properties": {
4+
"myArray": {
5+
"type": "array",
6+
"maxContains": 1,
7+
"contains": {"properties": {"itemType": {"const": "type A"}}
8+
},
9+
"items": {"properties": {"itemType": {"type": "string"}}}
10+
}
11+
}
12+
}
Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
{
2+
"$schema": "https://json-schema.org/draft/2020-12/schema",
3+
"properties": {
4+
"myArray": {
5+
"type": "array",
6+
"maxContains": 1,
7+
"contains": {"properties": {"itemType": {"const": "type A"}}
8+
},
9+
"items": {"properties": {"itemType": {"type": "string"}}}
10+
}
11+
}
12+
}
Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
{
2+
"$schema": "http://json-schema.org/draft-07/schema#",
3+
"properties": {
4+
"myArray": {
5+
"type": "array",
6+
"minContains": 2,
7+
"contains": {"properties": {"itemType": {"const": "type A"}}
8+
},
9+
"items": {"properties": {"itemType": {"type": "string"}}}
10+
}
11+
}
12+
}
Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
{
2+
"$schema": "https://json-schema.org/draft/2020-12/schema",
3+
"properties": {
4+
"myArray": {
5+
"type": "array",
6+
"minContains": 2,
7+
"contains": {"properties": {"itemType": {"const": "type A"}}
8+
},
9+
"items": {"properties": {"itemType": {"type": "string"}}}
10+
}
11+
}
12+
}

0 commit comments

Comments
 (0)