Skip to content

Commit e60f81f

Browse files
authored
Refactor format validation (#958)
* Refactor format to allow more complex validation * Refactor * Refactor * Partial * Refactor * Refactor * Fix assertions behavior * Fix * Refactor date time validator * Refactor to allow for localized error messages * Add docs for unknown formats * Add tests
1 parent 32356e9 commit e60f81f

32 files changed

+918
-284
lines changed

doc/compatibility.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -150,7 +150,12 @@ This behavior can be overridden to generate assertions by setting the `setFormat
150150
| uri-template | 🚫 | 🟢 | 🟢 | 🟢 | 🟢 |
151151
| uuid | 🚫 | 🚫 | 🟢 | 🟢 | 🟢 |
152152

153+
##### Unknown Formats
154+
155+
When the format assertion vocabularies are used in a meta schema, in accordance to the specification, unknown formats will result in assertions. If the format assertion vocabularies are not used, unknown formats will only result in assertions if the assertions are enabled and if `setStrict("format", true)`.
156+
153157
##### Footnotes
154158
1. Note that the validation are only optional for some of the keywords/formats.
155159
2. Refer to the corresponding JSON schema for more information on whether the keyword/format is optional or not.
156160

161+

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

Lines changed: 123 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -16,23 +16,143 @@
1616

1717
package com.networknt.schema;
1818

19+
import java.util.Collections;
20+
import java.util.Set;
21+
import java.util.function.Supplier;
22+
23+
import com.fasterxml.jackson.databind.JsonNode;
24+
1925
/**
2026
* Used to implement the various formats for the format keyword.
27+
* <p>
28+
* Simple implementations need only override {@link #matches(ExecutionContext, String)}.
2129
*/
2230
public interface Format {
2331
/**
32+
* Gets the format name.
33+
*
2434
* @return the format name as referred to in a json schema format node.
2535
*/
2636
String getName();
37+
38+
/**
39+
* Gets the message key to use for the message.
40+
* <p>
41+
* See jsv-messages.properties.
42+
* <p>
43+
* The following are the arguments.<br>
44+
* {0} The instance location<br>
45+
* {1} The format name<br>
46+
* {2} The error message description<br>
47+
* {3} The input value
48+
*
49+
* @return the message key
50+
*/
51+
default String getMessageKey() {
52+
return "format";
53+
}
54+
55+
/**
56+
* Gets the error message description.
57+
* <p>
58+
* Deprecated. Override getMessageKey() and set the localized message in the
59+
* resource bundle or message source.
60+
*
61+
* @return the error message description.
62+
*/
63+
@Deprecated
64+
default String getErrorMessageDescription() {
65+
return "";
66+
}
67+
2768

2869
/**
2970
* Determines if the value matches the format.
30-
*
71+
* <p>
72+
* This should be implemented for string node types.
73+
*
3174
* @param executionContext the execution context
3275
* @param value to match
3376
* @return true if matches
3477
*/
35-
boolean matches(ExecutionContext executionContext, String value);
78+
default boolean matches(ExecutionContext executionContext, String value) {
79+
return true;
80+
}
81+
82+
/**
83+
* Determines if the value matches the format.
84+
*
85+
* @param executionContext the execution context
86+
* @param validationContext the validation context
87+
* @param value to match
88+
* @return true if matches
89+
*/
90+
default boolean matches(ExecutionContext executionContext, ValidationContext validationContext, String value) {
91+
return matches(executionContext, value);
92+
}
93+
94+
/**
95+
* Determines if the value matches the format.
96+
*
97+
* @param executionContext the execution context
98+
* @param validationContext the validation context
99+
* @param value to match
100+
* @return true if matches
101+
*/
102+
default boolean matches(ExecutionContext executionContext, ValidationContext validationContext, JsonNode value) {
103+
JsonType nodeType = TypeFactory.getValueNodeType(value, validationContext.getConfig());
104+
if (nodeType != JsonType.STRING) {
105+
return true;
106+
}
107+
return matches(executionContext, validationContext, value.textValue());
108+
}
109+
110+
/**
111+
* Determines if the value matches the format.
112+
* <p>
113+
* This can be implemented for non-string node types.
114+
*
115+
* @param executionContext the execution context
116+
* @param validationContext the validation context
117+
* @param node the node
118+
* @param rootNode the root node
119+
* @param instanceLocation the instance location
120+
* @param assertionsEnabled if assertions are enabled
121+
* @param formatValidator the format validator
122+
* @return true if matches
123+
*/
124+
default boolean matches(ExecutionContext executionContext, ValidationContext validationContext, JsonNode node,
125+
JsonNode rootNode, JsonNodePath instanceLocation, boolean assertionsEnabled, FormatValidator formatValidator) {
126+
return matches(executionContext, validationContext, node);
127+
}
36128

37-
String getErrorMessageDescription();
129+
/**
130+
* Validates the format.
131+
* <p>
132+
* This is the most flexible method to implement.
133+
*
134+
* @param executionContext the execution context
135+
* @param validationContext the validation context
136+
* @param node the node
137+
* @param rootNode the root node
138+
* @param instanceLocation the instance locaiton
139+
* @param assertionsEnabled if assertions are enabled
140+
* @param message the message builder
141+
* @param formatValidator the format validator
142+
* @return the messages
143+
*/
144+
default Set<ValidationMessage> validate(ExecutionContext executionContext, ValidationContext validationContext,
145+
JsonNode node, JsonNode rootNode, JsonNodePath instanceLocation, boolean assertionsEnabled,
146+
Supplier<MessageSourceValidationMessage.Builder> message,
147+
FormatValidator formatValidator) {
148+
if (assertionsEnabled) {
149+
if (!matches(executionContext, validationContext, node, rootNode, instanceLocation, assertionsEnabled,
150+
formatValidator)) {
151+
return Collections
152+
.singleton(message.get()
153+
.arguments(this.getName(), this.getErrorMessageDescription(), node.asText()).build());
154+
}
155+
}
156+
return Collections.emptySet();
157+
}
38158
}

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

Lines changed: 18 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -17,23 +17,31 @@
1717
package com.networknt.schema;
1818

1919
import com.fasterxml.jackson.databind.JsonNode;
20-
import com.networknt.schema.format.DateTimeValidator;
21-
import com.networknt.schema.format.DurationFormat;
2220

2321
import java.util.Collection;
2422
import java.util.Collections;
2523
import java.util.Map;
2624

25+
/**
26+
* Format Keyword.
27+
*/
2728
public class FormatKeyword implements Keyword {
28-
private static final String DATE_TIME = "date-time";
29-
private static final String DURATION = "duration";
30-
31-
private final ValidatorTypeCode type;
29+
private final String value;
30+
private final ErrorMessageType errorMessageType;
3231
private final Map<String, Format> formats;
32+
33+
public FormatKeyword(Map<String, Format> formats) {
34+
this(ValidatorTypeCode.FORMAT, formats);
35+
}
3336

3437
public FormatKeyword(ValidatorTypeCode type, Map<String, Format> formats) {
35-
this.type = type;
38+
this(type.getValue(), type, formats);
39+
}
40+
41+
public FormatKeyword(String value, ErrorMessageType errorMessageType, Map<String, Format> formats) {
42+
this.value = value;
3643
this.formats = formats;
44+
this.errorMessageType = errorMessageType;
3745
}
3846

3947
Collection<Format> getFormats() {
@@ -46,27 +54,13 @@ public JsonValidator newValidator(SchemaLocation schemaLocation, JsonNodePath ev
4654
if (schemaNode != null && schemaNode.isTextual()) {
4755
String formatName = schemaNode.textValue();
4856
format = this.formats.get(formatName);
49-
if (format != null) {
50-
return new FormatValidator(schemaLocation, evaluationPath, schemaNode, parentSchema, validationContext, format, type);
51-
}
52-
53-
switch (formatName) {
54-
case DURATION:
55-
format = new DurationFormat(validationContext.getConfig().isStrict(DURATION));
56-
break;
57-
58-
case DATE_TIME: {
59-
ValidatorTypeCode typeCode = ValidatorTypeCode.DATETIME;
60-
return new DateTimeValidator(schemaLocation, evaluationPath, schemaNode, parentSchema, validationContext, typeCode);
61-
}
62-
}
6357
}
64-
65-
return new FormatValidator(schemaLocation, evaluationPath, schemaNode, parentSchema, validationContext, format, this.type);
58+
return new FormatValidator(schemaLocation, evaluationPath, schemaNode, parentSchema, validationContext, format,
59+
errorMessageType, this);
6660
}
6761

6862
@Override
6963
public String getValue() {
70-
return this.type.getValue();
64+
return this.value;
7165
}
7266
}

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

Lines changed: 95 additions & 42 deletions
Original file line numberDiff line numberDiff line change
@@ -23,71 +23,124 @@
2323
import org.slf4j.LoggerFactory;
2424

2525
import java.util.Collections;
26-
import java.util.LinkedHashSet;
2726
import java.util.Set;
2827
import java.util.regex.PatternSyntaxException;
2928

29+
/**
30+
* Validator for Format.
31+
*/
3032
public class FormatValidator extends BaseFormatJsonValidator implements JsonValidator {
3133
private static final Logger logger = LoggerFactory.getLogger(FormatValidator.class);
3234

3335
private final Format format;
34-
35-
public FormatValidator(SchemaLocation schemaLocation, JsonNodePath evaluationPath, JsonNode schemaNode, JsonSchema parentSchema, ValidationContext validationContext, Format format, ValidatorTypeCode type) {
36-
super(schemaLocation, evaluationPath, schemaNode, parentSchema, type, validationContext);
36+
37+
public FormatValidator(SchemaLocation schemaLocation, JsonNodePath evaluationPath, JsonNode schemaNode,
38+
JsonSchema parentSchema, ValidationContext validationContext, Format format,
39+
ErrorMessageType errorMessageType, Keyword keyword) {
40+
super(schemaLocation, evaluationPath, schemaNode, parentSchema, errorMessageType, keyword, validationContext);
3741
this.format = format;
3842
}
3943

44+
public FormatValidator(SchemaLocation schemaLocation, JsonNodePath evaluationPath, JsonNode schemaNode,
45+
JsonSchema parentSchema, ValidationContext validationContext, Format format, ValidatorTypeCode type) {
46+
this(schemaLocation, evaluationPath, schemaNode, parentSchema, validationContext, format, type, type);
47+
}
48+
49+
/**
50+
* Gets the annotation value.
51+
*
52+
* @return the annotation value
53+
*/
54+
protected Object getAnnotationValue() {
55+
if (this.format != null) {
56+
return this.format.getName();
57+
}
58+
return this.schemaNode.isTextual() ? schemaNode.textValue() : null;
59+
}
60+
4061
public Set<ValidationMessage> validate(ExecutionContext executionContext, JsonNode node, JsonNode rootNode, JsonNodePath instanceLocation) {
4162
debug(logger, node, rootNode, instanceLocation);
42-
43-
if (format != null) {
44-
if (collectAnnotations(executionContext)) {
63+
/*
64+
* Annotations must be collected even if the format is unknown according to the specification.
65+
*/
66+
if (collectAnnotations(executionContext)) {
67+
Object annotationValue = getAnnotationValue();
68+
if (annotationValue != null) {
4569
putAnnotation(executionContext,
46-
annotation -> annotation.instanceLocation(instanceLocation).value(this.format.getName()));
70+
annotation -> annotation.instanceLocation(instanceLocation).value(annotationValue));
4771
}
4872
}
4973

50-
JsonType nodeType = TypeFactory.getValueNodeType(node, this.validationContext.getConfig());
51-
if (nodeType != JsonType.STRING) {
52-
return Collections.emptySet();
53-
}
54-
5574
boolean assertionsEnabled = isAssertionsEnabled(executionContext);
56-
Set<ValidationMessage> errors = new LinkedHashSet<>();
57-
if (format != null) {
58-
if(format.getName().equals("ipv6")) {
59-
if(!node.textValue().trim().equals(node.textValue())) {
60-
if (assertionsEnabled) {
61-
// leading and trailing spaces
62-
errors.add(message().instanceNode(node).instanceLocation(instanceLocation)
63-
.locale(executionContext.getExecutionConfig().getLocale())
64-
.failFast(executionContext.isFailFast())
65-
.arguments(format.getName(), format.getErrorMessageDescription()).build());
66-
}
67-
} else if(node.textValue().contains("%")) {
68-
if (assertionsEnabled) {
69-
// zone id is not part of the ipv6
70-
errors.add(message().instanceNode(node).instanceLocation(instanceLocation)
71-
.locale(executionContext.getExecutionConfig().getLocale())
72-
.arguments(format.getName(), format.getErrorMessageDescription()).build());
73-
}
74-
}
75-
}
75+
if (this.format != null) {
7676
try {
77-
if (!format.matches(executionContext, node.textValue())) {
78-
if (assertionsEnabled) {
79-
errors.add(message().instanceNode(node).instanceLocation(instanceLocation)
80-
.locale(executionContext.getExecutionConfig().getLocale())
81-
.arguments(format.getName(), format.getErrorMessageDescription()).build());
82-
}
83-
}
77+
return format.validate(executionContext, validationContext, node, rootNode, instanceLocation,
78+
assertionsEnabled,
79+
() -> this.message().instanceNode(node).instanceLocation(instanceLocation)
80+
.messageKey(format.getMessageKey())
81+
.locale(executionContext.getExecutionConfig().getLocale())
82+
.failFast(executionContext.isFailFast()),
83+
this);
8484
} catch (PatternSyntaxException pse) {
8585
// String is considered valid if pattern is invalid
86-
logger.error("Failed to apply pattern on {}: Invalid RE syntax [{}]", instanceLocation, format.getName(), pse);
86+
logger.error("Failed to apply pattern on {}: Invalid RE syntax [{}]", instanceLocation,
87+
format.getName(), pse);
88+
return Collections.emptySet();
8789
}
90+
} else {
91+
return validateUnknownFormat(executionContext, node, rootNode, instanceLocation);
8892
}
93+
}
8994

90-
return Collections.unmodifiableSet(errors);
95+
/**
96+
* When the Format-Assertion vocabulary is specified, implementations MUST fail upon encountering unknown formats.
97+
*
98+
* @param executionContext the execution context
99+
* @param node the node
100+
* @param rootNode the root node
101+
* @param instanceLocation the instance location
102+
* @return the messages
103+
*/
104+
protected Set<ValidationMessage> validateUnknownFormat(ExecutionContext executionContext,
105+
JsonNode node, JsonNode rootNode, JsonNodePath instanceLocation) {
106+
/*
107+
* Unknown formats should create an assertion if the vocab is specified
108+
* according to the specification.
109+
*/
110+
if (createUnknownFormatAssertions(executionContext) && this.schemaNode.isTextual()) {
111+
return Collections.singleton(message().instanceLocation(instanceLocation).instanceNode(node)
112+
.messageKey("format.unknown").arguments(schemaNode.textValue()).build());
113+
}
114+
return Collections.emptySet();
115+
}
116+
117+
/**
118+
* When the Format-Assertion vocabulary is specified, implementations MUST fail
119+
* upon encountering unknown formats.
120+
* <p>
121+
* Note that this is different from setting the setFormatAssertionsEnabled
122+
* configuration option.
123+
* <p>
124+
* The following logic will return true if the format assertions option is
125+
* turned on and strict is enabled (default false) or the format assertion
126+
* vocabulary is enabled.
127+
*
128+
* @param executionContext the execution context
129+
* @return true if format assertions should be generated
130+
*/
131+
protected boolean createUnknownFormatAssertions(ExecutionContext executionContext) {
132+
return (isAssertionsEnabled(executionContext) && isStrict(executionContext)) || (isFormatAssertionVocabularyEnabled());
91133
}
92134

135+
/**
136+
* Determines if strict handling.
137+
* <p>
138+
* Note that this defaults to false.
139+
*
140+
* @param executionContext the execution context
141+
* @return whether to perform strict handling
142+
*/
143+
protected boolean isStrict(ExecutionContext executionContext) {
144+
return this.validationContext.getConfig().isStrict(getKeyword(), Boolean.FALSE);
145+
}
93146
}

0 commit comments

Comments
 (0)