Skip to content

Commit

Permalink
Merge pull request #14 from erosb/master
Browse files Browse the repository at this point in the history
  • Loading branch information
erosb committed Nov 26, 2015
2 parents 22da969 + d8bacf2 commit c346e70
Show file tree
Hide file tree
Showing 28 changed files with 1,247 additions and 528 deletions.
90 changes: 89 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ Add the following to your `pom.xml`:
<dependency>
<groupId>org.everit.json</groupId>
<artifactId>org.everit.json.schema</artifactId>
<version>1.0.0</version>
<version>1.1.0</version>
</dependency>
```

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

Investigating failures
----------------------

Starting from version `1.1.0` the validator collects every schema violations (instead of failing immediately on the first
one). Each failure is denoted by a JSON pointer, pointing from the root of the document to the violating part. If more
than one schema violations have been detected, then a `ValidationException` will be thrown at the most common parent
elements of the violations, and each separate violations can be obtained using the `ValidationException#getCausingExceptions()`
method.

To demonstrate the above concepts, lets see an example. Lets consider the following schema:

```
{
"type" : "object",
"properties" : {
"rectangle" : {"$ref" : "#/definitions/Rectangle" }
},
"definitions" : {
"size" : {
"type" : "number",
"minimum" : 0
},
"Rectangle" : {
"type" : "object",
"properties" : {
"a" : {"$ref" : "#/definitions/size"},
"b" : {"$ref" : "#/definitions/size"}
}
}
}
}
```

The following JSON document has only one violation against the schema (since "a" cannot be negative):

```
{
"rectangle" : {
"a" : -5,
"b" : 5
}
}
```

In this case the thrown `ValidationException` will point to `#/rectangle/a` and it won't contain sub-exceptions:

```
try {
schema.validate(rectangleSingleFailure);
} catch (ValidationException e) {
// prints #/rectangle/a: -5.0 is not higher or equal to 0
System.out.println(e.getMessage());
}
```


Now - to illustrate the way how multiple violations are handled - lets consider the following JSON document, where both
the "a" and "b" properties violate the above schema:

```
{
"rectangle" : {
"a" : -5,
"b" : "asd"
}
}
```

In this case the thrown `ValidationException` will point to `#/rectangle`, and it has 2 sub-exceptions, pointing to
`#/rectangle/a` and `#/rectangle/b` :

```
try {
schema.validate(rectangleMultipleFailures);
} catch (ValidationException e) {
System.out.println(e.getMessage());
e.getCausingExceptions().stream()
.map(ValidationException::getMessage)
.forEach(System.out::println);
}
```

This will print the following output:
```
#/rectangle: 2 schema violations found
#/rectangle/a: -5.0 is not higher or equal to 0
#/rectangle/b: expected type: Number, found: String
```

2 changes: 1 addition & 1 deletion core/pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,7 @@

<groupId>org.everit.json</groupId>
<artifactId>org.everit.json.schema</artifactId>
<version>1.0.1</version>
<version>1.1.0</version>

<packaging>bundle</packaging>

Expand Down
98 changes: 62 additions & 36 deletions core/src/main/java/org/everit/json/schema/ArraySchema.java
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@
import java.util.Collection;
import java.util.List;
import java.util.Objects;
import java.util.Optional;

import org.json.JSONArray;

Expand Down Expand Up @@ -66,16 +67,6 @@ public Builder addItemSchema(final Schema itemSchema) {
return this;
}

public Builder schemaOfAdditionalItems(final Schema schemaOfAdditionalItems) {
this.schemaOfAdditionalItems = schemaOfAdditionalItems;
return this;
}

public Builder requiresArray(final boolean requiresArray) {
this.requiresArray = requiresArray;
return this;
}

