Skip to content

Commit 6bd773e

Browse files
Ryan P Kilbycarltongibson
authored andcommitted
Improve composite field child errors (#5655)
* Fixup DictField test descriptions * Nest ListField/DictField errors under the idx/key * Add nested ListField/DictField tests
1 parent 22f1c59 commit 6bd773e

File tree

2 files changed

+73
-10
lines changed

2 files changed

+73
-10
lines changed

rest_framework/fields.py

Lines changed: 32 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1626,14 +1626,28 @@ def to_internal_value(self, data):
16261626
self.fail('not_a_list', input_type=type(data).__name__)
16271627
if not self.allow_empty and len(data) == 0:
16281628
self.fail('empty')
1629-
return [self.child.run_validation(item) for item in data]
1629+
return self.run_child_validation(data)
16301630

16311631
def to_representation(self, data):
16321632
"""
16331633
List of object instances -> List of dicts of primitive datatypes.
16341634
"""
16351635
return [self.child.to_representation(item) if item is not None else None for item in data]
16361636

1637+
def run_child_validation(self, data):
1638+
result = []
1639+
errors = OrderedDict()
1640+
1641+
for idx, item in enumerate(data):
1642+
try:
1643+
result.append(self.child.run_validation(item))
1644+
except ValidationError as e:
1645+
errors[idx] = e.detail
1646+
1647+
if not errors:
1648+
return result
1649+
raise ValidationError(errors)
1650+
16371651

16381652
class DictField(Field):
16391653
child = _UnvalidatedField()
@@ -1669,10 +1683,7 @@ def to_internal_value(self, data):
16691683
data = html.parse_html_dict(data)
16701684
if not isinstance(data, dict):
16711685
self.fail('not_a_dict', input_type=type(data).__name__)
1672-
return {
1673-
six.text_type(key): self.child.run_validation(value)
1674-
for key, value in data.items()
1675-
}
1686+
return self.run_child_validation(data)
16761687

16771688
def to_representation(self, value):
16781689
"""
@@ -1683,6 +1694,22 @@ def to_representation(self, value):
16831694
for key, val in value.items()
16841695
}
16851696

1697+
def run_child_validation(self, data):
1698+
result = {}
1699+
errors = OrderedDict()
1700+
1701+
for key, value in data.items():
1702+
key = six.text_type(key)
1703+
1704+
try:
1705+
result[key] = self.child.run_validation(value)
1706+
except ValidationError as e:
1707+
errors[key] = e.detail
1708+
1709+
if not errors:
1710+
return result
1711+
raise ValidationError(errors)
1712+
16861713

16871714
class JSONField(Field):
16881715
default_error_messages = {

tests/test_fields.py

Lines changed: 41 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1767,7 +1767,7 @@ class TestListField(FieldValues):
17671767
]
17681768
invalid_inputs = [
17691769
('not a list', ['Expected a list of items but got type "str".']),
1770-
([1, 2, 'error'], ['A valid integer is required.']),
1770+
([1, 2, 'error', 'error'], {2: ['A valid integer is required.'], 3: ['A valid integer is required.']}),
17711771
({'one': 'two'}, ['Expected a list of items but got type "dict".'])
17721772
]
17731773
outputs = [
@@ -1794,6 +1794,25 @@ def test_collection_types_are_invalid_input(self):
17941794
assert exc_info.value.detail == ['Expected a list of items but got type "dict".']
17951795

17961796

1797+
class TestNestedListField(FieldValues):
1798+
"""
1799+
Values for nested `ListField` with IntegerField as child.
1800+
"""
1801+
valid_inputs = [
1802+
([[1, 2], [3]], [[1, 2], [3]]),
1803+
([[]], [[]])
1804+
]
1805+
invalid_inputs = [
1806+
(['not a list'], {0: ['Expected a list of items but got type "str".']}),
1807+
([[1, 2, 'error'], ['error']], {0: {2: ['A valid integer is required.']}, 1: {0: ['A valid integer is required.']}}),
1808+
([{'one': 'two'}], {0: ['Expected a list of items but got type "dict".']})
1809+
]
1810+
outputs = [
1811+
([[1, 2], [3]], [[1, 2], [3]]),
1812+
]
1813+
field = serializers.ListField(child=serializers.ListField(child=serializers.IntegerField()))
1814+
1815+
17971816
class TestEmptyListField(FieldValues):
17981817
"""
17991818
Values for `ListField` with allow_empty=False flag.
@@ -1834,13 +1853,13 @@ class TestUnvalidatedListField(FieldValues):
18341853

18351854
class TestDictField(FieldValues):
18361855
"""
1837-
Values for `ListField` with CharField as child.
1856+
Values for `DictField` with CharField as child.
18381857
"""
18391858
valid_inputs = [
18401859
({'a': 1, 'b': '2', 3: 3}, {'a': '1', 'b': '2', '3': '3'}),
18411860
]
18421861
invalid_inputs = [
1843-
({'a': 1, 'b': None}, ['This field may not be null.']),
1862+
({'a': 1, 'b': None, 'c': None}, {'b': ['This field may not be null.'], 'c': ['This field may not be null.']}),
18441863
('not a dict', ['Expected a dictionary of items but got type "str".']),
18451864
]
18461865
outputs = [
@@ -1866,9 +1885,26 @@ def test_allow_null(self):
18661885
assert output is None
18671886

18681887

1888+
class TestNestedDictField(FieldValues):
1889+
"""
1890+
Values for nested `DictField` with CharField as child.
1891+
"""
1892+
valid_inputs = [
1893+
({0: {'a': 1, 'b': '2'}, 1: {3: 3}}, {'0': {'a': '1', 'b': '2'}, '1': {'3': '3'}}),
1894+
]
1895+
invalid_inputs = [
1896+
({0: {'a': 1, 'b': None}, 1: {'c': None}}, {'0': {'b': ['This field may not be null.']}, '1': {'c': ['This field may not be null.']}}),
1897+
({0: 'not a dict'}, {'0': ['Expected a dictionary of items but got type "str".']}),
1898+
]
1899+
outputs = [
1900+
({0: {'a': 1, 'b': '2'}, 1: {3: 3}}, {'0': {'a': '1', 'b': '2'}, '1': {'3': '3'}}),
1901+
]
1902+
field = serializers.DictField(child=serializers.DictField(child=serializers.CharField()))
1903+
1904+
18691905
class TestDictFieldWithNullChild(FieldValues):
18701906
"""
1871-
Values for `ListField` with allow_null CharField as child.
1907+
Values for `DictField` with allow_null CharField as child.
18721908
"""
18731909
valid_inputs = [
18741910
({'a': None, 'b': '2', 3: 3}, {'a': None, 'b': '2', '3': '3'}),
@@ -1883,7 +1919,7 @@ class TestDictFieldWithNullChild(FieldValues):
18831919

18841920
class TestUnvalidatedDictField(FieldValues):
18851921
"""
1886-
Values for `ListField` with no `child` argument.
1922+
Values for `DictField` with no `child` argument.
18871923
"""
18881924
valid_inputs = [
18891925
({'a': 1, 'b': [4, 5, 6], 1: 123}, {'a': 1, 'b': [4, 5, 6], '1': 123}),

0 commit comments

Comments
 (0)