From e39abdb69105e14cc5e02d9486c9580bca84f3f1 Mon Sep 17 00:00:00 2001 From: Julian Berman Date: Sun, 10 Jul 2022 09:10:06 +0200 Subject: [PATCH] Enhance best match to prefer errors from matching types. Closes: #728 --- CHANGELOG.rst | 6 ++++++ jsonschema/exceptions.py | 22 ++++++++++++++++++-- jsonschema/tests/test_exceptions.py | 32 +++++++++++++++++++++++++++++ jsonschema/validators.py | 1 + 4 files changed, 59 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.rst b/CHANGELOG.rst index 51e4d0672..812246957 100644 --- a/CHANGELOG.rst +++ b/CHANGELOG.rst @@ -1,3 +1,9 @@ +v4.7.0 +------ + +* Enhance ``best_match`` to prefer errors from branches of the schema which + match the instance's type (#728) + v4.6.2 ------ diff --git a/jsonschema/exceptions.py b/jsonschema/exceptions.py index 274e6c58d..07beb2d6b 100644 --- a/jsonschema/exceptions.py +++ b/jsonschema/exceptions.py @@ -31,6 +31,7 @@ def __init__( schema=_unset, schema_path=(), parent=None, + type_checker=_unset, ): super(_Error, self).__init__( message, @@ -54,6 +55,7 @@ def __init__( self.instance = instance self.schema = schema self.parent = parent + self._type_checker = type_checker for error in context: error.parent = self @@ -124,7 +126,10 @@ def json_path(self): path += "." + elem return path - def _set(self, **kwargs): + def _set(self, type_checker=None, **kwargs): + if type_checker is not None and self._type_checker is _unset: + self._type_checker = type_checker + for k, v in kwargs.items(): if getattr(self, k) is _unset: setattr(self, k, v) @@ -136,6 +141,14 @@ def _contents(self): ) return dict((attr, getattr(self, attr)) for attr in attrs) + def _matches_type(self): + try: + expected_type = self.schema["type"] + except (KeyError, TypeError): + return False + else: + return self._type_checker.is_type(self.instance, expected_type) + class ValidationError(_Error): """ @@ -307,7 +320,12 @@ 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 + return ( + -len(error.path), + validator not in weak, + validator in strong, + not error._matches_type(), + ) return relevance diff --git a/jsonschema/tests/test_exceptions.py b/jsonschema/tests/test_exceptions.py index c57c10b94..c3c525583 100644 --- a/jsonschema/tests/test_exceptions.py +++ b/jsonschema/tests/test_exceptions.py @@ -131,6 +131,38 @@ def test_nested_context_for_oneOf(self): best = self.best_match_of(instance={"foo": {"bar": 12}}, schema=schema) self.assertEqual(best.validator_value, "array") + def test_it_prioritizes_matching_types(self): + schema = { + "properties": { + "foo": { + "anyOf": [ + {"type": "array", "minItems": 2}, + {"type": "string", "minLength": 10}, + ], + }, + }, + } + best = self.best_match_of(instance={"foo": "bar"}, schema=schema) + self.assertEqual(best.validator, "minLength") + + reordered = { + "properties": { + "foo": { + "anyOf": [ + {"type": "string", "minLength": 10}, + {"type": "array", "minItems": 2}, + ], + }, + }, + } + best = self.best_match_of(instance={"foo": "bar"}, schema=reordered) + self.assertEqual(best.validator, "minLength") + + def test_boolean_schemas(self): + schema = {"properties": {"foo": False}} + best = self.best_match_of(instance={"foo": "bar"}, schema=schema) + self.assertIsNone(best.validator) + def test_one_error(self): validator = _LATEST_VERSION({"minProperties": 2}) error, = validator.iter_errors({}) diff --git a/jsonschema/validators.py b/jsonschema/validators.py index 5a0b8b939..f986f1c6a 100644 --- a/jsonschema/validators.py +++ b/jsonschema/validators.py @@ -246,6 +246,7 @@ def iter_errors(self, instance, _schema=None): validator_value=v, instance=instance, schema=_schema, + type_checker=self.TYPE_CHECKER, ) if k not in {"if", "$ref"}: error.schema_path.appendleft(k)