diff --git a/jsonschema/_utils.py b/jsonschema/_utils.py index d4524e876..664e809db 100644 --- a/jsonschema/_utils.py +++ b/jsonschema/_utils.py @@ -338,3 +338,9 @@ def find_evaluated_property_keys_by_schema(validator, instance, schema): ) return evaluated_keys + + +def errors_with_property_name(errors, property_name): + for error in errors: + error.extra_info = {"property": property_name} + yield error diff --git a/jsonschema/_validators.py b/jsonschema/_validators.py index e0845ea5d..13f1abdc5 100644 --- a/jsonschema/_validators.py +++ b/jsonschema/_validators.py @@ -5,6 +5,7 @@ from jsonschema._utils import ( ensure_list, equal, + errors_with_property_name, extras_msg, find_additional_properties, find_evaluated_item_indexes_by_schema, @@ -32,8 +33,7 @@ def propertyNames(validator, propertyNames, instance, schema): return for property in instance: - yield from validator.descend(instance=property, schema=propertyNames) - + yield from errors_with_property_name(validator.descend(instance=property, schema=propertyNames), property) def additionalProperties(validator, aP, instance, schema): if not validator.is_type(instance, "object"): @@ -43,8 +43,9 @@ def additionalProperties(validator, aP, instance, schema): if validator.is_type(aP, "object"): for extra in extras: - yield from validator.descend(instance[extra], aP, path=extra) + yield from errors_with_property_name(validator.descend(instance[extra], aP, path=extra), extra) elif not aP and extras: + extra_info_properties = [extra for extra in sorted(extras)] if "patternProperties" in schema: if len(extras) == 1: verb = "does" @@ -56,11 +57,13 @@ def additionalProperties(validator, aP, instance, schema): repr(each) for each in sorted(schema["patternProperties"]) ) error = f"{joined} {verb} not match any of the regexes: {patterns}" - yield ValidationError(error) + yield ValidationError(error, extra_info={"properties": extra_info_properties}) else: error = "Additional properties are not allowed (%s %s unexpected)" - yield ValidationError(error % extras_msg(extras)) - + yield ValidationError( + error % extras_msg(extras), + extra_info={"properties": extra_info_properties} + ) def items(validator, items, instance, schema): if not validator.is_type(instance, "array"): @@ -261,7 +264,7 @@ def dependentRequired(validator, dependentRequired, instance, schema): for each in dependency: if each not in instance: message = f"{each!r} is a dependency of {property!r}" - yield ValidationError(message) + yield ValidationError(message, extra_info={"property": property}) def dependentSchemas(validator, dependentSchemas, instance, schema): @@ -339,7 +342,7 @@ def required(validator, required, instance, schema): return for property in required: if property not in instance: - yield ValidationError(f"{property!r} is a required property") + yield ValidationError(f"{property!r} is a required property", extra_info={"property": property}) def minProperties(validator, mP, instance, schema): diff --git a/jsonschema/exceptions.py b/jsonschema/exceptions.py index e2ec35fa1..db37dcad4 100644 --- a/jsonschema/exceptions.py +++ b/jsonschema/exceptions.py @@ -29,6 +29,7 @@ def __init__( schema=_unset, schema_path=(), parent=None, + extra_info=None, ): super(_Error, self).__init__( message, @@ -41,6 +42,7 @@ def __init__( schema, schema_path, parent, + extra_info, ) self.message = message self.path = self.relative_path = deque(path) @@ -52,6 +54,7 @@ def __init__( self.instance = instance self.schema = schema self.parent = parent + self.extra_info = extra_info for error in context: error.parent = self @@ -130,7 +133,7 @@ def _set(self, **kwargs): def _contents(self): attrs = ( "message", "cause", "context", "validator", "validator_value", - "path", "schema_path", "instance", "schema", "parent", + "path", "schema_path", "instance", "schema", "parent", "extra_info", ) return dict((attr, getattr(self, attr)) for attr in attrs) diff --git a/jsonschema/tests/test_validators.py b/jsonschema/tests/test_validators.py index 5fabff93c..420795546 100644 --- a/jsonschema/tests/test_validators.py +++ b/jsonschema/tests/test_validators.py @@ -608,6 +608,40 @@ def test_unevaluated_properties(self): class TestValidationErrorDetails(TestCase): # TODO: These really need unit tests for each individual validator, rather # than just these higher level tests. + def test_extra_info_required(self): + instance = {"a": 1} + schema = { + "required": ["b", "c"], + } + + validator = validators.Draft4Validator(schema) + e1, e2 = validator.iter_errors(instance) + self.assertEqual(e1.extra_info, {"property": "b"}) + self.assertEqual(e2.extra_info, {"property": "c"}) + + def test_extra_info_additionalProperties_single(self): + instance = {"a": 1} + schema = {"additionalProperties": False} + + validator = validators.Draft4Validator(schema) + e1, = validator.iter_errors(instance) + self.assertEqual(e1.extra_info, {"properties": ["a"]}) + + def test_extra_info_additionalProperties_multiple(self): + instance = {"a": 1,"b": 2} + schema = {"additionalProperties": False} + + validator = validators.Draft4Validator(schema) + e1, = validator.iter_errors(instance) + self.assertEqual(e1.extra_info, {"properties": ["a", "b"]}) + + def test_extra_info_dependentRequired(self): + instance = {"a": {}} + schema = {"dependentRequired": {"a": ["bar"]}} + validator = validators.Draft202012Validator(schema) + e1, = validator.iter_errors(instance) + self.assertEqual(e1.extra_info, {"property": "a"}) + def test_anyOf(self): instance = 5 schema = { @@ -972,6 +1006,8 @@ def test_additionalProperties(self): self.assertEqual(e1.validator, "type") self.assertEqual(e2.validator, "minimum") + self.assertEqual(e1.extra_info, {"property" : "bar"}) + def test_patternProperties(self): instance = {"bar": 1, "foo": 2} schema = { @@ -1050,6 +1086,8 @@ def test_propertyNames(self): self.assertEqual(error.json_path, "$") self.assertEqual(error.schema_path, deque(["propertyNames", "not"])) + self.assertEqual(error.extra_info, {"property": "foo"}) + def test_if_then(self): schema = { "if": {"const": 12},