diff --git a/rest_framework/fields.py b/rest_framework/fields.py index 58e28ed4c..38aac3640 100644 --- a/rest_framework/fields.py +++ b/rest_framework/fields.py @@ -1614,7 +1614,8 @@ class ListField(Field): if len(val) > 0: # Support QueryDict lists in HTML input. return val - return html.parse_html_list(dictionary, prefix=self.field_name) + return html.parse_html_list(dictionary, prefix=self.field_name, default=empty) + return dictionary.get(self.field_name, empty) def to_internal_value(self, data): diff --git a/rest_framework/serializers.py b/rest_framework/serializers.py index 7e84372de..34cb74a86 100644 --- a/rest_framework/serializers.py +++ b/rest_framework/serializers.py @@ -607,7 +607,7 @@ class ListSerializer(BaseSerializer): # We override the default field access in order to support # lists in HTML forms. if html.is_html_input(dictionary): - return html.parse_html_list(dictionary, prefix=self.field_name) + return html.parse_html_list(dictionary, prefix=self.field_name, default=empty) return dictionary.get(self.field_name, empty) def run_validation(self, data=empty): @@ -635,7 +635,7 @@ class ListSerializer(BaseSerializer): List of dicts of native values <- List of dicts of primitive datatypes. """ if html.is_html_input(data): - data = html.parse_html_list(data) + data = html.parse_html_list(data, default=empty) if not isinstance(data, list): message = self.error_messages['not_a_list'].format( diff --git a/rest_framework/utils/html.py b/rest_framework/utils/html.py index 77167e470..8fd6af567 100644 --- a/rest_framework/utils/html.py +++ b/rest_framework/utils/html.py @@ -12,7 +12,7 @@ def is_html_input(dictionary): return hasattr(dictionary, 'getlist') -def parse_html_list(dictionary, prefix=''): +def parse_html_list(dictionary, prefix='', default=[]): """ Used to support list values in HTML forms. Supports lists of primitives and/or dictionaries. @@ -44,6 +44,8 @@ def parse_html_list(dictionary, prefix=''): {'foo': 'abc', 'bar': 'def'}, {'foo': 'hij', 'bar': 'klm'} ] + + :returns a list of objects, or the value specified in ``default`` if the list is empty """ ret = {} regex = re.compile(r'^%s\[([0-9]+)\](.*)$' % re.escape(prefix)) @@ -59,7 +61,7 @@ def parse_html_list(dictionary, prefix=''): ret[index][key] = value else: ret[index] = MultiValueDict({key: [value]}) - return [ret[item] for item in sorted(ret)] + return [ret[item] for item in sorted(ret)] if len(ret.keys()) > 0 else default def parse_html_dict(dictionary, prefix=''): diff --git a/tests/test_serializer_nested.py b/tests/test_serializer_nested.py index 09b8dd105..1cd0caf85 100644 --- a/tests/test_serializer_nested.py +++ b/tests/test_serializer_nested.py @@ -202,3 +202,42 @@ class TestNestedSerializerWithList: assert serializer.is_valid() assert serializer.validated_data['nested']['example'] == {1, 2} + + +class TestNotRequiredNestedSerializerWithMany: + def setup(self): + class NestedSerializer(serializers.Serializer): + one = serializers.IntegerField(max_value=10) + + class TestSerializer(serializers.Serializer): + nested = NestedSerializer(required=False, many=True) + + self.Serializer = TestSerializer + + def test_json_validate(self): + input_data = {} + serializer = self.Serializer(data=input_data) + + # request is empty, therefor 'nested' should not be in serializer.data + assert serializer.is_valid() + assert 'nested' not in serializer.validated_data + + input_data = {'nested': [{'one': '1'}, {'one': 2}]} + serializer = self.Serializer(data=input_data) + assert serializer.is_valid() + assert 'nested' in serializer.validated_data + + def test_multipart_validate(self): + # leave querydict empty + input_data = QueryDict('') + serializer = self.Serializer(data=input_data) + + # the querydict is empty, therefor 'nested' should not be in serializer.data + assert serializer.is_valid() + assert 'nested' not in serializer.validated_data + + input_data = QueryDict('nested[0]one=1&nested[1]one=2') + + serializer = self.Serializer(data=input_data) + assert serializer.is_valid() + assert 'nested' in serializer.validated_data