Skip to content

Commit c346e70

Browse files
committed
Merge pull request #14 from erosb/master
2 parents 22da969 + d8bacf2 commit c346e70

28 files changed

+1247
-528
lines changed

README.md

Lines changed: 89 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,7 @@ Add the following to your `pom.xml`:
1212
<dependency>
1313
<groupId>org.everit.json</groupId>
1414
<artifactId>org.everit.json.schema</artifactId>
15-
<version>1.0.0</version>
15+
<version>1.1.0</version>
1616
</dependency>
1717
```
1818

@@ -32,4 +32,92 @@ try (InputStream inputStream = getClass().getResourceAsStream("/path/to/your/sch
3232
}
3333
```
3434

35+
Investigating failures
36+
----------------------
37+
38+
Starting from version `1.1.0` the validator collects every schema violations (instead of failing immediately on the first
39+
one). Each failure is denoted by a JSON pointer, pointing from the root of the document to the violating part. If more
40+
than one schema violations have been detected, then a `ValidationException` will be thrown at the most common parent
41+
elements of the violations, and each separate violations can be obtained using the `ValidationException#getCausingExceptions()`
42+
method.
43+
44+
To demonstrate the above concepts, lets see an example. Lets consider the following schema:
45+
46+
```
47+
{
48+
"type" : "object",
49+
"properties" : {
50+
"rectangle" : {"$ref" : "#/definitions/Rectangle" }
51+
},
52+
"definitions" : {
53+
"size" : {
54+
"type" : "number",
55+
"minimum" : 0
56+
},
57+
"Rectangle" : {
58+
"type" : "object",
59+
"properties" : {
60+
"a" : {"$ref" : "#/definitions/size"},
61+
"b" : {"$ref" : "#/definitions/size"}
62+
}
63+
}
64+
}
65+
}
66+
```
67+
68+
The following JSON document has only one violation against the schema (since "a" cannot be negative):
69+
70+
```
71+
{
72+
"rectangle" : {
73+
"a" : -5,
74+
"b" : 5
75+
}
76+
}
77+
```
78+
79+
In this case the thrown `ValidationException` will point to `#/rectangle/a` and it won't contain sub-exceptions:
80+
81+
```
82+
try {
83+
schema.validate(rectangleSingleFailure);
84+
} catch (ValidationException e) {
85+
// prints #/rectangle/a: -5.0 is not higher or equal to 0
86+
System.out.println(e.getMessage());
87+
}
88+
```
89+
90+
91+
Now - to illustrate the way how multiple violations are handled - lets consider the following JSON document, where both
92+
the "a" and "b" properties violate the above schema:
93+
94+
```
95+
{
96+
"rectangle" : {
97+
"a" : -5,
98+
"b" : "asd"
99+
}
100+
}
101+
```
102+
103+
In this case the thrown `ValidationException` will point to `#/rectangle`, and it has 2 sub-exceptions, pointing to
104+
`#/rectangle/a` and `#/rectangle/b` :
105+
106+
```
107+
try {
108+
schema.validate(rectangleMultipleFailures);
109+
} catch (ValidationException e) {
110+
System.out.println(e.getMessage());
111+
e.getCausingExceptions().stream()
112+
.map(ValidationException::getMessage)
113+
.forEach(System.out::println);
114+
}
115+
```
116+
117+
This will print the following output:
118+
```
119+
#/rectangle: 2 schema violations found
120+
#/rectangle/a: -5.0 is not higher or equal to 0
121+
#/rectangle/b: expected type: Number, found: String
122+
```
35123

core/pom.xml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -29,7 +29,7 @@
2929

3030
<groupId>org.everit.json</groupId>
3131
<artifactId>org.everit.json.schema</artifactId>
32-
<version>1.0.1</version>
32+
<version>1.1.0</version>
3333

3434
<packaging>bundle</packaging>
3535

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

Lines changed: 62 additions & 36 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@
1919
import java.util.Collection;
2020
import java.util.List;
2121
import java.util.Objects;
22+
import java.util.Optional;
2223

2324
import org.json.JSONArray;
2425

@@ -66,16 +67,6 @@ public Builder addItemSchema(final Schema itemSchema) {
6667
return this;
6768
}
6869

