diff --git a/docs/api-guide/fields.md b/docs/api-guide/fields.md index d371bb8fd7..b2830d0c9d 100644 --- a/docs/api-guide/fields.md +++ b/docs/api-guide/fields.md @@ -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=, min_length=None, max_length=None)` +**Signature**: `ListField(child=, 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. @@ -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=)` +**Signature**: `DictField(child=, 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: @@ -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=)` +**Signature**: `HStoreField(child=, 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. diff --git a/rest_framework/fields.py b/rest_framework/fields.py index 5e3132074f..e2fb8cdc66 100644 --- a/rest_framework/fields.py +++ b/rest_framework/fields.py @@ -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, ( @@ -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): diff --git a/tests/test_fields.py b/tests/test_fields.py index 41d08bd5e5..e7f16c1780 100644 --- a/tests/test_fields.py +++ b/tests/test_fields.py @@ -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.']}), @@ -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): """