Skip to content

Commit

Permalink
Fix items/prefixItems' message when used with heterogeneous arrays.
Browse files Browse the repository at this point in the history
Closes: #1157
  • Loading branch information
Julian committed Oct 30, 2023
1 parent d9be1a4 commit 8cff13d
Show file tree
Hide file tree
Showing 4 changed files with 108 additions and 8 deletions.
5 changes: 5 additions & 0 deletions CHANGELOG.rst
Original file line number Diff line number Diff line change
@@ -1,3 +1,8 @@
v4.19.2
=======

* Fix the error message for additional items when used with heterogeneous arrays.

v4.19.1
=======

Expand Down
19 changes: 14 additions & 5 deletions jsonschema/_keywords.py
Original file line number Diff line number Diff line change
Expand Up @@ -54,7 +54,7 @@ def additionalProperties(validator, aP, instance, schema):
yield ValidationError(error)
else:
error = "Additional properties are not allowed (%s %s unexpected)"
yield ValidationError(error % extras_msg(extras))
yield ValidationError(error % extras_msg(sorted(extras, key=str)))


def items(validator, items, instance, schema):
Expand All @@ -63,9 +63,17 @@ def items(validator, items, instance, schema):

prefix = len(schema.get("prefixItems", []))
total = len(instance)
if items is False and total > prefix:
message = f"Expected at most {prefix} items, but found {total}"
yield ValidationError(message)
extra = total - prefix
if extra <= 0:
return

if items is False:
rest = instance[prefix:] if extra != 1 else instance[prefix]
item = "items" if prefix != 1 else "item"
yield ValidationError(
f"Expected at most {prefix} {item} but found {extra} "
f"extra: {rest!r}",
)
else:
for index in range(prefix, total):
yield from validator.descend(
Expand Down Expand Up @@ -427,7 +435,8 @@ def unevaluatedProperties(validator, unevaluatedProperties, instance, schema):
if unevaluated_keys:
if unevaluatedProperties is False:
error = "Unevaluated properties are not allowed (%s %s unexpected)"
yield ValidationError(error % extras_msg(unevaluated_keys))
extras = sorted(unevaluated_keys, key=str)
yield ValidationError(error % extras_msg(extras))
else:
error = (
"Unevaluated properties are not valid under "
Expand Down
2 changes: 1 addition & 1 deletion jsonschema/_utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -91,7 +91,7 @@ def extras_msg(extras):
"""

verb = "was" if len(extras) == 1 else "were"
return ", ".join(repr(extra) for extra in sorted(extras)), verb
return ", ".join(repr(extra) for extra in extras), verb


def ensure_list(thing):
Expand Down
90 changes: 88 additions & 2 deletions jsonschema/tests/test_validators.py
Original file line number Diff line number Diff line change
Expand Up @@ -510,11 +510,24 @@ def test_maxItems(self):
self.assertEqual(message, "[1, 2, 3] is too long")

def test_prefixItems_with_items(self):
message = self.message_for(
instance=[1, 2, "foo"],
schema={"items": False, "prefixItems": [{}, {}]},
)
self.assertEqual(
message,
"Expected at most 2 items but found 1 extra: 'foo'"
)

def test_prefixItems_with_multiple_extra_items(self):
message = self.message_for(
instance=[1, 2, "foo", 5],
schema={"items": False, "prefixItems": [{}, {}]},
)
self.assertEqual(message, "Expected at most 2 items, but found 4")
self.assertEqual(
message,
"Expected at most 2 items but found 2 extra: ['foo', 5]"
)

def test_minLength(self):
message = self.message_for(
Expand Down Expand Up @@ -655,7 +668,7 @@ def test_unevaluated_items(self):
message = self.message_for(instance=["foo", "bar"], schema=schema)
self.assertIn(
message,
"Unevaluated items are not allowed ('bar', 'foo' were unexpected)",
"Unevaluated items are not allowed ('foo', 'bar' were unexpected)",
)

def test_unevaluated_items_on_invalid_type(self):
Expand Down Expand Up @@ -702,6 +715,79 @@ def test_unevaluated_properties_on_invalid_type(self):
message = self.message_for(instance="foo", schema=schema)
self.assertEqual(message, "'foo' is not of type 'object'")

def test_single_item(self):
schema = {"prefixItems": [{}], "items": False}
message = self.message_for(
instance=["foo", "bar", "baz"],
schema=schema,
)
self.assertEqual(
message,
"Expected at most 1 item but found 2 extra: ['bar', 'baz']",
)

def test_heterogeneous_additionalItems_with_Items(self):
schema = {"items": [{}], "additionalItems": False}
message = self.message_for(
instance=["foo", "bar", 37],
schema=schema,
cls=validators.Draft7Validator,
)
self.assertEqual(
message,
"Additional items are not allowed ('bar', 37 were unexpected)"
)

def test_heterogeneous_items_prefixItems(self):
schema = {"prefixItems": [{}], "items": False}
message = self.message_for(
instance=["foo", "bar", 37],
schema=schema,
)
self.assertEqual(
message,
"Expected at most 1 item but found 2 extra: ['bar', 37]",
)

def test_heterogeneous_unevaluatedItems_prefixItems(self):
schema = {"prefixItems": [{}], "unevaluatedItems": False}
message = self.message_for(
instance=["foo", "bar", 37],
schema=schema,
)
self.assertEqual(
message,
"Unevaluated items are not allowed ('bar', 37 were unexpected)",
)

def test_heterogeneous_properties_additionalProperties(self):
"""
Not valid deserialized JSON, but this should not blow up.
"""
schema = {"properties": {"foo": {}}, "additionalProperties": False}
message = self.message_for(
instance={"foo": {}, "a": "baz", 37: 12},
schema=schema,
)
self.assertEqual(
message,
"Additional properties are not allowed (37, 'a' were unexpected)",
)

def test_heterogeneous_properties_unevaluatedProperties(self):
"""
Not valid deserialized JSON, but this should not blow up.
"""
schema = {"properties": {"foo": {}}, "unevaluatedProperties": False}
message = self.message_for(
instance={"foo": {}, "a": "baz", 37: 12},
schema=schema,
)
self.assertEqual(
message,
"Unevaluated properties are not allowed (37, 'a' were unexpected)",
)


class TestValidationErrorDetails(TestCase):
# TODO: These really need unit tests for each individual keyword, rather
Expand Down

0 comments on commit 8cff13d

Please sign in to comment.