diff --git a/rest_framework/serializers.py b/rest_framework/serializers.py index 8fe284bc84..faff64c5bc 100644 --- a/rest_framework/serializers.py +++ b/rest_framework/serializers.py @@ -652,6 +652,52 @@ def run_child_validation(self, data): self.child.initial_data = data return super().run_child_validation(data) """ + child_instance = getattr(self.child, "instance", None) + + if self.instance is not None: + pk_name = None + child_meta = getattr(self.child, "Meta", None) + model = getattr(child_meta, "model", None) if child_meta else None + + if model is not None: + pk_name = model._meta.pk.name + + obj_id = None + if pk_name: + for field_name, field in self.child.fields.items(): + if getattr(field, "source", None) == pk_name: + obj_id = data.get(field_name) + if obj_id is not None: + break + + if obj_id is None: + obj_id = data.get(pk_name) or data.get("pk") or data.get("id") + + resolved_instance = None + + if obj_id is not None and pk_name: + try: + obj_id = model._meta.pk.to_python(obj_id) + except Exception: + pass + + if not hasattr(self, "_instance_index"): + self._instance_index = { + getattr(obj, pk_name): obj for obj in self.instance + } + + resolved_instance = self._instance_index.get(obj_id) + + if resolved_instance is None: + if model is not None and self.context.get("allow_create", True): + resolved_instance = model() + else: + resolved_instance = child_instance + + child_instance = resolved_instance + + self.child.instance = child_instance + self.child.initial_data = data return self.child.run_validation(data) def to_internal_value(self, data): diff --git a/tests/models.py b/tests/models.py index 88e3d8dcaf..79a5308bcd 100644 --- a/tests/models.py +++ b/tests/models.py @@ -150,3 +150,30 @@ def __new__(cls, *args, **kwargs): help_text='OneToOneTarget', verbose_name='OneToOneTarget', on_delete=models.CASCADE) + + +class ListModelForTest(RESTFrameworkModel): + name = models.CharField(max_length=100) + status = models.CharField(max_length=100, blank=True) + + @property + def is_valid(self): + return self.name == 'valid' + + +class EmailPKModel(RESTFrameworkModel): + email = models.EmailField(primary_key=True) + name = models.CharField(max_length=100) + + @property + def is_valid(self): + return self.name == 'valid' + + +class PersonUUID(RESTFrameworkModel): + id = models.UUIDField(primary_key=True) + name = models.CharField(max_length=100) + + @property + def is_valid(self): + return self.name == 'valid' diff --git a/tests/test_serializer_lists.py b/tests/test_serializer_lists.py index 42ebf4771e..041cbb2edb 100644 --- a/tests/test_serializer_lists.py +++ b/tests/test_serializer_lists.py @@ -5,7 +5,8 @@ from rest_framework import serializers from rest_framework.exceptions import ErrorDetail from tests.models import ( - CustomManagerModel, NullableOneToOneSource, OneToOneTarget + CustomManagerModel, EmailPKModel, ListModelForTest, NullableOneToOneSource, + OneToOneTarget, PersonUUID ) @@ -775,3 +776,102 @@ def test(self): queryset = NullableOneToOneSource.objects.all() serializer = self.serializer(queryset, many=True) assert serializer.data + + +@pytest.mark.django_db +class TestManyTrueValidationCheck: + """ + Tests ListSerializer validation with many=True across different primary key types + (integer and email). + """ + + class PersonUUIDSerializer(serializers.ModelSerializer): + uuid = serializers.UUIDField(source="id") + + class Meta: + model = PersonUUID + fields = ("uuid", "name") + read_only_fields = ("uuid",) + + def validate_name(self, value): + if value and not self.instance.is_valid: + return False + return value + + def setup_method(self): + self.obj1 = ListModelForTest.objects.create(name="valid", status="new") + self.obj2 = ListModelForTest.objects.create(name="invalid", status="") + self.email_obj1 = EmailPKModel.objects.create(email="test@test.com", name="A") + self.email_obj2 = EmailPKModel.objects.create(email="test2@test.com", name="B") + + self.serializer, self.email_serializer = self.get_serializers() + + def get_serializers(self): + class ListModelForTestSerializer(serializers.ModelSerializer): + class Meta: + model = ListModelForTest + fields = ("id", "name", "status") + + def validate_status(self, value): + if value and not self.instance.is_valid: + return False + return value + + class EmailPKSerializer(serializers.ModelSerializer): + class Meta: + model = EmailPKModel + fields = ("email", "name") + read_only_fields = ('email',) + + def validate_name(self, value): + if value and not self.instance.is_valid: + return False + return value + + return ListModelForTestSerializer, EmailPKSerializer + + def test_run_child_validation_with_many_true(self): + input_data = [ + {"id": self.obj1.pk, "name": "other", "status": "new"}, + {"id": self.obj2.pk, "name": "valid", "status": "progress"}, + ] + + serializer = self.serializer([self.obj1, self.obj2], data=input_data, many=True) + assert serializer.is_valid(), serializer.errors + + serializer = self.serializer(ListModelForTest.objects.all(), data=input_data, many=True) + assert serializer.is_valid(), serializer.errors + + def test_validation_error_for_invalid_data(self): + input_data = [{"id": self.obj1.pk, "name": "", "status": "mystatus"}] + + serializer = self.serializer([self.obj1], data=input_data, many=True) + assert not serializer.is_valid() + assert "name" in serializer.errors[0] + + def test_email_pk_instance_validation(self): + input_data = [{"email": "test@test.com", "name": "bar"}] + serializer = self.email_serializer(instance=EmailPKModel.objects.all(), data=input_data, many=True) + assert serializer.is_valid(), serializer.errors + + def test_uuid_validate_many(self): + PersonUUID.objects.create(id="c20f2f31-65a3-451f-ae7d-e939b7d9f84b", name="valid") + PersonUUID.objects.create(id="3308237e-18d8-4074-9d05-79cc0fdb5bb3", name="other") + + input_data = [ + { + "uuid": "c20f2f31-65a3-451f-ae7d-e939b7d9f84b", + "name": "bar", + }, + ] + serializer = self.PersonUUIDSerializer(instance=list(PersonUUID.objects.all()), data=input_data, many=True) + assert serializer.is_valid(), serializer.errors + + def test_uuid_validate_single(self): + instance = PersonUUID.objects.create(id="c20f2f31-65a3-451f-ae7d-e939b7d9f84b", name="food") + + serializer = self.PersonUUIDSerializer( + instance=instance, + data={"uuid": "c20f2f31-65a3-451f-ae7d-e939b7d9f84b", "name": "valid"}, + ) + assert serializer.is_valid(), serializer.errors