diff --git a/jsonschema/exceptions.py b/jsonschema/exceptions.py index 46ffceda5..0948966a3 100644 --- a/jsonschema/exceptions.py +++ b/jsonschema/exceptions.py @@ -283,6 +283,46 @@ def total_errors(self): child_errors = sum(len(tree) for _, tree in self._contents.items()) return len(self.errors) + child_errors +def get_instance_type(value): + """ + Returns the type to use in type comparaisons of values' types + + Arguments: + value (object): + an object from the schema or the instance + + Returns (type): + the type of the object passed consistent with other JSON types + """ + value_type = type(value) + if value_type in [int, float]: + return float + elif value_type in [bool, str, list, dict]: + return value_type + return None + +def get_schema_type(value): + """ + Returns the type of the validation rule as a python type + + Arguments: + value (string): + One of the values : ['string', 'array', 'object', 'number', None] + + Returns (type): + One of the values : [str, list, dict, float, None] + """ + if value == 'string': + return str + elif value == 'number': + return float + elif value == 'object': + return dict + elif value == 'array': + return list + else: + return None + def by_relevance(weak=WEAK_MATCHES, strong=STRONG_MATCHES): """ @@ -301,8 +341,31 @@ def by_relevance(weak=WEAK_MATCHES, strong=STRONG_MATCHES): a collection of validator names to consider to be "strong" """ def relevance(error): - validator = error.validator - return -len(error.path), validator not in weak, validator in strong + same_type = False + # If the 'type' property has been provided within the schema rule + # set rule_type to the python corresponding + if isinstance(error.schema, dict) and 'type' in error.schema: + rule_type = get_schema_type(error.schema['type']) + else: + rule_type = None + + instance = error.instance + instance_type = None + instance_type = get_instance_type(instance) + + if rule_type is not None: + same_type = instance_type == rule_type + elif error.validator == 'enum': + if isinstance(error.validator_value, list): + for value in error.validator_value: + same_type = get_instance_type(value) == instance_type + if same_type: + break + elif error.validator == 'const': + same_type = get_instance_type(error.validator_value) == instance_type + + return -len(error.path), error.validator not in weak, error.validator in strong, -same_type + return relevance diff --git a/jsonschema/tests/test_exceptions.py b/jsonschema/tests/test_exceptions.py index a28555029..4881740e9 100644 --- a/jsonschema/tests/test_exceptions.py +++ b/jsonschema/tests/test_exceptions.py @@ -1,7 +1,7 @@ from unittest import TestCase import textwrap -from jsonschema import Draft4Validator, exceptions +from jsonschema import Draft4Validator, Draft7Validator, exceptions class TestBestMatch(TestCase): @@ -154,6 +154,87 @@ def test_no_errors(self): validator = Draft4Validator({}) self.assertIsNone(exceptions.best_match(validator.iter_errors({}))) + def test_same_type_is_prioritized(self): + # Best match to prioritize error with the same type as value regardless of order + validator = Draft7Validator({ + "properties" : { + "value" : { + "anyOf" : [ + {"type" : "array" , "minItems" : 2}, + {"type" : "string", "minLength" : 10}, + ], + }, + }, + }, + ) + self.assertEqual(exceptions.best_match(validator.iter_errors({"value" : "foo"})).validator, "minLength") + + validator = Draft7Validator({ + "properties" : { + "value" : { + "anyOf" : [ + {"type" : "string", "minLength" : 10}, + {"type" : "array" , "minItems" : 2}, + ], + }, + }, + }, + ) + self.assertEqual(exceptions.best_match(validator.iter_errors({"value" : "foo"})).validator, "minLength") + + def test_same_type_is_prioritized_const(self): + validator = Draft7Validator({ + "properties" : { + "value" : { + "anyOf" : [ + {"type" : "array" , "minItems" : 2}, + {"const" : "bar"}, + ], + }, + }, + }, + ) + self.assertEqual(exceptions.best_match(validator.iter_errors({"value" : "foo"})).validator, "const") + + validator = Draft7Validator({ + "properties" : { + "value" : { + "anyOf" : [ + {"const" : "bar"}, + {"type" : "array" , "minItems" : 2}, + ], + }, + }, + }, + ) + self.assertEqual(exceptions.best_match(validator.iter_errors({"value" : "foo"})).validator, "const") + + def test_same_type_is_prioritized_enum(self): + validator = Draft7Validator({ + "properties" : { + "value" : { + "anyOf" : [ + {"type" : "array" , "minItems" : 2}, + {"enum" : ["bar", 2.3]}, + ], + }, + }, + }, + ) + self.assertEqual(exceptions.best_match(validator.iter_errors({"value" : "foo"})).validator, "enum") + + validator = Draft7Validator({ + "properties" : { + "value" : { + "anyOf" : [ + {"enum" : ["bar", 2.3]}, + {"type" : "array" , "minItems" : 2}, + ], + }, + }, + }, + ) + self.assertEqual(exceptions.best_match(validator.iter_errors({"value" : "foo"})).validator, "enum") class TestByRelevance(TestCase): def test_short_paths_are_better_matches(self):