django-rest-framework icon indicating copy to clipboard operation
django-rest-framework copied to clipboard

List Serializer Breaks on Unique Constraint Validator

Open max-muoto opened this issue 1 year ago • 3 comments

Checklist

3.15 added support for unique constraint validators in serializers: https://github.com/encode/django-rest-framework/pull/7438

This unfortunately breaks when it comes to list serializers. It could make sense these not to run for list serializers if it's difficult to do so in an efficient manner without an O(N) query, and document that fact.

Reproducible Example

from django.db import models
from django.db.models import UniqueConstraint, Q
from rest_framework import serializers

class Pet(models.Model):
    name = models.CharField(max_length=100)
    animal_type = models.CharField(max_length=100)
    can_fly = models.BooleanField(null=True)

    class Meta:
        constraints = [
            UniqueConstraint(
                fields=["name", "animal_type"],
                name="unique_pet",
            )
        ]

class PetSerializer(serializers.ModelSerializer):
    class Meta:
        model = Pet
        fields = ('name', 'animal_type', 'can_fly')

class PetListSerializer(serializers.ListSerializer):
    child = PetSerializer()

    def create(self, validated_data):
        pets = [Pet(**item) for item in validated_data]
        return Pet.objects.bulk_create(pets)

instances = Pet.objects.all()
new_data = [
    {"name": "Polly", "animal_type": "Parrot", "can_fly": True},
    {"name": "Penguin", "animal_type": "Bird", "can_fly": False},
]
serializer = PetListSerializer(instances, data=new_data, many=True)
if serializer.is_valid(raise_exception=True):
    serializer.save()  # This will create all pets in a single query

Running this will yield

AttributeError: 'BaseQuerySet' object has no attribute 'pk'

max-muoto avatar Jul 31 '24 18:07 max-muoto

Hi @max-muoto . I'm not sure to understand why you are trying to introduce PetListSerializer. It's DRF job to use ListSerializer once you will use PetSerializer with many=True. Also, if your intent is to create new pets in bulk ops, you haven't to pass instance to PetSerializer. Just data.

Your code could look like

# pet/models.py

from django.db import models
from django.db.models import UniqueConstraint


class Pet(models.Model):
    name = models.CharField(max_length=100)
    animal_type = models.CharField(max_length=100)
    can_fly = models.BooleanField(null=True)

    class Meta:
        constraints = [
            UniqueConstraint(
                fields=["name", "animal_type"],
                name="unique_pet",
            )
        ]

# pet/serializers.py

from rest_framework import serializers
from pet.models import Pet


class PetSerializer(serializers.ModelSerializer):
    class Meta:
        model = Pet
        fields = ("name", "animal_type", "can_fly")

# in your view, you have to write

s = PetSerializer(data=[{"name":"Bob", "animal_type":"mammal", "can_fly":False}, {"name":"Drake", "animal_type": "not mamal"}], many=True)
s.is_valid()
s.save() # pets are created here

# if you try to create same entries again 
s = PetSerializer(data=[{"name":"Bob", "animal_type":"mammal", "can_fly":False}, {"name":"Drake", "animal_type": "not mamal"}], many=True)
s.is_valid() # False
s.errors # [{'non_field_errors': [ErrorDetail(string='The fields name, animal_type must make a unique set.', code='unique')]}, {'non_field_errors': [ErrorDetail(string='The fields name, animal_type must make a unique set.', code='unique')]}]

I hope this helps you.

ghost avatar Aug 25 '24 14:08 ghost

I've a similar issue with 3.15.2, but updating:

        instances = self.get_queryset()
        serializer = self.get_serializer(instances, data=data, partial=False, many=True)
        serializer.is_valid(raise_exception=True)

rest_framework/validators.py", line 153, in exclude_current_instance return queryset.exclude(pk=instance.pk) ^^^^^^^^^^^ AttributeError: 'MyQueryset' object has no attribute 'pk'

It's also a bulk operation but for updates.

  • self.many = True
  • self.instance contains 'MyQueryset'
  • self.initial_data - data to update
  • model has models.UniqueConstraint with multiple fields

the problem could be in my implementation, but it wasn’t failing in version 3.14.

SorianoMarmol avatar Oct 22 '24 10:10 SorianoMarmol

I can confirm that if i change my constraint to unique_together instead of UniqueConstraint the issue also happens with 3.14 so in this case could be my implementation

SorianoMarmol avatar Oct 22 '24 12:10 SorianoMarmol

Bump, this is the important issue for ANY serializer which is using model with UniqueConstraint and many=True

This is the easy reproducible example from your tests:

    def test_update_many_fails(self):
        instances = NullUniquenessTogetherModel.objects.bulk_create(
            [
                NullUniquenessTogetherModel(
                    date_of_birth=datetime.date(2000, 1, 1),
                    race_name='Paris Marathon',
                    position=1
                ),
                NullUniquenessTogetherModel(
                    date_of_birth=datetime.date(2000, 1, 1),
                    race_name='Paris Marathon',
                    position=2
                )
            ]
        )
        data = [{'date': 'None', 'race_name': 'Paris Marathon 1'}, {'date': None, 'race_name': 'Paris Marathon 2'}]
        serializer = NullUniquenessTogetherSerializer(instance=instances, data=data, many=True)
        assert serializer.is_valid(), serializer.errors

And the error will be:

AttributeError: 'list' object has no attribute 'pk'

sshishov avatar Aug 02 '25 17:08 sshishov

Bump, this is the important issue for ANY serializer which is using model with UniqueConstraint and many=True

This is the easy reproducible example from your tests:

def test_update_many_fails(self):
    instances = NullUniquenessTogetherModel.objects.bulk_create(
        [
            NullUniquenessTogetherModel(
                date_of_birth=datetime.date(2000, 1, 1),
                race_name='Paris Marathon',
                position=1
            ),
            NullUniquenessTogetherModel(
                date_of_birth=datetime.date(2000, 1, 1),
                race_name='Paris Marathon',
                position=2
            )
        ]
    )
    data = [{'date': 'None', 'race_name': 'Paris Marathon 1'}, {'date': None, 'race_name': 'Paris Marathon 2'}]
    serializer = NullUniquenessTogetherSerializer(instance=instances, data=data, many=True)
    assert serializer.is_valid(), serializer.errors

And the error will be:

AttributeError: 'list' object has no attribute 'pk'

is your pr going to fix this?

auvipy avatar Aug 03 '25 11:08 auvipy

Hi @auvipy , I do not have a PR for this unfortunately... But I guess when many=True is used, we should not call exclude_self or we should properly handle the Iterable.

What do you think?

sshishov avatar Aug 03 '25 11:08 sshishov

This issue has been automatically marked as stale because it has not had recent activity. It will be closed if no further activity occurs. Thank you for your contributions.

stale[bot] avatar Oct 18 '25 05:10 stale[bot]