From 97edc97a13969157f239a0dcb8eea9bfe1871557 Mon Sep 17 00:00:00 2001 From: Samiul Sk <9149283+kernelshard@users.noreply.github.com> Date: Wed, 7 Oct 2026 02:59:55 +0530 Subject: [PATCH] Fix nested serializer required field validation when partial=False (#2585) --- rest_framework/fields.py | 4 +-- rest_framework/serializers.py | 12 ++++++++- tests/test_serializer.py | 49 +++++++++++++++++++++++++++++++++++ 3 files changed, 62 insertions(+), 3 deletions(-) diff --git a/rest_framework/fields.py b/rest_framework/fields.py index 9eb2a50a8b..15e20c81d6 100644 --- a/rest_framework/fields.py +++ b/rest_framework/fields.py @@ -478,7 +478,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): @@ -504,7 +504,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 5f34b00194..281f63d5ec 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 ed8a749118..3e2784e0db 100644 --- a/tests/test_serializer.py +++ b/tests/test_serializer.py @@ -809,3 +809,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'}}