Skip to content
Open
Show file tree
Hide file tree
Changes from 5 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 5 additions & 2 deletions rest_framework/serializers.py
Original file line number Diff line number Diff line change
Expand Up @@ -1523,8 +1523,11 @@ def get_uniqueness_extra_kwargs(self, field_names, declared_fields, extra_kwargs
default = unique_constraint_field.default
elif unique_constraint_field.null:
default = None
else:
default = empty
elif unique_constraint_field.blank:
if isinstance(unique_constraint_field, (models.CharField, models.TextField)):
default = ''
else:
default = empty

if unique_constraint_name in model_fields:
# The corresponding field is present in the serializer
Expand Down
69 changes: 69 additions & 0 deletions tests/test_validators.py
Original file line number Diff line number Diff line change
Expand Up @@ -149,6 +149,14 @@ class Meta:
unique_together = ('race_name', 'position')


class BlankUniquenessTogetherModel(models.Model):
race_name = models.CharField(max_length=100, blank=True)
position = models.IntegerField()

class Meta:
unique_together = ('race_name', 'position')

Comment on lines +152 to +158
Copy link

Copilot AI Dec 14, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Consider adding a docstring to BlankUniquenessTogetherModel similar to NullUniquenessTogetherModel (lines 160-172) to explain the purpose and expected behavior of blank fields in unique_together constraints.

Copilot uses AI. Check for mistakes.

class NullUniquenessTogetherModel(models.Model):
"""
Used to ensure that null values are not included when checking
Expand Down Expand Up @@ -176,6 +184,12 @@ class Meta:
fields = '__all__'


class BlankUniquenessTogetherSerializer(serializers.ModelSerializer):
class Meta:
model = BlankUniquenessTogetherModel
fields = '__all__'


class NullUniquenessTogetherSerializer(serializers.ModelSerializer):
class Meta:
model = NullUniquenessTogetherModel
Expand Down Expand Up @@ -461,6 +475,34 @@ def test_do_not_ignore_validation_for_null_fields(self):
serializer = NullUniquenessTogetherSerializer(data=data)
assert not serializer.is_valid()

def test_validation_for_provided_blank_fields(self):
BlankUniquenessTogetherModel.objects.create(
position=1
)
data = {
'race_name': '',
'position': 1
}
serializer = BlankUniquenessTogetherSerializer(data=data)
assert not serializer.is_valid()

def test_validation_for_missing_blank_fields(self):
BlankUniquenessTogetherModel.objects.create(
position=1
)
data = {
'position': 1
}
serializer = BlankUniquenessTogetherSerializer(data=data)
assert not serializer.is_valid()

def test_ignore_validation_for_missing_blank_fields(self):
data = {
'position': 1
}
serializer = BlankUniquenessTogetherSerializer(data=data)
assert serializer.is_valid(), serializer.errors

Comment on lines +489 to +505
Copy link

Copilot AI Dec 14, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The test names test_validation_for_missing_blank_fields and test_ignore_validation_for_missing_blank_fields are confusing because both refer to "missing blank fields" but have opposite expectations. Consider renaming to make the distinction clearer, such as test_validation_fails_for_duplicate_blank_defaults and test_validation_passes_when_no_duplicates.

Copilot uses AI. Check for mistakes.
def test_ignore_validation_for_unchanged_fields(self):
"""
If all fields in the unique together constraint are unchanged,
Expand Down Expand Up @@ -589,6 +631,21 @@ class Meta:
]


class UniqueConstraintBlankModel(models.Model):
title = models.CharField(max_length=100, blank=True)
age = models.IntegerField()
tag = models.CharField(max_length=100, blank=True)

class Meta:
constraints = [
# Unique constraint on one required field (age) and one blank field (tag)
models.UniqueConstraint(
name='unique_constraint',
fields=('age', 'tag'),
condition=~models.Q(models.Q(title='') & models.Q(tag='True')))
]


class UniqueConstraintReadOnlyFieldModel(models.Model):
state = models.CharField(max_length=100, default="new")
position = models.IntegerField()
Expand Down Expand Up @@ -642,6 +699,12 @@ class Meta:
fields = '__all__'


class UniqueConstraintBlankSerializer(serializers.ModelSerializer):
class Meta:
model = UniqueConstraintBlankModel
fields = ('title', 'age', 'tag')


class UniqueConstraintNullableSerializer(serializers.ModelSerializer):
class Meta:
model = UniqueConstraintNullableModel
Expand Down Expand Up @@ -755,6 +818,12 @@ def test_single_field_uniq_validators(self):
ids_in_qs = {frozenset(v.queryset.values_list('id', flat=True)) for v in validators if hasattr(v, "queryset")}
assert ids_in_qs == {frozenset([1]), frozenset([3])}

def test_blank_unique_constraint_fields_are_not_required(self):
serializer = UniqueConstraintBlankSerializer(data={'age': 25})
self.assertTrue(serializer.is_valid(), serializer.errors)
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can we split it into multiple assertions?

result = serializer.save()
self.assertIsInstance(result, UniqueConstraintBlankModel)

def test_nullable_unique_constraint_fields_are_not_required(self):
serializer = UniqueConstraintNullableSerializer(data={'title': 'Bob'})
self.assertTrue(serializer.is_valid(), serializer.errors)
Expand Down
Loading