public Builder additionalItems(final boolean additionalItems) {
this.additionalItems = additionalItems;
return this;
Expand All @@ -101,6 +92,16 @@ public Builder minItems(final Integer minItems) {
return this;
}

public Builder requiresArray(final boolean requiresArray) {
this.requiresArray = requiresArray;
return this;
}

public Builder schemaOfAdditionalItems(final Schema schemaOfAdditionalItems) {
this.schemaOfAdditionalItems = schemaOfAdditionalItems;
return this;
}

public Builder uniqueItems(final boolean uniqueItems) {
this.uniqueItems = uniqueItems;
return this;
Expand Down Expand Up @@ -168,6 +169,19 @@ public Integer getMinItems() {
return minItems;
}

public Schema getSchemaOfAdditionalItems() {
return schemaOfAdditionalItems;
}

private Optional<ValidationException> ifFails(final Schema schema, final Object input) {
try {
schema.validate(input);
return Optional.empty();
} catch (ValidationException e) {
return Optional.of(e);
}
}

public boolean needsUniqueItems() {
return uniqueItems;
}
Expand All @@ -176,78 +190,90 @@ public boolean permitsAdditionalItems() {
return additionalItems;
}

private void testItemCount(final JSONArray subject) {
public boolean requiresArray() {
return requiresArray;
}

private Optional<ValidationException> testItemCount(final JSONArray subject) {
int actualLength = subject.length();
if (minItems != null && actualLength < minItems) {
throw new ValidationException("expected minimum item count: " + minItems + ", found: "
+ actualLength);
return Optional.of(new ValidationException(this, "expected minimum item count: " + minItems
+ ", found: " + actualLength));
}
if (maxItems != null && maxItems < actualLength) {
throw new ValidationException("expected maximum item count: " + minItems + ", found: "
+ actualLength);
return Optional.of(new ValidationException(this, "expected maximum item count: " + minItems
+ ", found: " + actualLength));
}
return Optional.empty();
}

private void testItems(final JSONArray subject) {
private List<ValidationException> testItems(final JSONArray subject) {
List<ValidationException> rval = new ArrayList<>();
if (allItemSchema != null) {
for (int i = 0; i < subject.length(); ++i) {
allItemSchema.validate(subject.get(i));
int copyOfI = i; // i is not effectively final so we copy it
ifFails(allItemSchema, subject.get(i))
.map(exc -> exc.prepend(String.valueOf(copyOfI)))
.ifPresent(rval::add);
}
} else if (itemSchemas != null) {
if (!additionalItems && subject.length() > itemSchemas.size()) {
throw new ValidationException(String.format("expected: [%d] array items, found: [%d]",
itemSchemas.size(), subject.length()));
rval.add(new ValidationException(this, String.format(
"expected: [%d] array items, found: [%d]",
itemSchemas.size(), subject.length())));
}
int itemValidationUntil = Math.min(subject.length(), itemSchemas.size());
for (int i = 0; i < itemValidationUntil; ++i) {
itemSchemas.get(i).validate(subject.get(i));
int copyOfI = i; // i is not effectively final so we copy it
ifFails(itemSchemas.get(i), subject.get(i))
.map(exc -> exc.prepend(String.valueOf(copyOfI)))
.ifPresent(rval::add);
}
if (schemaOfAdditionalItems != null) {
for (int i = itemValidationUntil; i < subject.length(); ++i) {
schemaOfAdditionalItems.validate(subject.get(i));
int copyOfI = i; // i is not effectively final so we copy it
ifFails(schemaOfAdditionalItems, subject.get(i))
.map(exc -> exc.prepend(String.valueOf(copyOfI)))
.ifPresent(rval::add);
}
}
}
return rval;
}

private void testUniqueness(final JSONArray subject) {
private Optional<ValidationException> testUniqueness(final JSONArray subject) {
if (subject.length() == 0) {
return;
return Optional.empty();
}
Collection<Object> uniqueItems = new ArrayList<Object>(subject.length());
for (int i = 0; i < subject.length(); ++i) {
Object item = subject.get(i);
for (Object contained : uniqueItems) {
if (ObjectComparator.deepEquals(contained, item)) {
throw new ValidationException("array items are not unique");
return Optional.of(new ValidationException(this, "array items are not unique"));
}
}
uniqueItems.add(item);
}
return Optional.empty();
}

@Override
public void validate(final Object subject) {
List<ValidationException> failures = new ArrayList<>();
if (!(subject instanceof JSONArray)) {
if (requiresArray) {
throw new ValidationException(JSONArray.class, subject);
throw new ValidationException(this, JSONArray.class, subject);
}
} else {
JSONArray arrSubject = (JSONArray) subject;
testItemCount(arrSubject);
testItemCount(arrSubject).ifPresent(failures::add);
if (uniqueItems) {
testUniqueness(arrSubject);
testUniqueness(arrSubject).ifPresent(failures::add);
}
testItems(arrSubject);
failures.addAll(testItems(arrSubject));
}
}

public boolean requiresArray() {
return requiresArray;
}

public Schema getSchemaOfAdditionalItems() {
return schemaOfAdditionalItems;
ValidationException.throwFor(this, failures);
}

}
8 changes: 4 additions & 4 deletions core/src/main/java/org/everit/json/schema/BooleanSchema.java
Original file line number Diff line number Diff line change
Expand Up @@ -20,12 +20,10 @@
*/
public class BooleanSchema extends Schema {

public static final BooleanSchema INSTANCE = new BooleanSchema(builder());

/**
* Builder class for {@link BooleanSchema}.
*/
public static class Builder extends Schema.Builder {
public static class Builder extends Schema.Builder<BooleanSchema> {

@Override
public BooleanSchema build() {
Expand All @@ -34,6 +32,8 @@ public BooleanSchema build() {

}

public static final BooleanSchema INSTANCE = new BooleanSchema(builder());

public static Builder builder() {
return new Builder();
}
Expand All @@ -45,7 +45,7 @@ public BooleanSchema(final Builder builder) {
@Override
public void validate(final Object subject) {
if (!(subject instanceof Boolean)) {
throw new ValidationException(Boolean.class, subject);
throw new ValidationException(this, Boolean.class, subject);
}
}

Expand Down
32 changes: 18 additions & 14 deletions core/src/main/java/org/everit/json/schema/CombinedSchema.java
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,11 @@ public static class Builder extends Schema.Builder<CombinedSchema> {

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

@Override
public CombinedSchema build() {
return new CombinedSchema(this);
}

public Builder criterion(final ValidationCriterion criterion) {
this.criterion = criterion;
return this;
Expand All @@ -48,19 +53,6 @@ public Builder subschemas(final Collection<Schema> subschemas) {
return this;
}

@Override
public CombinedSchema build() {
return new CombinedSchema(this);
}

}

public static Builder builder() {
return new Builder();
}

public static Builder builder(final Collection<Schema> subschemas) {
return new Builder().subschemas(subschemas);
}

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

public static Builder builder() {
return new Builder();
}

public static Builder builder(final Collection<Schema> subschemas) {
return new Builder().subschemas(subschemas);
}

public static Builder oneOf(final Collection<Schema> schemas) {
return builder(schemas).criterion(ONE_CRITERION);
}
Expand Down Expand Up @@ -164,7 +164,11 @@ public void validate(final Object subject) {
int matchingCount = (int) subschemas.stream()
.filter(schema -> succeeds(schema, subject))
.count();
criterion.validate(subschemas.size(), matchingCount);
try {
criterion.validate(subschemas.size(), matchingCount);
} catch (ValidationException e) {
throw new ValidationException(this, e.getMessage());
}
}

}
6 changes: 4 additions & 2 deletions core/src/main/java/org/everit/json/schema/EnumSchema.java
Original file line number Diff line number Diff line change
Expand Up @@ -65,11 +65,13 @@ public Set<Object> getPossibleValues() {

@Override
public void validate(final Object subject) {
possibleValues.stream()
possibleValues
.stream()
.filter(val -> ObjectComparator.deepEquals(val, subject))
.findAny()
.orElseThrow(
() -> new ValidationException(String.format("%s is not a valid enum value", subject)));
() -> new ValidationException(this, String.format("%s is not a valid enum value",
subject)));
}

}
Loading

0 comments on commit c346e70

Please sign in to comment.