69-
public Builder schemaOfAdditionalItems(final Schema schemaOfAdditionalItems) {
70-
this.schemaOfAdditionalItems = schemaOfAdditionalItems;
71-
return this;
72-
}
73-
74-
public Builder requiresArray(final boolean requiresArray) {
75-
this.requiresArray = requiresArray;
76-
return this;
77-
}
78-
7970
public Builder additionalItems(final boolean additionalItems) {
8071
this.additionalItems = additionalItems;
8172
return this;
@@ -101,6 +92,16 @@ public Builder minItems(final Integer minItems) {
10192
return this;
10293
}
10394

95+
public Builder requiresArray(final boolean requiresArray) {
96+
this.requiresArray = requiresArray;
97+
return this;
98+
}
99+
100+
public Builder schemaOfAdditionalItems(final Schema schemaOfAdditionalItems) {
101+
this.schemaOfAdditionalItems = schemaOfAdditionalItems;
102+
return this;
103+
}
104+
104105
public Builder uniqueItems(final boolean uniqueItems) {
105106
this.uniqueItems = uniqueItems;
106107
return this;
@@ -168,6 +169,19 @@ public Integer getMinItems() {
168169
return minItems;
169170
}
170171

172+
public Schema getSchemaOfAdditionalItems() {
173+
return schemaOfAdditionalItems;
174+
}
175+
176+
private Optional<ValidationException> ifFails(final Schema schema, final Object input) {
177+
try {
178+
schema.validate(input);
179+
return Optional.empty();
180+
} catch (ValidationException e) {
181+
return Optional.of(e);
182+
}
183+
}
184+
171185
public boolean needsUniqueItems() {
172186
return uniqueItems;
173187
}
@@ -176,78 +190,90 @@ public boolean permitsAdditionalItems() {
176190
return additionalItems;
177191
}
178192

179-
private void testItemCount(final JSONArray subject) {
193+
public boolean requiresArray() {
194+
return requiresArray;
195+
}
196+
197+
private Optional<ValidationException> testItemCount(final JSONArray subject) {
180198
int actualLength = subject.length();
181199
if (minItems != null && actualLength < minItems) {
182-
throw new ValidationException("expected minimum item count: " + minItems + ", found: "
183-
+ actualLength);
200+
return Optional.of(new ValidationException(this, "expected minimum item count: " + minItems
201+
+ ", found: " + actualLength));
184202
}
185203
if (maxItems != null && maxItems < actualLength) {
186-
throw new ValidationException("expected maximum item count: " + minItems + ", found: "
187-
+ actualLength);
204+
return Optional.of(new ValidationException(this, "expected maximum item count: " + minItems
205+
+ ", found: " + actualLength));
188206
}
207+
return Optional.empty();
189208
}
190209

191-
private void testItems(final JSONArray subject) {
210+
private List<ValidationException> testItems(final JSONArray subject) {
211+
List<ValidationException> rval = new ArrayList<>();
192212
if (allItemSchema != null) {
193213
for (int i = 0; i < subject.length(); ++i) {
194-
allItemSchema.validate(subject.get(i));
214+
int copyOfI = i; // i is not effectively final so we copy it
215+
ifFails(allItemSchema, subject.get(i))
216+
.map(exc -> exc.prepend(String.valueOf(copyOfI)))
217+
.ifPresent(rval::add);
195218
}
196219
} else if (itemSchemas != null) {
197220
if (!additionalItems && subject.length() > itemSchemas.size()) {
198-
throw new ValidationException(String.format("expected: [%d] array items, found: [%d]",
199-
itemSchemas.size(), subject.length()));
221+
rval.add(new ValidationException(this, String.format(
222+
"expected: [%d] array items, found: [%d]",
223+
itemSchemas.size(), subject.length())));
200224
}
201225
int itemValidationUntil = Math.min(subject.length(), itemSchemas.size());
202226
for (int i = 0; i < itemValidationUntil; ++i) {
203-
itemSchemas.get(i).validate(subject.get(i));
227+
int copyOfI = i; // i is not effectively final so we copy it
228+
ifFails(itemSchemas.get(i), subject.get(i))
229+
.map(exc -> exc.prepend(String.valueOf(copyOfI)))
230+
.ifPresent(rval::add);
204231
}
205232
if (schemaOfAdditionalItems != null) {
206233
for (int i = itemValidationUntil; i < subject.length(); ++i) {
207-
schemaOfAdditionalItems.validate(subject.get(i));
234+
int copyOfI = i; // i is not effectively final so we copy it
235+
ifFails(schemaOfAdditionalItems, subject.get(i))
236+
.map(exc -> exc.prepend(String.valueOf(copyOfI)))
237+
.ifPresent(rval::add);
208238
}
209239
}
210240
}
241+
return rval;
211242
}
212243

213-
private void testUniqueness(final JSONArray subject) {
244+
private Optional<ValidationException> testUniqueness(final JSONArray subject) {
214245
if (subject.length() == 0) {
215-
return;
246+
return Optional.empty();
216247
}
217248
Collection<Object> uniqueItems = new ArrayList<Object>(subject.length());
218249
for (int i = 0; i < subject.length(); ++i) {
219250
Object item = subject.get(i);
220251
for (Object contained : uniqueItems) {
221252
if (ObjectComparator.deepEquals(contained, item)) {
222-
throw new ValidationException("array items are not unique");
253+
return Optional.of(new ValidationException(this, "array items are not unique"));
223254
}
224255
}
225256
uniqueItems.add(item);
226257
}
258+
return Optional.empty();
227259
}
228260

229261
@Override
230262
public void validate(final Object subject) {
263+
List<ValidationException> failures = new ArrayList<>();
231264
if (!(subject instanceof JSONArray)) {
232265
if (requiresArray) {
233-
throw new ValidationException(JSONArray.class, subject);
266+
throw new ValidationException(this, JSONArray.class, subject);
234267
}
235268
} else {
236269
JSONArray arrSubject = (JSONArray) subject;
237-
testItemCount(arrSubject);
270+
testItemCount(arrSubject).ifPresent(failures::add);
238271
if (uniqueItems) {
239-
testUniqueness(arrSubject);
272+
testUniqueness(arrSubject).ifPresent(failures::add);
240273
}
241-
testItems(arrSubject);
274+
failures.addAll(testItems(arrSubject));
242275
}
243-
}
244-
245-
public boolean requiresArray() {
246-
return requiresArray;
247-
}
248-
249-
public Schema getSchemaOfAdditionalItems() {
250-
return schemaOfAdditionalItems;
276+
ValidationException.throwFor(this, failures);
251277
}
252278

253279
}

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

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -20,12 +20,10 @@
2020
*/
2121
public class BooleanSchema extends Schema {
2222

23-
public static final BooleanSchema INSTANCE = new BooleanSchema(builder());
24-
2523
/**
2624
* Builder class for {@link BooleanSchema}.
2725
*/
28-
public static class Builder extends Schema.Builder {
26+
public static class Builder extends Schema.Builder<BooleanSchema> {
2927

3028
@Override
3129
public BooleanSchema build() {
@@ -34,6 +32,8 @@ public BooleanSchema build() {
3432

3533
}
3634

35+
public static final BooleanSchema INSTANCE = new BooleanSchema(builder());
36+
3737
public static Builder builder() {
3838
return new Builder();
3939
}
@@ -45,7 +45,7 @@ public BooleanSchema(final Builder builder) {
4545
@Override
4646
public void validate(final Object subject) {
4747
if (!(subject instanceof Boolean)) {
48-
throw new ValidationException(Boolean.class, subject);
48+
throw new ValidationException(this, Boolean.class, subject);
4949
}
5050
}
5151

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

Lines changed: 18 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,11 @@ public static class Builder extends Schema.Builder<CombinedSchema> {
3333

3434
private Collection<Schema> subschemas = new ArrayList<>();
3535

36+
@Override
37+
public CombinedSchema build() {
38+
return new CombinedSchema(this);
39+
}
40+
3641
public Builder criterion(final ValidationCriterion criterion) {
3742
this.criterion = criterion;
3843
return this;
@@ -48,19 +53,6 @@ public Builder subschemas(final Collection<Schema> subschemas) {
4853
return this;
4954
}
5055

51-
@Override
52-
public CombinedSchema build() {
53-
return new CombinedSchema(this);
54-
}
55-
56-
}
57-
58-
public static Builder builder() {
59-
return new Builder();
60-
}
61-
62-
public static Builder builder(final Collection<Schema> subschemas) {
63-
return new Builder().subschemas(subschemas);
6456
}
6557

6658
/**
@@ -122,6 +114,14 @@ public static Builder anyOf(final Collection<Schema> schemas) {
122114
return builder(schemas).criterion(ANY_CRITERION);
123115
}
124116

117+
public static Builder builder() {
118+
return new Builder();
119+
}
120+
121+
public static Builder builder(final Collection<Schema> subschemas) {
122+
return new Builder().subschemas(subschemas);
123+
}
124+
125125
public static Builder oneOf(final Collection<Schema> schemas) {
126126
return builder(schemas).criterion(ONE_CRITERION);
127127
}
@@ -164,7 +164,11 @@ public void validate(final Object subject) {
164164
int matchingCount = (int) subschemas.stream()
165165
.filter(schema -> succeeds(schema, subject))
166166
.count();
167-
criterion.validate(subschemas.size(), matchingCount);
167+
try {
168+
criterion.validate(subschemas.size(), matchingCount);
169+
} catch (ValidationException e) {
170+
throw new ValidationException(this, e.getMessage());
171+
}
168172
}
169173

170174
}

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

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -65,11 +65,13 @@ public Set<Object> getPossibleValues() {
6565

6666
@Override
6767
public void validate(final Object subject) {
68-
possibleValues.stream()
68+
possibleValues
69+
.stream()
6970
.filter(val -> ObjectComparator.deepEquals(val, subject))
7071
.findAny()
7172
.orElseThrow(
72-
() -> new ValidationException(String.format("%s is not a valid enum value", subject)));
73+
() -> new ValidationException(this, String.format("%s is not a valid enum value",
74+
subject)));
7375
}
7476

7577
}

0 commit comments

Comments
 (0)