Skip to content
Open
Show file tree
Hide file tree
Changes from all 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
5 changes: 5 additions & 0 deletions rest_framework/serializers.py
Original file line number Diff line number Diff line change
Expand Up @@ -1523,6 +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
elif unique_constraint_field.blank:
if isinstance(unique_constraint_field, (models.CharField, models.TextField)):
default = ''
else:
default = empty
else:
default = empty

Expand Down
70 changes: 70 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,22 @@ 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(tag=''),
)
]


class UniqueConstraintReadOnlyFieldModel(models.Model):
state = models.CharField(max_length=100, default="new")
position = models.IntegerField()
Expand Down Expand Up @@ -642,6 +700,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 +819,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