Skip to content

Commit f99034e

Browse files
authored
Lenient validation mode (#429)
## Summary Adds lenient validation mode for primitive values. In lenient mode, strings are accepted as numbers, booleans or nulls, if they are parseable. By default, lenient mode is turned off. ## Details Adding `PrimitiveValidationStrategy` enum as a feature switch for the lenient validation mode. Also adding appropriate builder method for `Validator` for setting it up. Changing signature of `ValidatingVisitor#passesTypeCheck()` so that instead of returning a boolean, it accepts a callback, which is either called or not, depending on if the type-check passed or not. Furthermore, if the validator runs in LENIENT mode, the passesTypeCheck() attempts to perform a conversion to the expected value, if that is possible. If it succeeds, then the `onPass` callback will be invoked with the converted value as the parameter. The following ValidatingVisitor subclasses are updated to call the new `passesTypeCheck()` method correctly: * `ArraySchemaValidatingVisitor` * `StringSchemaValidatingVisitor` * `NumberSchemaValidatingVisitor` * `ObjectSchemaValidatingVisitor` The string-to-other-primitive conversion is performed by the `StringToValueConverter` class. The methods of this class are copied from `org.json.JSONObject`. Although it would be possible to call `JSONObject#stringToValue()` from ValidatingVisitor#ifPassesTypeCheck()`, we can not do it, because `JSONObject#stringToValue()` does not exist in the android flavor of the org.json package, therefore on android it would throw a `NoSuchMethodError`. For that reason, these methods are copied to the everit-org/json-schema library, to make sure that they exist at run-time. Furthermore, this implementation accepts [all 22 boolean literals of YAML](https://yaml.org/type/bool.html) as valid booleans. Credits go to @nfrankel for pointing out this extensive boolean support in a recent LinkedIn post :)
1 parent 9512ec6 commit f99034e

File tree

14 files changed

+535
-142
lines changed

14 files changed

+535
-142
lines changed

README.md

Lines changed: 57 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@
1111
* [JSON report of the failures](#json-report-of-the-failures)
1212
* [ValidationListeners - Tracking the validation process](#validationlisteners---tracking-the-validation-process)
1313
* [Early failure mode](#early-failure-mode)
14+
* [Lenient mode](#lenient-mode)
1415
* [Default values](#default-values)
1516
* [RegExp implementations](#regexp-implementations)
1617
* [readOnly and writeOnly context](#readonly-and-writeonly-context)
@@ -276,6 +277,62 @@ validator.performValidation(schema, input);
276277
_Note: the `Validator` class is immutable and thread-safe, so you don't have to create a new one for each validation, it is enough
277278
to configure it only once._
278279

280+
## Lenient mode
281+
282+
In some cases, when validating numbers or booleans, it makes sense to accept string values that are parseable as such primitives, because
283+
any successive processing will also automatically parse these literals into proper numeric and logical values. Also, non-string primitive values are trivial to convert to strings, so why not to permit any json primitives as strings?
284+
285+
For example, let's take this schema:
286+
287+
```json
288+
{
289+
"properties": {
290+
"booleanProp": {
291+
"type": "boolean"
292+
},
293+
"integerProp": {
294+
"type": "integer"
295+
},
296+
"nullProp": {
297+
"type": "null"
298+
},
299+
"numberProp": {
300+
"type": "number"
301+
},
302+
"stringProp": {
303+
"type": "string"
304+
}
305+
}
306+
}
307+
```
308+
309+
The following JSON document fails to validate, although all of the strings could easily be converted into appropriate values:
310+
311+
```json
312+
{
313+
"numberProp": "12.34",
314+
"integerProp": "12",
315+
"booleanProp": "true",
316+
"nullProp": "null",
317+
"stringProp": 12.34
318+
}
319+
```
320+
321+
In this case, if you want the above instance to pass the validation against the schema, you need to use the lenient primitive validation configuration turned on. Example:
322+
323+
324+
```java
325+
import org.everit.json.schema.*;
326+
...
327+
Validator validator = Validator.builder()
328+
.primitiveValidationStrategry(PrimitiveValidationStrategy.LENIENT)
329+
.build();
330+
validator.performValidation(schema, input);
331+
```
332+
333+
_Note: in lenient parsing mode, [all 22 possible boolean literals](https://yaml.org/type/bool.html) will be accepted as logical values._
334+
335+
279336

280337
## Default values
281338

core/src/main/java/org/everit/json/schema/ArraySchemaValidatingVisitor.java

Lines changed: 10 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -14,8 +14,6 @@
1414

1515
class ArraySchemaValidatingVisitor extends Visitor {
1616

17-
private final Object subject;
18-
1917
private final ValidatingVisitor owner;
2018

2119
private JSONArray arraySubject;
@@ -24,18 +22,19 @@ class ArraySchemaValidatingVisitor extends Visitor {
2422

2523
private int subjectLength;
2624

27-
public ArraySchemaValidatingVisitor(Object subject, ValidatingVisitor owner) {
28-
this.subject = subject;
25+
public ArraySchemaValidatingVisitor(ValidatingVisitor owner) {
2926
this.owner = requireNonNull(owner, "owner cannot be null");
3027
}
3128

32-
@Override void visitArraySchema(ArraySchema arraySchema) {
33-
if (owner.passesTypeCheck(JSONArray.class, arraySchema.requiresArray(), arraySchema.isNullable())) {
34-
this.arraySubject = (JSONArray) subject;
35-
this.subjectLength = arraySubject.length();
36-
this.arraySchema = arraySchema;
37-
super.visitArraySchema(arraySchema);
38-
}
29+
@Override
30+
void visitArraySchema(ArraySchema arraySchema) {
31+
owner.ifPassesTypeCheck(JSONArray.class, arraySchema.requiresArray(), arraySchema.isNullable(),
32+
arraySubject -> {
33+
this.arraySubject = arraySubject;
34+
this.subjectLength = arraySubject.length();
35+
this.arraySchema = arraySchema;
36+
super.visitArraySchema(arraySchema);
37+
});
3938
}
4039

4140
@Override void visitMinItems(Integer minItems) {

core/src/main/java/org/everit/json/schema/NumberSchemaValidatingVisitor.java

Lines changed: 8 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -25,11 +25,14 @@ class NumberSchemaValidatingVisitor extends Visitor {
2525

2626
@Override
2727
void visitNumberSchema(NumberSchema numberSchema) {
28-
Class expectedType = numberSchema.requiresInteger() ? Integer.class : Number.class;
29-
if (owner.passesTypeCheck(expectedType, numberSchema.requiresInteger() || numberSchema.isRequiresNumber(), numberSchema.isNullable())) {
30-
this.numberSubject = ((Number) subject);
31-
super.visitNumberSchema(numberSchema);
32-
}
28+
Class<? extends Number> expectedType = numberSchema.requiresInteger() ? Integer.class : Number.class;
29+
boolean schemaRequiresType = numberSchema.requiresInteger() || numberSchema.isRequiresNumber();
30+
owner.ifPassesTypeCheck(expectedType, Number.class::cast, schemaRequiresType,
31+
numberSchema.isNullable(),
32+
numberSubject -> {
33+
this.numberSubject = numberSubject;
34+
super.visitNumberSchema(numberSchema);
35+
});
3336
}
3437

3538
@Override

core/src/main/java/org/everit/json/schema/ObjectSchemaValidatingVisitor.java

Lines changed: 18 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -14,8 +14,6 @@
1414

1515
class ObjectSchemaValidatingVisitor extends Visitor {
1616

17-
private final Object subject;
18-
1917
private JSONObject objSubject;
2018

2119
private ObjectSchema schema;
@@ -24,26 +22,27 @@ class ObjectSchemaValidatingVisitor extends Visitor {
2422

2523
private final ValidatingVisitor owner;
2624

27-
public ObjectSchemaValidatingVisitor(Object subject, ValidatingVisitor owner) {
28-
this.subject = requireNonNull(subject, "subject cannot be null");
25+
public ObjectSchemaValidatingVisitor(ValidatingVisitor owner) {
2926
this.owner = requireNonNull(owner, "owner cannot be null");
3027
}
3128

32-
@Override void visitObjectSchema(ObjectSchema objectSchema) {
33-
if (owner.passesTypeCheck(JSONObject.class, objectSchema.requiresObject(), objectSchema.isNullable())) {
34-
objSubject = (JSONObject) subject;
35-
objectSize = objSubject.length();
36-
this.schema = objectSchema;
37-
Object failureState = owner.getFailureState();
38-
Set<String> objSubjectKeys = null;
39-
if (objectSchema.hasDefaultProperty()) {
40-
objSubjectKeys = new HashSet<>(objSubject.keySet());
41-
}
42-
super.visitObjectSchema(objectSchema);
43-
if (owner.isFailureStateChanged(failureState) && objectSchema.hasDefaultProperty()) {
44-
objSubject.keySet().retainAll(objSubjectKeys);
45-
}
46-
}
29+
@Override
30+
void visitObjectSchema(ObjectSchema objectSchema) {
31+
owner.ifPassesTypeCheck(JSONObject.class, objectSchema.requiresObject(), objectSchema.isNullable(),
32+
objSubject -> {
33+
this.objSubject = objSubject;
34+
this.objectSize = objSubject.length();
35+
this.schema = objectSchema;
36+
Object failureState = owner.getFailureState();
37+
Set<String> objSubjectKeys = null;
38+
if (objectSchema.hasDefaultProperty()) {
39+
objSubjectKeys = new HashSet<>(objSubject.keySet());
40+
}
41+
super.visitObjectSchema(objectSchema);
42+
if (owner.isFailureStateChanged(failureState) && objectSchema.hasDefaultProperty()) {
43+
objSubject.keySet().retainAll(objSubjectKeys);
44+
}
45+
});
4746
}
4847

4948
@Override void visitRequiredPropertyName(String requiredPropName) {
Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
package org.everit.json.schema;
2+
3+
public enum PrimitiveValidationStrategy {
4+
STRICT, LENIENT
5+
}

core/src/main/java/org/everit/json/schema/StringSchemaValidatingVisitor.java

Lines changed: 18 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,8 @@
77

88
import org.everit.json.schema.regexp.Regexp;
99

10-
public class StringSchemaValidatingVisitor extends Visitor {
10+
public class StringSchemaValidatingVisitor
11+
extends Visitor {
1112

1213
private final Object subject;
1314

@@ -22,36 +23,42 @@ public StringSchemaValidatingVisitor(Object subject, ValidatingVisitor owner) {
2223
this.owner = requireNonNull(owner, "failureReporter cannot be null");
2324
}
2425

25-
@Override void visitStringSchema(StringSchema stringSchema) {
26-
if (owner.passesTypeCheck(String.class, stringSchema.requireString(), stringSchema.isNullable())) {
27-
stringSubject = (String) subject;
28-
stringLength = stringSubject.codePointCount(0, stringSubject.length());
29-
super.visitStringSchema(stringSchema);
30-
}
26+
@Override
27+
void visitStringSchema(StringSchema stringSchema) {
28+
owner.ifPassesTypeCheck(String.class, stringSchema.requireString(), stringSchema.isNullable(),
29+
stringSubject -> {
30+
this.stringSubject = stringSubject;
31+
this.stringLength = stringSubject.codePointCount(0, stringSubject.length());
32+
super.visitStringSchema(stringSchema);
33+
});
3134
}
3235

33-
@Override void visitMinLength(Integer minLength) {
36+
@Override
37+
void visitMinLength(Integer minLength) {
3438
if (minLength != null && stringLength < minLength.intValue()) {
3539
owner.failure("expected minLength: " + minLength + ", actual: "
3640
+ stringLength, "minLength");
3741
}
3842
}
3943

40-
@Override void visitMaxLength(Integer maxLength) {
44+
@Override
45+
void visitMaxLength(Integer maxLength) {
4146
if (maxLength != null && stringLength > maxLength.intValue()) {
4247
owner.failure("expected maxLength: " + maxLength + ", actual: "
4348
+ stringLength, "maxLength");
4449
}
4550
}
4651

47-
@Override void visitPattern(Regexp pattern) {
52+
@Override
53+
void visitPattern(Regexp pattern) {
4854
if (pattern != null && pattern.patternMatchingFailure(stringSubject).isPresent()) {
4955
String message = format("string [%s] does not match pattern %s", subject, pattern.toString());
5056
owner.failure(message, "pattern");
5157
}
5258
}
5359

54-
@Override void visitFormat(FormatValidator formatValidator) {
60+
@Override
61+
void visitFormat(FormatValidator formatValidator) {
5562
Optional<String> failure = formatValidator.validate(stringSubject);
5663
if (failure.isPresent()) {
5764
owner.failure(failure.get(), "format");

0 commit comments

Comments
 (0)