django-rest-framework
django-rest-framework copied to clipboard
List Serializer Breaks on Unique Constraint Validator
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'
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.
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.
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
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'
Bump, this is the important issue for ANY serializer which is using model with
UniqueConstraintandmany=TrueThis 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.errorsAnd the error will be:
AttributeError: 'list' object has no attribute 'pk'
is your pr going to fix this?
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?
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.