Skip to content

Commit 284e952

Browse files
fduttonFaron Dutton
andauthored
Produces validation messages when oneOf has no valid schemas. (#720)
Corrects #678 #516 #197 Co-authored-by: Faron Dutton <[email protected]>
1 parent 6cea2ca commit 284e952

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

42 files changed

+965
-1065
lines changed

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

Lines changed: 12 additions & 193 deletions
Original file line numberDiff line numberDiff line change
@@ -21,112 +21,24 @@
2121
import org.slf4j.LoggerFactory;
2222

2323
import java.util.*;
24-
import java.util.stream.Collectors;
2524

2625
public class OneOfValidator extends BaseJsonValidator {
2726
private static final Logger logger = LoggerFactory.getLogger(OneOfValidator.class);
2827

29-
private final List<ShortcutValidator> schemas = new ArrayList<ShortcutValidator>();
30-
31-
private static class ShortcutValidator {
32-
private final JsonSchema schema;
33-
private final Map<String, String> constants;
34-
35-
ShortcutValidator(JsonNode schemaNode, JsonSchema parentSchema,
36-
ValidationContext validationContext, JsonSchema schema) {
37-
JsonNode refNode = schemaNode.get(ValidatorTypeCode.REF.getValue());
38-
JsonSchema resolvedRefSchema = refNode != null && refNode.isTextual() ? RefValidator.getRefSchema(parentSchema, validationContext, refNode.textValue()).getSchema() : null;
39-
this.constants = extractConstants(schemaNode, resolvedRefSchema);
40-
this.schema = schema;
41-
}
42-
43-
private Map<String, String> extractConstants(JsonNode schemaNode, JsonSchema resolvedRefSchema) {
44-
Map<String, String> refMap = resolvedRefSchema != null ? extractConstants(resolvedRefSchema.getSchemaNode()) : Collections.<String, String>emptyMap();
45-
Map<String, String> schemaMap = extractConstants(schemaNode);
46-
if (refMap.isEmpty()) {
47-
return schemaMap;
48-
}
49-
if (schemaMap.isEmpty()) {
50-
return refMap;
51-
}
52-
Map<String, String> joined = new HashMap<String, String>();
53-
joined.putAll(schemaMap);
54-
joined.putAll(refMap);
55-
return joined;
56-
}
57-
58-
private Map<String, String> extractConstants(JsonNode schemaNode) {
59-
Map<String, String> result = new HashMap<String, String>();
60-
if (!schemaNode.isObject()) {
61-
return result;
62-
}
63-
64-
JsonNode propertiesNode = schemaNode.get("properties");
65-
if (propertiesNode == null || !propertiesNode.isObject()) {
66-
return result;
67-
}
68-
Iterator<String> fit = propertiesNode.fieldNames();
69-
while (fit.hasNext()) {
70-
String fieldName = fit.next();
71-
JsonNode jsonNode = propertiesNode.get(fieldName);
72-
String constantFieldValue = getConstantFieldValue(jsonNode);
73-
if (constantFieldValue != null && !constantFieldValue.isEmpty()) {
74-
result.put(fieldName, constantFieldValue);
75-
}
76-
}
77-
return result;
78-
}
79-
80-
private String getConstantFieldValue(JsonNode jsonNode) {
81-
if (jsonNode == null || !jsonNode.isObject() || !jsonNode.has("enum")) {
82-
return null;
83-
}
84-
JsonNode enumNode = jsonNode.get("enum");
85-
if (enumNode == null || !enumNode.isArray()) {
86-
return null;
87-
}
88-
if (enumNode.size() != 1) {
89-
return null;
90-
}
91-
JsonNode valueNode = enumNode.get(0);
92-
if (valueNode == null || !valueNode.isTextual()) {
93-
return null;
94-
}
95-
return valueNode.textValue();
96-
}
97-
98-
public boolean allConstantsMatch(JsonNode node) {
99-
for (Map.Entry<String, String> e : constants.entrySet()) {
100-
JsonNode valueNode = node.get(e.getKey());
101-
if (valueNode != null && valueNode.isTextual()) {
102-
boolean match = e.getValue().equals(valueNode.textValue());
103-
if (!match) {
104-
return false;
105-
}
106-
}
107-
}
108-
return true;
109-
}
110-
111-
private JsonSchema getSchema() {
112-
return schema;
113-
}
114-
115-
}
28+
private final List<JsonSchema> schemas = new ArrayList<>();
11629

11730
public OneOfValidator(String schemaPath, JsonNode schemaNode, JsonSchema parentSchema, ValidationContext validationContext) {
11831
super(schemaPath, schemaNode, parentSchema, ValidatorTypeCode.ONE_OF, validationContext);
11932
int size = schemaNode.size();
12033
for (int i = 0; i < size; i++) {
12134
JsonNode childNode = schemaNode.get(i);
122-
JsonSchema childSchema = new JsonSchema(validationContext, schemaPath + "/" + i, parentSchema.getCurrentUri(), childNode, parentSchema);
123-
schemas.add(new ShortcutValidator(childNode, parentSchema, validationContext, childSchema));
35+
schemas.add(new JsonSchema(validationContext, schemaPath + "/" + i, parentSchema.getCurrentUri(), childNode, parentSchema));
12436
}
12537
parseErrorCode(getValidatorType().getErrorCodeKey());
12638
}
12739

12840
public Set<ValidationMessage> validate(JsonNode node, JsonNode rootNode, String at) {
129-
Set<ValidationMessage> errors = new LinkedHashSet<ValidationMessage>();
41+
Set<ValidationMessage> errors = new LinkedHashSet<>();
13042

13143
// As oneOf might contain multiple schemas take a backup of evaluatedProperties.
13244
Set<String> backupEvaluatedProperties = CollectorContext.getInstance().copyEvaluatedProperties();
@@ -143,28 +55,13 @@ public Set<ValidationMessage> validate(JsonNode node, JsonNode rootNode, String
14355
state.setComplexValidator(true);
14456

14557
int numberOfValidSchema = 0;
146-
Set<ValidationMessage> childErrors = new LinkedHashSet<ValidationMessage>();
147-
// validate that only a single element has been received in the oneOf node
148-
// validation should not continue, as it contradicts the oneOf requirement of only one
149-
// if(node.isObject() && node.size()>1) {
150-
// errors = Collections.singleton(buildValidationMessage(at, ""));
151-
// return Collections.unmodifiableSet(errors);
152-
// }
58+
Set<ValidationMessage> childErrors = new LinkedHashSet<>();
15359

154-
for (ShortcutValidator validator : schemas) {
60+
for (JsonSchema schema : schemas) {
15561
Set<ValidationMessage> schemaErrors = null;
15662
// Reset state in case the previous validator did not match
15763
state.setMatchedNode(true);
15864

159-
//This prevents from collecting all the error messages in proper format.
160-
/* if (!validator.allConstantsMatch(node)) {
161-
// take a shortcut: if there is any constant that does not match,
162-
// we can bail out of the validation
163-
continue;
164-
}*/
165-
166-
// get the current validator
167-
JsonSchema schema = validator.schema;
16865
if (!state.isWalkEnabled()) {
16966
schemaErrors = schema.validate(node, rootNode, at);
17067
} else {
@@ -188,37 +85,15 @@ public Set<ValidationMessage> validate(JsonNode node, JsonNode rootNode, String
18885

18986
childErrors.addAll(schemaErrors);
19087
}
191-
Set<ValidationMessage> childNotRequiredErrors = childErrors.stream().filter(error -> !ValidatorTypeCode.REQUIRED.getValue().equals(error.getType())).collect(Collectors.toSet());
19288

19389
// ensure there is always an "OneOf" error reported if number of valid schemas is not equal to 1.
194-
if (numberOfValidSchema > 1) {
195-
final ValidationMessage message = getMultiSchemasValidErrorMsg(at);
90+
if (numberOfValidSchema != 1) {
91+
ValidationMessage message = buildValidationMessage(at, Integer.toString(numberOfValidSchema));
19692
if (failFast) {
19793
throw new JsonSchemaException(message);
19894
}
19995
errors.add(message);
200-
}
201-
202-
// ensure there is always an "OneOf" error reported if number of valid schemas is not equal to 1.
203-
else if (numberOfValidSchema < 1) {
204-
if (!childNotRequiredErrors.isEmpty()) {
205-
childErrors = childNotRequiredErrors;
206-
}
207-
if (!childErrors.isEmpty()) {
208-
if (childErrors.size() > 1) {
209-
Set<ValidationMessage> notAdditionalPropertiesOnly = new LinkedHashSet<>(childErrors.stream()
210-
.filter((ValidationMessage validationMessage) -> !ValidatorTypeCode.ADDITIONAL_PROPERTIES.getValue().equals(validationMessage.getType()))
211-
.sorted((vm1, vm2) -> compareValidationMessages(vm1, vm2))
212-
.collect(Collectors.toList()));
213-
if (notAdditionalPropertiesOnly.size() > 0) {
214-
childErrors = notAdditionalPropertiesOnly;
215-
}
216-
}
217-
errors.addAll(childErrors);
218-
}
219-
if (failFast) {
220-
throw new JsonSchemaException(errors.toString());
221-
}
96+
errors.addAll(childErrors);
22297
}
22398

22499
// Make sure to signal parent handlers we matched
@@ -238,85 +113,29 @@ else if (numberOfValidSchema < 1) {
238113
}
239114
}
240115

241-
/**
242-
* Sort <code>ValidationMessage</code> by its type
243-
* @return
244-
*/
245-
private static int compareValidationMessages(ValidationMessage vm1, ValidationMessage vm2) {
246-
// ValidationMessage's type has smaller index in the list below has high priority
247-
final List<String> typeCodes = Arrays.asList(
248-
ValidatorTypeCode.TYPE.getValue(),
249-
ValidatorTypeCode.DATETIME.getValue(),
250-
ValidatorTypeCode.UUID.getValue(),
251-
ValidatorTypeCode.ID.getValue(),
252-
ValidatorTypeCode.EXCLUSIVE_MAXIMUM.getValue(),
253-
ValidatorTypeCode.EXCLUSIVE_MINIMUM.getValue(),
254-
ValidatorTypeCode.TRUE.getValue(),
255-
ValidatorTypeCode.FALSE.getValue(),
256-
ValidatorTypeCode.CONST.getValue(),
257-
ValidatorTypeCode.CONTAINS.getValue(),
258-
ValidatorTypeCode.PROPERTYNAMES.getValue()
259-
);
260-
261-
final int index1 = typeCodes.indexOf(vm1.getType());
262-
final int index2 = typeCodes.indexOf(vm2.getType());
263-
264-
if (index1 >= 0) {
265-
if (index2 >= 0) {
266-
return Integer.compare(index1, index2);
267-
} else {
268-
return -1;
269-
}
270-
} else {
271-
if (index2 >= 0) {
272-
return 1;
273-
} else {
274-
return vm1.getCode().compareTo(vm2.getCode());
275-
}
276-
}
277-
}
278-
279116
private void resetValidatorState() {
280117
ValidatorState state = (ValidatorState) CollectorContext.getInstance().get(ValidatorState.VALIDATOR_STATE_KEY);
281118
state.setComplexValidator(false);
282119
state.setMatchedNode(true);
283120
}
284121

285-
public List<JsonSchema> getChildSchemas() {
286-
List<JsonSchema> childJsonSchemas = new ArrayList<JsonSchema>();
287-
for (ShortcutValidator shortcutValidator: schemas ) {
288-
childJsonSchemas.add(shortcutValidator.getSchema());
289-
}
290-
return childJsonSchemas;
291-
}
292-
293122
@Override
294123
public Set<ValidationMessage> walk(JsonNode node, JsonNode rootNode, String at, boolean shouldValidateSchema) {
295124
HashSet<ValidationMessage> validationMessages = new LinkedHashSet<ValidationMessage>();
296125
if (shouldValidateSchema) {
297126
validationMessages.addAll(validate(node, rootNode, at));
298127
} else {
299-
for (ShortcutValidator validator : schemas) {
300-
validator.schema.walk(node, rootNode, at , shouldValidateSchema);
128+
for (JsonSchema schema : schemas) {
129+
schema.walk(node, rootNode, at, shouldValidateSchema);
301130
}
302131
}
303132
return validationMessages;
304133
}
305134

306-
private ValidationMessage getMultiSchemasValidErrorMsg(String at){
307-
String msg="";
308-
for(ShortcutValidator schema: schemas){
309-
String schemaValue = schema.getSchema().getSchemaNode().toString();
310-
msg = msg.concat(schemaValue);
311-
}
312-
313-
return ValidationMessage.of(getValidatorType().getValue(), ValidatorTypeCode.ONE_OF, at, schemaPath, msg);
314-
}
315-
316135
@Override
317136
public void preloadJsonSchema() {
318-
for (final ShortcutValidator scValidator: schemas) {
319-
scValidator.getSchema().initializeValidators();
137+
for (JsonSchema schema: schemas) {
138+
schema.initializeValidators();
320139
}
321140
}
322141
}

src/main/resources/jsv-messages.properties

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -33,7 +33,7 @@ minimum = {0}: must have a minimum value of {1}
3333
multipleOf = {0}: must be multiple of {1}
3434
not = {0}: should not be valid to the schema {1}
3535
notAllowed = {0}.{1}: is not allowed but it is in the data
36-
oneOf = {0}: should be valid to one and only one of schema, but more than one are valid: {1}
36+
oneOf = {0}: should be valid to one and only one schema, but {1} are valid
3737
pattern = {0}: does not match the regex pattern {1}
3838
patternProperties = {0}: has some error with 'pattern properties'
3939
prefixItems = {0}[{1}]: no validator found at this index

src/main/resources/jsv-messages_de.properties

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -28,7 +28,7 @@ minimum = {0} muss mindestens den Wert {1} haben
2828
multipleOf = {0} muss ein Vielfaches von {1} sein
2929
not = {0} darf nicht gültig sein für das Schema {1}
3030
notAllowed = {0}.{1} ist nicht erlaubt und darf folglich nicht auftreten
31-
oneOf = {0} darf nur für ein einziges Schema gültig sein, aber mehr als ein Schema ist gültig: {1}
31+
oneOf = {0} sollte für genau ein Schema gültig sein, aber {1} sind gültig
3232
pattern = {0} stimmt nicht mit dem regulären Ausdruck {1} überein
3333
patternProperties = {0} stimmt nicht überein mit dem Format definiert in 'pattern properties'
3434
properties = {0}: Ein Fehler mit 'properties' ist aufgetreten

src/main/resources/jsv-messages_fa_IR.properties

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -28,7 +28,8 @@ minimum = {0}: باید حداقل {1} ویژگی داشته باشد
2828
multipleOf = {0}: باید مضرب از {1} باشد
2929
not = {0}: نباید برای طرحواره معتبر باشد {1}
3030
notAllowed = {0}.{1}: مجاز نیست اما در داده ها وجود دارد
31-
oneOf = {0}: باید برای یک و تنها یکی از طرحواره ها معتبر باشد، اما بیش از یکی معتبر است: {1}
31+
# needs to be re-worked by a native speaker
32+
#oneOf = {0}: باید برای یک و تنها یکی از طرحواره ها معتبر باشد، اما بیش از یکی معتبر است: {1}
3233
pattern = {0}: با الگوی regex مطابقت ندارد {1}
3334
patternProperties = {0}: دارای مقداری خطا با 'خواص الگو'
3435
prefixItems = {0}[{1}]: هیچ اعتبارسنجی در این فهرست یافت نشد

src/main/resources/jsv-messages_fr.properties

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -28,7 +28,7 @@ minimum = {0}: doit avoir une valeur minimale de {1}
2828
multipleOf = {0}: doit être un multiple de {1}
2929
not = {0}: ne doit pas être valide pour le schéma {1}
3030
notAllowed = {0}.{1} n'est pas autorisé mais est dans les données
31-
oneOf = {0}: devrait être valide pour un et un seul des schémas, mais plus d'un sont valides : {1}
31+
oneOf = {0}: doit être valide pour un et un seul schéma, mais {1} sont valides
3232
pattern = {0} ne correspond pas à l'expression régulière {1}
3333
patternProperties = {0}: a des erreurs avec 'pattern properties'
3434
properties = {0} : a une erreur avec 'properties'

src/main/resources/jsv-messages_it.properties

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -28,7 +28,7 @@ minimum={0}: deve avere un valore minimo di {1}
2828
multipleOf={0}: deve essere un multiplo di {1}
2929
not={0}: non dovrebbe essere valido per lo schema {1}
3030
notAllowed={0}.{1}: non è consentito ma è nel dato
31-
oneOf={0}: dovrebbe essere valido a uno e solo uno schema, ma più di uno sono validi: {1}
31+
oneOf={0}: dovrebbe essere valido per uno e un solo schema, ma {1} sono validi
3232
pattern={0}: non corrisponde alla regex {1}
3333
patternProperties={0}: ha qualche errore con ''pattern properties''
3434
prefixItems={0}[{1}]: nessun validatore trovato a quest''indice

0 commit comments

Comments
 (0)