diff --git a/rest_framework/fields.py b/rest_framework/fields.py index 9b922ed918..d56cfd4a20 100644 --- a/rest_framework/fields.py +++ b/rest_framework/fields.py @@ -473,7 +473,7 @@ def get_default(self): raise `SkipField`, indicating that no value should be set in the validated data for this field. """ - if self.default is empty or getattr(self.root, 'partial', False): + if self.default is empty or getattr(self.parent, 'partial', False): # No default, or this is a partial update. raise SkipField() if callable(self.default): @@ -499,7 +499,7 @@ def validate_empty_values(self, data): return (True, self.get_default()) if data is empty: - if getattr(self.root, 'partial', False): + if getattr(self.parent, 'partial', False): raise SkipField() if self.required: self.fail('required') diff --git a/rest_framework/serializers.py b/rest_framework/serializers.py index adb6a65b5b..f1f368894f 100644 --- a/rest_framework/serializers.py +++ b/rest_framework/serializers.py @@ -115,11 +115,21 @@ def __init__(self, instance=None, data=empty, **kwargs): self.instance = instance if data is not empty: self.initial_data = data - self.partial = kwargs.pop('partial', False) + self._partial = kwargs.pop('partial', None) self._context = kwargs.pop('context', {}) kwargs.pop('many', None) super().__init__(**kwargs) + @property + def partial(self): + if self._partial is not None: + return self._partial + return getattr(self.parent, 'partial', False) + + @partial.setter + def partial(self, value): + self._partial = value + def __new__(cls, *args, **kwargs): # We override this method in order to automatically create # `ListSerializer` classes instead when `many=True` is set. diff --git a/tests/test_serializer.py b/tests/test_serializer.py index fac49dcb94..4a974e38e8 100644 --- a/tests/test_serializer.py +++ b/tests/test_serializer.py @@ -829,3 +829,52 @@ class Meta: with pytest.raises(ValueError) as exc_info: serializer.is_valid(raise_exception=True) assert str(exc_info.value) == error_msg + + +class TestNestedSerializerPartial: + def test_nested_serializer_with_partial_false(self): + class LocationSerializer(serializers.Serializer): + country = serializers.CharField() + city = serializers.CharField() + + class UserSerializer(serializers.Serializer): + username = serializers.CharField() + location = LocationSerializer(partial=False) + + # 1. When location is provided as empty dict, required fields must not be skipped + serializer = UserSerializer(data={'location': {}}, partial=True) + assert not serializer.is_valid() + assert serializer.errors == { + 'location': { + 'country': ['This field is required.'], + 'city': ['This field is required.'] + } + } + + # 2. When location is provided with only one field, the other required field must not be skipped + serializer2 = UserSerializer(data={'location': {'city': 'Paris'}}, partial=True) + assert not serializer2.is_valid() + assert serializer2.errors == { + 'location': { + 'country': ['This field is required.'] + } + } + + # 3. When location is omitted entirely, the location field itself is skipped on partial update + serializer3 = UserSerializer(data={'username': 'alice'}, partial=True) + assert serializer3.is_valid() + assert serializer3.validated_data == {'username': 'alice'} + + def test_nested_serializer_inherits_partial_by_default(self): + class LocationSerializer(serializers.Serializer): + country = serializers.CharField() + city = serializers.CharField() + + class UserSerializer(serializers.Serializer): + username = serializers.CharField() + location = LocationSerializer() + + # When partial is not specified on the nested serializer, it should inherit partial=True + serializer = UserSerializer(data={'location': {'city': 'Paris'}}, partial=True) + assert serializer.is_valid() + assert serializer.validated_data == {'location': {'city': 'Paris'}}