Skip to content

Commit bd5ea73

Browse files
committed
Don't let best_match traverse into applicators with equally bad sub-errors.
I.e., if an anyOf or oneOf has branches where all errors seem equally bad, return the anyOf/oneOf itself, rather than traversing into its context. Closes: #646 Which should now produce a nicer error.
1 parent 8e090fb commit bd5ea73

File tree

3 files changed

+79
-4
lines changed

3 files changed

+79
-4
lines changed

jsonschema/exceptions.py

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@
66
from collections import defaultdict, deque
77
from pprint import pformat
88
from textwrap import dedent, indent
9+
import heapq
910
import itertools
1011

1112
import attr
@@ -383,5 +384,10 @@ def best_match(errors, key=relevance):
383384
best = max(itertools.chain([best], errors), key=key)
384385

385386
while best.context:
386-
best = min(best.context, key=key)
387+
# Calculate the minimum via nsmallest, because we don't recurse if
388+
# all nested errors have the same relevance (i.e. if min == max == all)
389+
smallest = heapq.nsmallest(2, best.context, key=key)
390+
if len(smallest) == 2 and key(smallest[0]) == key(smallest[1]):
391+
return best
392+
best = smallest[0]
387393
return best

jsonschema/tests/test_exceptions.py

Lines changed: 65 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -66,6 +66,36 @@ def test_if_the_most_relevant_error_is_anyOf_it_is_traversed(self):
6666
best = self.best_match_of(instance={"foo": {"bar": 12}}, schema=schema)
6767
self.assertEqual(best.validator_value, "array")
6868

69+
def test_no_anyOf_traversal_for_equally_relevant_errors(self):
70+
"""
71+
We don't traverse into an anyOf (as above) if all of its context errors
72+
seem to be equally "wrong" against the instance.
73+
"""
74+
75+
schema = {
76+
"anyOf": [
77+
{"type": "string"},
78+
{"type": "integer"},
79+
{"type": "object"},
80+
],
81+
}
82+
best = self.best_match_of(instance=[], schema=schema)
83+
self.assertEqual(best.validator, "anyOf")
84+
85+
def test_anyOf_traversal_for_single_equally_relevant_error(self):
86+
"""
87+
We *do* traverse anyOf with a single nested error, even though it is
88+
vacuously equally relevant to itself.
89+
"""
90+
91+
schema = {
92+
"anyOf": [
93+
{"type": "string"},
94+
],
95+
}
96+
best = self.best_match_of(instance=[], schema=schema)
97+
self.assertEqual(best.validator, "type")
98+
6999
def test_if_the_most_relevant_error_is_oneOf_it_is_traversed(self):
70100
"""
71101
If the most relevant error is an oneOf, then we traverse its context
@@ -89,6 +119,36 @@ def test_if_the_most_relevant_error_is_oneOf_it_is_traversed(self):
89119
best = self.best_match_of(instance={"foo": {"bar": 12}}, schema=schema)
90120
self.assertEqual(best.validator_value, "array")
91121

122+
def test_no_oneOf_traversal_for_equally_relevant_errors(self):
123+
"""
124+
We don't traverse into an oneOf (as above) if all of its context errors
125+
seem to be equally "wrong" against the instance.
126+
"""
127+
128+
schema = {
129+
"oneOf": [
130+
{"type": "string"},
131+
{"type": "integer"},
132+
{"type": "object"},
133+
],
134+
}
135+
best = self.best_match_of(instance=[], schema=schema)
136+
self.assertEqual(best.validator, "oneOf")
137+
138+
def test_oneOf_traversal_for_single_equally_relevant_error(self):
139+
"""
140+
We *do* traverse oneOf with a single nested error, even though it is
141+
vacuously equally relevant to itself.
142+
"""
143+
144+
schema = {
145+
"oneOf": [
146+
{"type": "string"},
147+
],
148+
}
149+
best = self.best_match_of(instance=[], schema=schema)
150+
self.assertEqual(best.validator, "type")
151+
92152
def test_if_the_most_relevant_error_is_allOf_it_is_traversed(self):
93153
"""
94154
Now, if the error is allOf, we traverse but select the *most* relevant
@@ -109,6 +169,11 @@ def test_if_the_most_relevant_error_is_allOf_it_is_traversed(self):
109169
self.assertEqual(best.validator_value, "string")
110170

111171
def test_nested_context_for_oneOf(self):
172+
"""
173+
We traverse into nested contexts (a oneOf containing an error in a
174+
nested oneOf here).
175+
"""
176+
112177
schema = {
113178
"properties": {
114179
"foo": {

jsonschema/tests/test_validators.py

Lines changed: 7 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1959,11 +1959,15 @@ def test_schema_error_message(self):
19591959
)
19601960

19611961
def test_it_uses_best_match(self):
1962-
# This is a schema that best_match will recurse into
1963-
schema = {"oneOf": [{"type": "string"}, {"type": "array"}]}
1962+
schema = {
1963+
"oneOf": [
1964+
{"type": "number", "minimum": 20},
1965+
{"type": "array"},
1966+
],
1967+
}
19641968
with self.assertRaises(exceptions.ValidationError) as e:
19651969
validators.validate(12, schema)
1966-
self.assertIn("12 is not of type", str(e.exception))
1970+
self.assertIn("12 is less than the minimum of 20", str(e.exception))
19671971

19681972

19691973
class TestRefResolver(TestCase):

0 commit comments

Comments
 (0)