Skip to content

Commit ccebfeb

Browse files
authored
Merge pull request #139 from everit-org/nullable
Nullable support
2 parents 9bacceb + 6f98c0a commit ccebfeb

16 files changed

+294
-49
lines changed

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

Lines changed: 1 addition & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -30,11 +30,7 @@ public ArraySchemaValidatingVisitor(Object subject, ValidatingVisitor owner) {
3030
}
3131

3232
@Override void visitArraySchema(ArraySchema arraySchema) {
33-
if (!(subject instanceof JSONArray)) {
34-
if (arraySchema.requiresArray()) {
35-
owner.failure(JSONArray.class, subject);
36-
}
37-
} else {
33+
if (owner.passesTypeCheck(JSONArray.class, arraySchema.requiresArray(), arraySchema.isNullable())) {
3834
this.arraySubject = (JSONArray) subject;
3935
this.subjectLength = arraySubject.length();
4036
this.arraySchema = arraySchema;

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

Lines changed: 12 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -8,27 +8,23 @@ class NumberSchemaValidatingVisitor extends Visitor {
88

99
private final Object subject;
1010

11-
private final ValidationFailureReporter failureReporter;
11+
private final ValidatingVisitor owner;
1212

1313
private boolean exclusiveMinimum;
1414

1515
private boolean exclusiveMaximum;
1616

1717
private double numberSubject;
1818

19-
NumberSchemaValidatingVisitor(Object subject, ValidationFailureReporter failureReporter) {
19+
NumberSchemaValidatingVisitor(Object subject, ValidatingVisitor owner) {
2020
this.subject = subject;
21-
this.failureReporter = failureReporter;
21+
this.owner= owner;
2222
}
2323

2424
@Override void visitNumberSchema(NumberSchema numberSchema) {
25-
if (!(subject instanceof Number)) {
26-
if (numberSchema.isRequiresNumber()) {
27-
failureReporter.failure(Number.class, subject);
28-
}
29-
} else {
25+
if (owner.passesTypeCheck(Number.class, numberSchema.isRequiresNumber(), numberSchema.isNullable())) {
3026
if (!(subject instanceof Integer || subject instanceof Long) && numberSchema.requiresInteger()) {
31-
failureReporter.failure(Integer.class, subject);
27+
owner.failure(Integer.class, subject);
3228
} else {
3329
this.numberSubject = ((Number) subject).doubleValue();
3430
super.visitNumberSchema(numberSchema);
@@ -45,16 +41,16 @@ class NumberSchemaValidatingVisitor extends Visitor {
4541
return;
4642
}
4743
if (exclusiveMinimum && numberSubject <= minimum.doubleValue()) {
48-
failureReporter.failure(subject + " is not greater than " + minimum, "exclusiveMinimum");
44+
owner.failure(subject + " is not greater than " + minimum, "exclusiveMinimum");
4945
} else if (numberSubject < minimum.doubleValue()) {
50-
failureReporter.failure(subject + " is not greater or equal to " + minimum, "minimum");
46+
owner.failure(subject + " is not greater or equal to " + minimum, "minimum");
5147
}
5248
}
5349

5450
@Override void visitExclusiveMinimumLimit(Number exclusiveMinimumLimit) {
5551
if (exclusiveMinimumLimit != null) {
5652
if (numberSubject <= exclusiveMinimumLimit.doubleValue()) {
57-
failureReporter.failure(subject + " is not greater than " + exclusiveMinimumLimit, "exclusiveMinimum");
53+
owner.failure(subject + " is not greater than " + exclusiveMinimumLimit, "exclusiveMinimum");
5854
}
5955
}
6056
}
@@ -64,9 +60,9 @@ class NumberSchemaValidatingVisitor extends Visitor {
6460
return;
6561
}
6662
if (exclusiveMaximum && maximum.doubleValue() <= numberSubject) {
67-
failureReporter.failure(subject + " is not less than " + maximum, "exclusiveMaximum");
63+
owner.failure(subject + " is not less than " + maximum, "exclusiveMaximum");
6864
} else if (maximum.doubleValue() < numberSubject) {
69-
failureReporter.failure(subject + " is not less or equal to " + maximum, "maximum");
65+
owner.failure(subject + " is not less or equal to " + maximum, "maximum");
7066
}
7167
}
7268

@@ -77,7 +73,7 @@ class NumberSchemaValidatingVisitor extends Visitor {
7773
@Override void visitExclusiveMaximumLimit(Number exclusiveMaximumLimit) {
7874
if (exclusiveMaximumLimit != null) {
7975
if (numberSubject >= exclusiveMaximumLimit.doubleValue()) {
80-
failureReporter.failure(format("is not less than " + exclusiveMaximumLimit), "exclusiveMaximum");
76+
owner.failure(format("is not less than " + exclusiveMaximumLimit), "exclusiveMaximum");
8177
}
8278
}
8379
}
@@ -87,7 +83,7 @@ class NumberSchemaValidatingVisitor extends Visitor {
8783
BigDecimal remainder = BigDecimal.valueOf(numberSubject).remainder(
8884
BigDecimal.valueOf(multipleOf.doubleValue()));
8985
if (remainder.compareTo(BigDecimal.ZERO) != 0) {
90-
failureReporter.failure(subject + " is not a multiple of " + multipleOf, "multipleOf");
86+
owner.failure(subject + " is not a multiple of " + multipleOf, "multipleOf");
9187
}
9288
}
9389
}

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

Lines changed: 1 addition & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -28,11 +28,7 @@ public ObjectSchemaValidatingVisitor(Object subject, ValidatingVisitor owner) {
2828
}
2929

3030
@Override void visitObjectSchema(ObjectSchema objectSchema) {
31-
if (!(subject instanceof JSONObject)) {
32-
if (objectSchema.requiresObject()) {
33-
owner.failure(JSONObject.class, subject);
34-
}
35-
} else {
31+
if (owner.passesTypeCheck(JSONObject.class, objectSchema.requiresObject(), objectSchema.isNullable())) {
3632
objSubject = (JSONObject) subject;
3733
objectSize = objSubject.length();
3834
this.schema = objectSchema;

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

Lines changed: 18 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,8 @@ public abstract static class Builder<S extends Schema> {
3131

3232
private Object defaultValue;
3333

34+
private Boolean nullable = null;
35+
3436
public Builder<S> title(String title) {
3537
this.title = title;
3638
return this;
@@ -56,10 +58,15 @@ public Builder<S> defaultValue(Object defaultValue) {
5658
return this;
5759
}
5860

61+
public Builder<S> nullable(Boolean nullable) {
62+
this.nullable = nullable;
63+
return this;
64+
}
65+
5966
public abstract S build();
6067

61-
}
6268

69+
}
6370
private final String title;
6471

6572
private final String description;
@@ -70,6 +77,8 @@ public Builder<S> defaultValue(Object defaultValue) {
7077

7178
private final Object defaultValue;
7279

80+
private final Boolean nullable;
81+
7382
/**
7483
* Constructor.
7584
*
@@ -82,6 +91,7 @@ protected Schema(Builder<?> builder) {
8291
this.id = builder.id;
8392
this.schemaLocation = builder.schemaLocation;
8493
this.defaultValue = builder.defaultValue;
94+
this.nullable = builder.nullable;
8595
}
8696

8797
/**
@@ -154,15 +164,16 @@ public boolean equals(Object o) {
154164
Objects.equals(title, schema.title) &&
155165
Objects.equals(defaultValue, schema.defaultValue) &&
156166
Objects.equals(description, schema.description) &&
157-
Objects.equals(id, schema.id);
167+
Objects.equals(id, schema.id) &&
168+
Objects.equals(nullable, schema.nullable);
158169
} else {
159170
return false;
160171
}
161172
}
162173

163174
@Override
164175
public int hashCode() {
165-
return Objects.hash(title, description, id, defaultValue);
176+
return Objects.hash(title, description, id, defaultValue, nullable);
166177
}
167178

168179
public String getTitle() {
@@ -189,6 +200,9 @@ public boolean hasDefaultValue() {
189200
return this.defaultValue != null;
190201
}
191202

203+
public Boolean isNullable() {
204+
return nullable;
205+
}
192206
/**
193207
* Describes the instance as a JSONObject to {@code writer}.
194208
* <p>
@@ -206,6 +220,7 @@ public void describeTo(JSONPrinter writer) {
206220
writer.ifPresent("description", description);
207221
writer.ifPresent("id", id);
208222
writer.ifPresent("default", defaultValue);
223+
writer.ifPresent("nullable", nullable);
209224
describePropertiesTo(writer);
210225
writer.endObject();
211226
}

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

Lines changed: 9 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -14,19 +14,15 @@ public class StringSchemaValidatingVisitor extends Visitor {
1414

1515
private int stringLength;
1616

17-
private final ValidationFailureReporter failureReporter;
17+
private final ValidatingVisitor owner;
1818

19-
public StringSchemaValidatingVisitor(Object subject, ValidationFailureReporter failureReporter) {
19+
public StringSchemaValidatingVisitor(Object subject, ValidatingVisitor owner) {
2020
this.subject = subject;
21-
this.failureReporter = requireNonNull(failureReporter, "failureReporter cannot be null");
21+
this.owner = requireNonNull(owner, "failureReporter cannot be null");
2222
}
2323

2424
@Override void visitStringSchema(StringSchema stringSchema) {
25-
if (!(subject instanceof String)) {
26-
if (stringSchema.requireString()) {
27-
failureReporter.failure(String.class, subject);
28-
}
29-
} else {
25+
if (owner.passesTypeCheck(String.class, stringSchema.requireString(), stringSchema.isNullable())) {
3026
stringSubject = (String) subject;
3127
stringLength = stringSubject.codePointCount(0, stringSubject.length());
3228
super.visitStringSchema(stringSchema);
@@ -35,14 +31,14 @@ public StringSchemaValidatingVisitor(Object subject, ValidationFailureReporter f
3531

3632
@Override void visitMinLength(Integer minLength) {
3733
if (minLength != null && stringLength < minLength.intValue()) {
38-
failureReporter.failure("expected minLength: " + minLength + ", actual: "
34+
owner.failure("expected minLength: " + minLength + ", actual: "
3935
+ stringLength, "minLength");
4036
}
4137
}
4238

4339
@Override void visitMaxLength(Integer maxLength) {
4440
if (maxLength != null && stringLength > maxLength.intValue()) {
45-
failureReporter.failure("expected maxLength: " + maxLength + ", actual: "
41+
owner.failure("expected maxLength: " + maxLength + ", actual: "
4642
+ stringLength, "maxLength");
4743
}
4844
}
@@ -51,14 +47,15 @@ public StringSchemaValidatingVisitor(Object subject, ValidationFailureReporter f
5147
if (pattern != null && !pattern.matcher(stringSubject).find()) {
5248
String message = format("string [%s] does not match pattern %s",
5349
subject, pattern.pattern());
54-
failureReporter.failure(message, "pattern");
50+
owner.failure(message, "pattern");
5551
}
5652
}
5753

5854
@Override void visitFormat(FormatValidator formatValidator) {
5955
Optional<String> failure = formatValidator.validate(stringSubject);
6056
if (failure.isPresent()) {
61-
failureReporter.failure(failure.get(), "format");
57+
owner.failure(failure.get(), "format");
6258
}
6359
}
60+
6461
}

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

Lines changed: 23 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -19,9 +19,12 @@ private static boolean isNull(Object obj) {
1919

2020
private ValidationFailureReporter failureReporter;
2121

22-
ValidatingVisitor(Schema schema, Object subject) {
23-
this.subject = subject;
24-
this.failureReporter = new CollectingFailureReporter(schema);
22+
@Override
23+
void visit(Schema schema) {
24+
if (schema.isNullable() == Boolean.FALSE && isNull(subject)) {
25+
failureReporter.failure("value cannot be null", "nullable");
26+
}
27+
super.visit(schema);
2528
}
2629

2730
ValidatingVisitor(Object subject, ValidationFailureReporter failureReporter) {
@@ -30,7 +33,7 @@ private static boolean isNull(Object obj) {
3033
}
3134

3235
@Override void visitNumberSchema(NumberSchema numberSchema) {
33-
numberSchema.accept(new NumberSchemaValidatingVisitor(subject, failureReporter));
36+
numberSchema.accept(new NumberSchemaValidatingVisitor(subject, this));
3437
}
3538

3639
@Override void visitArraySchema(ArraySchema arraySchema) {
@@ -97,7 +100,7 @@ private static boolean isNull(Object obj) {
97100
}
98101

99102
@Override void visitStringSchema(StringSchema stringSchema) {
100-
stringSchema.accept(new StringSchemaValidatingVisitor(subject, failureReporter));
103+
stringSchema.accept(new StringSchemaValidatingVisitor(subject, this));
101104
}
102105

103106
@Override void visitCombinedSchema(CombinedSchema combinedSchema) {
@@ -148,4 +151,19 @@ void failure(ValidationException exc) {
148151
failureReporter.failure(exc);
149152
}
150153

154+
boolean passesTypeCheck(Class<?> expectedType, boolean schemaRequiresType, Boolean nullable) {
155+
if (isNull(subject)) {
156+
if (schemaRequiresType && nullable != Boolean.TRUE) {
157+
failureReporter.failure(expectedType, subject);
158+
}
159+
return false;
160+
}
161+
if (expectedType.isAssignableFrom(subject.getClass())) {
162+
return true;
163+
}
164+
if (schemaRequiresType) {
165+
failureReporter.failure(expectedType, subject);
166+
}
167+
return false;
168+
}
151169
}

core/src/main/java/org/everit/json/schema/loader/LoaderConfig.java

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,12 +25,20 @@ static LoaderConfig defaultV4Config() {
2525

2626
final boolean useDefaults;
2727

28+
final boolean nullableSupport;
29+
2830
LoaderConfig(SchemaClient httpClient, Map<String, FormatValidator> formatValidators,
2931
SpecificationVersion specVersion, boolean useDefaults) {
32+
this(httpClient, formatValidators, specVersion, useDefaults, false);
33+
}
34+
35+
LoaderConfig(SchemaClient httpClient, Map<String, FormatValidator> formatValidators,
36+
SpecificationVersion specVersion, boolean useDefaults, boolean nullableSupport) {
3037
this.httpClient = requireNonNull(httpClient, "httpClient cannot be null");
3138
this.formatValidators = requireNonNull(formatValidators, "formatValidators cannot be null");
3239
this.specVersion = requireNonNull(specVersion, "specVersion cannot be null");
3340
this.useDefaults = useDefaults;
41+
this.nullableSupport = nullableSupport;
3442
}
3543

3644
}

core/src/main/java/org/everit/json/schema/loader/SchemaLoader.java

Lines changed: 22 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -71,6 +71,8 @@ public static class SchemaLoaderBuilder {
7171

7272
boolean useDefaults = false;
7373

74+
private boolean nullableSupport = false;
75+
7476
/**
7577
* Registers a format validator with the name returned by {@link FormatValidator#formatName()}.
7678
*
@@ -188,6 +190,10 @@ public SchemaLoaderBuilder useDefaults(boolean useDefaults) {
188190
return this;
189191
}
190192

193+
public SchemaLoaderBuilder nullableSupport(boolean nullableSupport) {
194+
this.nullableSupport = nullableSupport;
195+
return this;
196+
}
191197
}
192198

193199
private static final List<String> NUMBER_SCHEMA_PROPS = asList("minimum", "maximum",
@@ -266,7 +272,11 @@ public SchemaLoader(SchemaLoaderBuilder builder) {
266272
specVersion = SpecificationVersion.getByMetaSchemaUrl((String) schemaValue);
267273
}
268274
}
269-
this.config = new LoaderConfig(builder.httpClient, builder.formatValidators, specVersion, builder.useDefaults);
275+
this.config = new LoaderConfig(builder.httpClient,
276+
builder.formatValidators,
277+
specVersion,
278+
builder.useDefaults,
279+
builder.nullableSupport);
270280
this.ls = new LoadingState(config,
271281
builder.pointerSchemas,
272282
builder.rootSchemaJson == null ? builder.schemaJson : builder.rootSchemaJson,
@@ -359,14 +369,24 @@ private Schema.Builder loadSchemaObject(JsonObject o) {
359369
}
360370
});
361371
}
372+
loadCommonSchemaProperties(builder);
373+
return builder;
374+
}
375+
376+
private void loadCommonSchemaProperties(Schema.Builder builder) {
362377
ls.schemaJson().maybe(config.specVersion.idKeyword()).map(JsonValue::requireString).ifPresent(builder::id);
363378
ls.schemaJson().maybe("title").map(JsonValue::requireString).ifPresent(builder::title);
364379
ls.schemaJson().maybe("description").map(JsonValue::requireString).ifPresent(builder::description);
380+
if (config.nullableSupport) {
381+
builder.nullable(ls.schemaJson()
382+
.maybe("nullable")
383+
.map(JsonValue::requireBoolean)
384+
.orElse(Boolean.FALSE));
385+
}
365386
if (config.useDefaults) {
366387
ls.schemaJson().maybe("default").map(JsonValue::deepToOrgJson).ifPresent(builder::defaultValue);
367388
}
368389
builder.schemaLocation(new JSONPointer(ls.pointerToCurrentObj).toURIFragment());
369-
return builder;
370390
}
371391

372392
/**

core/src/test/java/org/everit/json/schema/ArraySchemaTest.java

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -247,4 +247,10 @@ public void containedItemSchemaEmptyArr() {
247247
.expect();
248248
}
249249

250+
@Test
251+
public void requiresArray_nullable() {
252+
ArraySchema subject = ArraySchema.builder().requiresArray(true).nullable(true).build();
253+
subject.validate(JSONObject.NULL);
254+
}
255+
250256
}

0 commit comments

Comments
 (0)