Fix DictField returning empty dict instead of empty for missing HTML input (#6234)

When using DictField with HTML form (multipart/form-data) input,
parse_html_dict always returned an empty MultiValueDict when no
matching keys were found. This made it impossible to distinguish
between an unspecified field and an empty input, causing issues
with required/default field handling.

This aligns parse_html_dict with parse_html_list by adding a
default parameter that is returned when no matching keys are found.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
Varun Chawla 2026-02-08 15:05:58 -08:00
parent 1b63dce808
commit 34065b70e7
4 changed files with 58 additions and 5 deletions

View File

@ -1749,7 +1749,7 @@ class DictField(Field):
# We override the default field access in order to support
# dictionaries in HTML forms.
if html.is_html_input(dictionary):
return html.parse_html_dict(dictionary, prefix=self.field_name)
return html.parse_html_dict(dictionary, prefix=self.field_name, default=empty)
return dictionary.get(self.field_name, empty)
def to_internal_value(self, data):
@ -1757,7 +1757,7 @@ class DictField(Field):
Dicts of native values <- Dicts of primitive datatypes.
"""
if html.is_html_input(data):
data = html.parse_html_dict(data)
data = html.parse_html_dict(data, default=data)
if not isinstance(data, dict):
self.fail('not_a_dict', input_type=type(data).__name__)
if not self.allow_empty and len(data) == 0:

View File

@ -428,7 +428,7 @@ class Serializer(BaseSerializer, metaclass=SerializerMetaclass):
# We override the default field access in order to support
# nested HTML forms.
if html.is_html_input(dictionary):
return html.parse_html_dict(dictionary, prefix=self.field_name) or empty
return html.parse_html_dict(dictionary, prefix=self.field_name, default=empty)
return dictionary.get(self.field_name, empty)
def run_validation(self, data=empty):

View File

@ -66,7 +66,7 @@ def parse_html_list(dictionary, prefix='', default=None):
return [ret[item] for item in sorted(ret)] if ret else default
def parse_html_dict(dictionary, prefix=''):
def parse_html_dict(dictionary, prefix='', default=None):
"""
Used to support dictionary values in HTML forms.
@ -81,6 +81,9 @@ def parse_html_dict(dictionary, prefix=''):
'email': 'example@example.com'
}
}
:returns a MultiValueDict of the parsed data, or the value specified in
``default`` if the dict field was not present in the input
"""
ret = MultiValueDict()
regex = re.compile(r'^%s\.(.+)$' % re.escape(prefix))
@ -92,4 +95,4 @@ def parse_html_dict(dictionary, prefix=''):
value = dictionary.getlist(field)
ret.setlist(key, value)
return ret
return ret if ret else default

View File

@ -2496,6 +2496,56 @@ class TestDictField(FieldValues):
assert exc_info.value.detail == ['This dictionary may not be empty.']
def test_querydict_dict_input(self):
"""
DictField should correctly parse HTML form (QueryDict) input
with dot-separated keys.
"""
class TestSerializer(serializers.Serializer):
data = serializers.DictField(child=serializers.CharField())
serializer = TestSerializer(data=QueryDict('data.a=1&data.b=2'))
assert serializer.is_valid()
assert serializer.validated_data == {'data': {'a': '1', 'b': '2'}}
def test_querydict_dict_input_no_values_uses_default(self):
"""
When no matching keys are present in the QueryDict and a default
is set, the field should return the default value.
"""
class TestSerializer(serializers.Serializer):
a = serializers.IntegerField(required=True)
data = serializers.DictField(default=lambda: {'x': 'y'})
serializer = TestSerializer(data=QueryDict('a=1'))
assert serializer.is_valid()
assert serializer.validated_data == {'a': 1, 'data': {'x': 'y'}}
def test_querydict_dict_input_no_values_no_default_and_not_required(self):
"""
When no matching keys are present in the QueryDict, there is no
default, and the field is not required, the field should be
skipped entirely from validated_data.
"""
class TestSerializer(serializers.Serializer):
data = serializers.DictField(required=False)
serializer = TestSerializer(data=QueryDict(''))
assert serializer.is_valid()
assert serializer.validated_data == {}
def test_querydict_dict_input_no_values_required(self):
"""
When no matching keys are present in the QueryDict and the field
is required, validation should fail.
"""
class TestSerializer(serializers.Serializer):
data = serializers.DictField(required=True)
serializer = TestSerializer(data=QueryDict(''))
assert not serializer.is_valid()
assert 'data' in serializer.errors
class TestNestedDictField(FieldValues):
"""