Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 6 additions & 3 deletions docs/api-guide/fields.md
Original file line number Diff line number Diff line change
Expand Up @@ -448,9 +448,10 @@ Requires either the `Pillow` package or `PIL` package. The `Pillow` package is

A field class that validates a list of objects.

**Signature**: `ListField(child=<A_FIELD_INSTANCE>, min_length=None, max_length=None)`
**Signature**: `ListField(child=<A_FIELD_INSTANCE>, allow_empty=True, min_length=None, max_length=None)`

- `child` - A field instance that should be used for validating the objects in the list. If this argument is not provided then objects in the list will not be validated.
- `allow_empty` - Designates if empty lists are allowed.
- `min_length` - Validates that the list contains no fewer than this number of elements.
- `max_length` - Validates that the list contains no more than this number of elements.

Expand All @@ -471,9 +472,10 @@ We can now reuse our custom `StringListField` class throughout our application,

A field class that validates a dictionary of objects. The keys in `DictField` are always assumed to be string values.

**Signature**: `DictField(child=<A_FIELD_INSTANCE>)`
**Signature**: `DictField(child=<A_FIELD_INSTANCE>, allow_empty=True)`

- `child` - A field instance that should be used for validating the values in the dictionary. If this argument is not provided then values in the mapping will not be validated.
- `allow_empty` - Designates if empty dictionaries are allowed.

For example, to create a field that validates a mapping of strings to strings, you would write something like this:

Expand All @@ -488,9 +490,10 @@ You can also use the declarative style, as with `ListField`. For example:

A preconfigured `DictField` that is compatible with Django's postgres `HStoreField`.

**Signature**: `HStoreField(child=<A_FIELD_INSTANCE>)`
**Signature**: `HStoreField(child=<A_FIELD_INSTANCE>, allow_empty=True)`

- `child` - A field instance that is used for validating the values in the dictionary. The default child field accepts both empty strings and null values.
- `allow_empty` - Designates if empty dictionaries are allowed.

Note that the child field **must** be an instance of `CharField`, as the hstore extension stores values as strings.

Expand Down
7 changes: 6 additions & 1 deletion rest_framework/fields.py
Original file line number Diff line number Diff line change
Expand Up @@ -1663,11 +1663,13 @@ class DictField(Field):
child = _UnvalidatedField()
initial = {}
default_error_messages = {
'not_a_dict': _('Expected a dictionary of items but got type "{input_type}".')
'not_a_dict': _('Expected a dictionary of items but got type "{input_type}".'),
'empty': _('This dictionary may not be empty.'),
}

def __init__(self, *args, **kwargs):
self.child = kwargs.pop('child', copy.deepcopy(self.child))
self.allow_empty = kwargs.pop('allow_empty', True)

assert not inspect.isclass(self.child), '`child` has not been instantiated.'
assert self.child.source is None, (
Expand All @@ -1693,6 +1695,9 @@ def to_internal_value(self, data):
data = html.parse_html_dict(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:
self.fail('empty')

return self.run_child_validation(data)

def to_representation(self, value):
Expand Down
11 changes: 11 additions & 0 deletions tests/test_fields.py
Original file line number Diff line number Diff line change
Expand Up @@ -1982,6 +1982,7 @@ class TestDictField(FieldValues):
"""
valid_inputs = [
({'a': 1, 'b': '2', 3: 3}, {'a': '1', 'b': '2', '3': '3'}),
({}, {}),
]
invalid_inputs = [
({'a': 1, 'b': None, 'c': None}, {'b': ['This field may not be null.'], 'c': ['This field may not be null.']}),
Expand Down Expand Up @@ -2009,6 +2010,16 @@ def test_allow_null(self):
output = field.run_validation(None)
assert output is None

def test_allow_empty_disallowed(self):
"""
If allow_empty is False then an empty dict is not a valid input.
"""
field = serializers.DictField(allow_empty=False)
with pytest.raises(serializers.ValidationError) as exc_info:
field.run_validation({})

assert exc_info.value.detail == ['This dictionary may not be empty.']


class TestNestedDictField(FieldValues):
"""
Expand Down