Skip to content

Commit

Permalink
Consider errors from earlier indices (in instances) to be better matches
Browse files Browse the repository at this point in the history
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
  • Loading branch information
Julian committed Apr 24, 2024
1 parent 41b49c6 commit b20234e
Show file tree
Hide file tree
Showing 3 changed files with 71 additions and 6 deletions.
6 changes: 6 additions & 0 deletions CHANGELOG.rst
@@ -1,3 +1,9 @@
v4.22.0
=======

* 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).
* (Micro-)optimize equality checks when comparing for JSON Schema equality by first checking for object identity, as ``==`` would.

v4.21.1
=======

Expand Down
13 changes: 7 additions & 6 deletions jsonschema/exceptions.py
Expand Up @@ -395,12 +395,13 @@ def by_relevance(weak=WEAK_MATCHES, strong=STRONG_MATCHES):

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

return relevance

Expand Down
58 changes: 58 additions & 0 deletions jsonschema/tests/test_exceptions.py
Expand Up @@ -100,6 +100,35 @@ def test_anyOf_traversal_for_single_equally_relevant_error(self):
best = self.best_match_of(instance=[], schema=schema)
self.assertEqual(best.validator, "type")

def test_anyOf_traversal_for_single_sibling_errors(self):
"""
We *do* traverse anyOf with a single subschema that fails multiple
times (e.g. on multiple items).
"""

schema = {
"anyOf": [
{"items": {"const": 37}},
],
}
best = self.best_match_of(instance=[12, 12], schema=schema)
self.assertEqual(best.validator, "const")

def test_anyOf_traversal_for_non_type_matching_sibling_errors(self):
"""
We *do* traverse anyOf with multiple subschemas when one does not type
match.
"""

schema = {
"anyOf": [
{"type": "object"},
{"items": {"const": 37}},
],
}
best = self.best_match_of(instance=[12, 12], schema=schema)
self.assertEqual(best.validator, "const")

def test_if_the_most_relevant_error_is_oneOf_it_is_traversed(self):
"""
If the most relevant error is an oneOf, then we traverse its context
Expand Down Expand Up @@ -153,6 +182,35 @@ def test_oneOf_traversal_for_single_equally_relevant_error(self):
best = self.best_match_of(instance=[], schema=schema)
self.assertEqual(best.validator, "type")

def test_oneOf_traversal_for_single_sibling_errors(self):
"""
We *do* traverse oneOf with a single subschema that fails multiple
times (e.g. on multiple items).
"""

schema = {
"oneOf": [
{"items": {"const": 37}},
],
}
best = self.best_match_of(instance=[12, 12], schema=schema)
self.assertEqual(best.validator, "const")

def test_oneOf_traversal_for_non_type_matching_sibling_errors(self):
"""
We *do* traverse oneOf with multiple subschemas when one does not type
match.
"""

schema = {
"oneOf": [
{"type": "object"},
{"items": {"const": 37}},
],
}
best = self.best_match_of(instance=[12, 12], schema=schema)
self.assertEqual(best.validator, "const")

def test_if_the_most_relevant_error_is_allOf_it_is_traversed(self):
"""
Now, if the error is allOf, we traverse but select the *most* relevant
Expand Down

0 comments on commit b20234e

Please sign in to comment.