Skip to content

Commit b20234e

Browse files
committed
Consider errors from earlier indices (in instances) to be better matches
Improves `best_match` and generally error messages in the presence of `anyOf` / `oneOf` in cases where the errors are at the same level of depth within the instance but occur earlier. In other words: {"anyOf": [{"items": {"const": 37}]} now behaves like simply `{"items": {"const": 37}}`. Closes: #1250
1 parent 41b49c6 commit b20234e

File tree

3 files changed

+71
-6
lines changed

3 files changed

+71
-6
lines changed

CHANGELOG.rst

+6
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,9 @@
1+
v4.22.0
2+
=======
3+
4+
* Improve ``best_match`` (and thereby error messages from ``jsonschema.validate``) in cases where there are multiple *sibling* errors from applying ``anyOf`` / ``allOf`` -- i.e. when multiple elements of a JSON array have errors, we now do prefer showing errors from earlier elements rather than simply showing an error for the full array (#1250).
5+
* (Micro-)optimize equality checks when comparing for JSON Schema equality by first checking for object identity, as ``==`` would.
6+
17
v4.21.1
28
=======
39

jsonschema/exceptions.py

+7-6
Original file line numberDiff line numberDiff line change
@@ -395,12 +395,13 @@ def by_relevance(weak=WEAK_MATCHES, strong=STRONG_MATCHES):
395395

396396
def relevance(error):
397397
validator = error.validator
398-
return (
399-
-len(error.path),
400-
validator not in weak,
401-
validator in strong,
402-
not error._matches_type(),
403-
)
398+
return ( # prefer errors which are ...
399+
-len(error.path), # 'deeper' and thereby more specific
400+
error.path, # earlier (for sibling errors)
401+
validator not in weak, # for a non-low-priority keyword
402+
validator in strong, # for a high priority keyword
403+
not error._matches_type(), # at least match the instance's type
404+
) # otherwise we'll treat them the same
404405

405406
return relevance
406407

jsonschema/tests/test_exceptions.py

+58
Original file line numberDiff line numberDiff line change
@@ -100,6 +100,35 @@ def test_anyOf_traversal_for_single_equally_relevant_error(self):
100100
best = self.best_match_of(instance=[], schema=schema)
101101
self.assertEqual(best.validator, "type")
102102

103+
def test_anyOf_traversal_for_single_sibling_errors(self):
104+
"""
105+
We *do* traverse anyOf with a single subschema that fails multiple
106+
times (e.g. on multiple items).
107+
"""
108+
109+
schema = {
110+
"anyOf": [
111+
{"items": {"const": 37}},
112+
],
113+
}
114+
best = self.best_match_of(instance=[12, 12], schema=schema)
115+
self.assertEqual(best.validator, "const")
116+
117+
def test_anyOf_traversal_for_non_type_matching_sibling_errors(self):
118+
"""
119+
We *do* traverse anyOf with multiple subschemas when one does not type
120+
match.
121+
"""
122+
123+
schema = {
124+
"anyOf": [
125+
{"type": "object"},
126+
{"items": {"const": 37}},
127+
],
128+
}
129+
best = self.best_match_of(instance=[12, 12], schema=schema)
130+
self.assertEqual(best.validator, "const")
131+
103132
def test_if_the_most_relevant_error_is_oneOf_it_is_traversed(self):
104133
"""
105134
If the most relevant error is an oneOf, then we traverse its context
@@ -153,6 +182,35 @@ def test_oneOf_traversal_for_single_equally_relevant_error(self):
153182
best = self.best_match_of(instance=[], schema=schema)
154183
self.assertEqual(best.validator, "type")
155184

185+
def test_oneOf_traversal_for_single_sibling_errors(self):
186+
"""
187+
We *do* traverse oneOf with a single subschema that fails multiple
188+
times (e.g. on multiple items).
189+
"""
190+
191+
schema = {
192+
"oneOf": [
193+
{"items": {"const": 37}},
194+
],
195+
}
196+
best = self.best_match_of(instance=[12, 12], schema=schema)
197+
self.assertEqual(best.validator, "const")
198+
199+
def test_oneOf_traversal_for_non_type_matching_sibling_errors(self):
200+
"""
201+
We *do* traverse oneOf with multiple subschemas when one does not type
202+
match.
203+
"""
204+
205+
schema = {
206+
"oneOf": [
207+
{"type": "object"},
208+
{"items": {"const": 37}},
209+
],
210+
}
211+
best = self.best_match_of(instance=[12, 12], schema=schema)
212+
self.assertEqual(best.validator, "const")
213+
156214
def test_if_the_most_relevant_error_is_allOf_it_is_traversed(self):
157215
"""
158216
Now, if the error is allOf, we traverse but select the *most* relevant

0 commit comments

Comments
 (0)