From b20234e86c4dadf5d691400383a6fc0a1e9afc34 Mon Sep 17 00:00:00 2001 From: Julian Berman Date: Wed, 24 Apr 2024 17:35:54 +0300 Subject: [PATCH] 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 --- CHANGELOG.rst | 6 +++ jsonschema/exceptions.py | 13 ++++--- jsonschema/tests/test_exceptions.py | 58 +++++++++++++++++++++++++++++ 3 files changed, 71 insertions(+), 6 deletions(-) diff --git a/CHANGELOG.rst b/CHANGELOG.rst index b91e8827..0da30a6b 100644 --- a/CHANGELOG.rst +++ b/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 ======= diff --git a/jsonschema/exceptions.py b/jsonschema/exceptions.py index ace9df61..82d53da6 100644 --- a/jsonschema/exceptions.py +++ b/jsonschema/exceptions.py @@ -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 diff --git a/jsonschema/tests/test_exceptions.py b/jsonschema/tests/test_exceptions.py index 4052b5f1..5b3b4362 100644 --- a/jsonschema/tests/test_exceptions.py +++ b/jsonschema/tests/test_exceptions.py @@ -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 @@ -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