From 6bd773e7f8bf3535ee51e67708f4396c00f78a1d Mon Sep 17 00:00:00 2001 From: Ryan P Kilby Date: Tue, 2 Jan 2018 04:45:59 -0500 Subject: [PATCH] Improve composite field child errors (#5655) * Fixup DictField test descriptions * Nest ListField/DictField errors under the idx/key * Add nested ListField/DictField tests --- rest_framework/fields.py | 37 +++++++++++++++++++++++++++----- tests/test_fields.py | 46 +++++++++++++++++++++++++++++++++++----- 2 files changed, 73 insertions(+), 10 deletions(-) diff --git a/rest_framework/fields.py b/rest_framework/fields.py index a710df7b4..9b88784c8 100644 --- a/rest_framework/fields.py +++ b/rest_framework/fields.py @@ -1626,7 +1626,7 @@ class ListField(Field): self.fail('not_a_list', input_type=type(data).__name__) if not self.allow_empty and len(data) == 0: self.fail('empty') - return [self.child.run_validation(item) for item in data] + return self.run_child_validation(data) def to_representation(self, data): """ @@ -1634,6 +1634,20 @@ class ListField(Field): """ return [self.child.to_representation(item) if item is not None else None for item in data] + def run_child_validation(self, data): + result = [] + errors = OrderedDict() + + for idx, item in enumerate(data): + try: + result.append(self.child.run_validation(item)) + except ValidationError as e: + errors[idx] = e.detail + + if not errors: + return result + raise ValidationError(errors) + class DictField(Field): child = _UnvalidatedField() @@ -1669,10 +1683,7 @@ class DictField(Field): data = html.parse_html_dict(data) if not isinstance(data, dict): self.fail('not_a_dict', input_type=type(data).__name__) - return { - six.text_type(key): self.child.run_validation(value) - for key, value in data.items() - } + return self.run_child_validation(data) def to_representation(self, value): """ @@ -1683,6 +1694,22 @@ class DictField(Field): for key, val in value.items() } + def run_child_validation(self, data): + result = {} + errors = OrderedDict() + + for key, value in data.items(): + key = six.text_type(key) + + try: + result[key] = self.child.run_validation(value) + except ValidationError as e: + errors[key] = e.detail + + if not errors: + return result + raise ValidationError(errors) + class JSONField(Field): default_error_messages = { diff --git a/tests/test_fields.py b/tests/test_fields.py index fc9ce192a..bc11cd133 100644 --- a/tests/test_fields.py +++ b/tests/test_fields.py @@ -1767,7 +1767,7 @@ class TestListField(FieldValues): ] invalid_inputs = [ ('not a list', ['Expected a list of items but got type "str".']), - ([1, 2, 'error'], ['A valid integer is required.']), + ([1, 2, 'error', 'error'], {2: ['A valid integer is required.'], 3: ['A valid integer is required.']}), ({'one': 'two'}, ['Expected a list of items but got type "dict".']) ] outputs = [ @@ -1794,6 +1794,25 @@ class TestListField(FieldValues): assert exc_info.value.detail == ['Expected a list of items but got type "dict".'] +class TestNestedListField(FieldValues): + """ + Values for nested `ListField` with IntegerField as child. + """ + valid_inputs = [ + ([[1, 2], [3]], [[1, 2], [3]]), + ([[]], [[]]) + ] + invalid_inputs = [ + (['not a list'], {0: ['Expected a list of items but got type "str".']}), + ([[1, 2, 'error'], ['error']], {0: {2: ['A valid integer is required.']}, 1: {0: ['A valid integer is required.']}}), + ([{'one': 'two'}], {0: ['Expected a list of items but got type "dict".']}) + ] + outputs = [ + ([[1, 2], [3]], [[1, 2], [3]]), + ] + field = serializers.ListField(child=serializers.ListField(child=serializers.IntegerField())) + + class TestEmptyListField(FieldValues): """ Values for `ListField` with allow_empty=False flag. @@ -1834,13 +1853,13 @@ class TestUnvalidatedListField(FieldValues): class TestDictField(FieldValues): """ - Values for `ListField` with CharField as child. + Values for `DictField` with CharField as child. """ valid_inputs = [ ({'a': 1, 'b': '2', 3: 3}, {'a': '1', 'b': '2', '3': '3'}), ] invalid_inputs = [ - ({'a': 1, 'b': None}, ['This field may not be null.']), + ({'a': 1, 'b': None, 'c': None}, {'b': ['This field may not be null.'], 'c': ['This field may not be null.']}), ('not a dict', ['Expected a dictionary of items but got type "str".']), ] outputs = [ @@ -1866,9 +1885,26 @@ class TestDictField(FieldValues): assert output is None +class TestNestedDictField(FieldValues): + """ + Values for nested `DictField` with CharField as child. + """ + valid_inputs = [ + ({0: {'a': 1, 'b': '2'}, 1: {3: 3}}, {'0': {'a': '1', 'b': '2'}, '1': {'3': '3'}}), + ] + invalid_inputs = [ + ({0: {'a': 1, 'b': None}, 1: {'c': None}}, {'0': {'b': ['This field may not be null.']}, '1': {'c': ['This field may not be null.']}}), + ({0: 'not a dict'}, {'0': ['Expected a dictionary of items but got type "str".']}), + ] + outputs = [ + ({0: {'a': 1, 'b': '2'}, 1: {3: 3}}, {'0': {'a': '1', 'b': '2'}, '1': {'3': '3'}}), + ] + field = serializers.DictField(child=serializers.DictField(child=serializers.CharField())) + + class TestDictFieldWithNullChild(FieldValues): """ - Values for `ListField` with allow_null CharField as child. + Values for `DictField` with allow_null CharField as child. """ valid_inputs = [ ({'a': None, 'b': '2', 3: 3}, {'a': None, 'b': '2', '3': '3'}), @@ -1883,7 +1919,7 @@ class TestDictFieldWithNullChild(FieldValues): class TestUnvalidatedDictField(FieldValues): """ - Values for `ListField` with no `child` argument. + Values for `DictField` with no `child` argument. """ valid_inputs = [ ({'a': 1, 'b': [4, 5, 6], 1: 123}, {'a': 1, 'b': [4, 5, 6], '1': 123}),