Skip to content

Commit eae88ed

Browse files
authored
Merge pull request #405 from paulfrmbstn/issue/404
Fixing #404: Non-deterministic validation result when useDefaults are used with combined schema dependencies
2 parents 23cad68 + 0f32f90 commit eae88ed

File tree

6 files changed

+146
-2
lines changed

6 files changed

+146
-2
lines changed

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

Lines changed: 15 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@
77
import java.util.Collection;
88
import java.util.List;
99
import java.util.Objects;
10+
import java.util.stream.Collectors;
1011

1112
/**
1213
* Validator for {@code allOf}, {@code oneOf}, {@code anyOf} schemas.
@@ -167,7 +168,20 @@ public CombinedSchema(Builder builder) {
167168
super(builder);
168169
this.synthetic = builder.synthetic;
169170
this.criterion = requireNonNull(builder.criterion, "criterion cannot be null");
170-
this.subschemas = requireNonNull(builder.subschemas, "subschemas cannot be null");
171+
this.subschemas = sortByCombinedFirst(requireNonNull(builder.subschemas, "subschemas cannot be null"));
172+
}
173+
174+
private static int compareBySchemaType(Schema lschema, Schema rschema) {
175+
return lschema instanceof CombinedSchema ?
176+
(rschema instanceof CombinedSchema ? 0 : -1) :
177+
(rschema instanceof CombinedSchema ? 1 : 0);
178+
}
179+
180+
// ensure subschemas of type CombinedSchema are always visited first
181+
private static Collection<Schema> sortByCombinedFirst(Collection<Schema> schemas) {
182+
return schemas.stream()
183+
.sorted(CombinedSchema::compareBySchemaType)
184+
.collect(Collectors.toCollection(ArrayList::new));
171185
}
172186

173187
public ValidationCriterion getCriterion() {

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

Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,53 @@ public class CombinedSchemaTest {
3434
NumberSchema.builder().multipleOf(10).build(),
3535
NumberSchema.builder().multipleOf(3).build());
3636

37+
@Test
38+
public void subschemasSort() {
39+
CombinedSchema subcombined1 = CombinedSchema
40+
.allOf(asList(BooleanSchema.INSTANCE))
41+
.build();
42+
43+
CombinedSchema subcombined2 = CombinedSchema
44+
.allOf(asList(NullSchema.INSTANCE))
45+
.build();
46+
47+
BooleanSchema booleanSchema1 = BooleanSchema.INSTANCE;
48+
BooleanSchema booleanSchema2 = BooleanSchema.INSTANCE;
49+
50+
EmptySchema emptySchema1 = EmptySchema.INSTANCE;
51+
EmptySchema emptySchema2 = EmptySchema.INSTANCE;
52+
53+
NullSchema nullSchema1 = NullSchema.INSTANCE;
54+
NullSchema nullSchema2 = NullSchema.INSTANCE;
55+
56+
// subschemas of type CombinedSchema should bubble to the top,
57+
// all other schema types should remain in same order
58+
CombinedSchema subject = CombinedSchema.builder()
59+
.criterion(CombinedSchema.ALL_CRITERION)
60+
.subschema(nullSchema1)
61+
.subschema(emptySchema1)
62+
.subschema(booleanSchema1)
63+
.subschema(subcombined1)
64+
.subschema(booleanSchema2)
65+
.subschema(emptySchema2)
66+
.subschema(nullSchema2)
67+
.subschema(subcombined2)
68+
.isSynthetic(true)
69+
.build();
70+
71+
Object[] subschemas = subject.getSubschemas().toArray();
72+
73+
assertEquals(8, subschemas.length);
74+
assertEquals(subcombined1, subschemas[0]);
75+
assertEquals(subcombined2, subschemas[1]);
76+
assertEquals(nullSchema1, subschemas[2]);
77+
assertEquals(emptySchema1, subschemas[3]);
78+
assertEquals(booleanSchema1, subschemas[4]);
79+
assertEquals(booleanSchema2, subschemas[5]);
80+
assertEquals(emptySchema2, subschemas[6]);
81+
assertEquals(nullSchema2, subschemas[7]);
82+
}
83+
3784
@Test
3885
public void allCriterionFailure() {
3986
assertThrows(ValidationException.class, () -> {
Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,43 @@
1+
package org.everit.json.schema;
2+
3+
import org.everit.json.schema.loader.SchemaLoader;
4+
import org.json.JSONObject;
5+
import org.junit.jupiter.api.Test;
6+
7+
import static org.junit.jupiter.api.Assertions.*;
8+
9+
public class DefaultAndRequiredTest {
10+
11+
private static final ResourceLoader LOADER = ResourceLoader.DEFAULT;
12+
13+
@Test
14+
public void defaultAndRequired() {
15+
// tests consistent validation when useDefaults
16+
// is enabled and schema contains dependency on property with
17+
// default value; see issue #404
18+
int expectedExceptions = 20;
19+
int actualExceptions = 0;
20+
21+
for (int i = 0; i < expectedExceptions; i++) {
22+
JSONObject jsonSchema = LOADER.readObj("default-and-required-schema.json");
23+
JSONObject jsonSpecification = LOADER.readObj("default-and-required.json");
24+
Schema schema = SchemaLoader
25+
.builder()
26+
.useDefaults(true)
27+
.schemaJson(jsonSchema)
28+
.build()
29+
.load()
30+
.build();
31+
32+
try {
33+
schema.validate(jsonSpecification);
34+
} catch (ValidationException e) {
35+
assertEquals(1, e.getCausingExceptions().size());
36+
assertTrue(e.getMessage().contains("only 1 subschema matches out of 2"));
37+
actualExceptions += 1;
38+
}
39+
}
40+
41+
assertEquals(expectedExceptions, actualExceptions);
42+
}
43+
}
Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,37 @@
1+
{
2+
"$schema": "http://json-schema.org/draft-07/schema#",
3+
"title": "bugtest",
4+
"description": "everit bug",
5+
"type": "object",
6+
"additionalProperties": false,
7+
"dependencies": {},
8+
"properties": {
9+
"arrayPropEnabled": {
10+
"type": "boolean"
11+
},
12+
"arrayProp": {
13+
"type": "array",
14+
"items": {
15+
"type": "number"
16+
},
17+
"default": [0,1,2,3]
18+
}
19+
},
20+
"allOf": [
21+
{
22+
"if": {
23+
"properties": {
24+
"arrayPropEnabled": true
25+
}
26+
},
27+
"then": {
28+
"dependencies": {
29+
"arrayPropEnabled": [
30+
"arrayProp"
31+
]
32+
}
33+
}
34+
}
35+
]
36+
}
37+
Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
{
2+
"arrayPropEnabled": true
3+
}
Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1 +1 @@
1-
{prop: null}
1+
{"prop": null}

0 commit comments

Comments
 (0)