Skip to content

Conversation

@sshishov
Copy link
Contributor

@sshishov sshishov commented Aug 2, 2025

Description

This MR should fix the issue with blank=True field in UniqueTogether constraint. It should not become required all of a sudden.

Fixes #9750

@sshishov sshishov force-pushed the ss/fix/allow-blank-unique-constraint branch from 3bcd191 to 8de6a6c Compare August 2, 2025 12:56
@sshishov
Copy link
Contributor Author

sshishov commented Aug 2, 2025

I have added tests similar to the tests added in #9531

Copy link
Collaborator

@auvipy auvipy left a comment

Choose a reason for hiding this comment

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

did you check this pr #9725 ?

class Meta:
constraints = [
# Unique constraint on 2 blank fields
models.UniqueConstraint(name='unique_constraint', fields=('age', 'tag'), condition=~models.Q(models.Q(title='') & models.Q(tag='True')))
Copy link
Contributor

Choose a reason for hiding this comment

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

Don't we have a char length limit here?


def test_blank_uqnique_constraint_fields_are_not_required(self):
serializer = UniqueConstraintBlankSerializer(data={'age': 25})
self.assertTrue(serializer.is_valid(), serializer.errors)
Copy link
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?

ids_in_qs = {frozenset(v.queryset.values_list(flat=True)) for v in validators if hasattr(v, "queryset")}
assert ids_in_qs == {frozenset([1]), frozenset([3])}

def test_blank_uqnique_constraint_fields_are_not_required(self):
Copy link
Contributor

Choose a reason for hiding this comment

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

Typo here

@mahdighadiriii
Copy link
Contributor

Hi @sshishov @auvipy @deepakangadi,
I'm excited to contribute to DRF as my first contribution! I noticed this PR (#9751) addresses a critical issue (#9750) with blank=True fields in UniqueConstraints, which aligns with my experience in Django/DRF serializers and API optimization.

I can help unblock this by:

  • Fixing the typo in test_blank_uqnique_constraint_fields_are_not_required (uqniqueunique).
  • Splitting assertions in the test for clarity, as suggested by @deepakangadi.
  • Adding char length limit (e.g., max_length=255) to the constraint.
  • Resolving conflicts by rebasing on main.
  • Checking for overlap with PR docs: unique constraints cause 'required=True' #9725, as mentioned by @auvipy.

If this sounds good, I can start working on these changes. Should I update this branch (if permitted) or create a new PR? Thanks for your guidance!

@stale
Copy link

stale bot commented Oct 18, 2025

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.

Copy link

Copilot AI left a comment

Choose a reason for hiding this comment

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

Pull request overview

This PR fixes an issue where blank=True fields in unique_together constraints were incorrectly becoming required. The fix adds special handling for blank fields by setting their default value to an empty string, similar to how null=True fields default to None. This ensures that blank fields in uniqueness constraints remain optional while still properly validating uniqueness.

Key Changes

  • Added default='' handling for blank fields in unique constraints in get_uniqueness_extra_kwargs
  • Added comprehensive test coverage for both unique_together and UniqueConstraint with blank fields
  • Tests verify that blank fields are not required, while still enforcing uniqueness validation

Reviewed changes

Copilot reviewed 2 out of 2 changed files in this pull request and generated 5 comments.

File Description
rest_framework/serializers.py Added conditional logic to set default='' for blank fields in the uniqueness constraint handling, following the same pattern as null field handling
tests/test_validators.py Added new test models (BlankUniquenessTogetherModel, UniqueConstraintBlankModel) and test cases to verify blank fields in uniqueness constraints are not required but still validate correctly

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment on lines +152 to +158
class BlankUniquenessTogetherModel(models.Model):
race_name = models.CharField(max_length=100, blank=True)
position = models.IntegerField()

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

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 Meta:
constraints = [
# Unique constraint on 2 blank fields
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 comment "Unique constraint on 2 blank fields" is misleading. The constraint is on 'age' and 'tag', but 'age' is an IntegerField without blank=True. Only 'tag' has blank=True. Consider updating the comment to accurately reflect that the constraint includes one required field (age) and one blank field (tag).

Suggested change
# Unique constraint on 2 blank fields
# Unique constraint on one required field (age) and one blank field (tag)

Copilot uses AI. Check for mistakes.
Comment on lines +1500 to 1502
default = ''
else:
default = empty
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 field type check before setting default to empty string. The Django blank attribute is primarily meaningful for text fields (CharField, TextField). For other field types with blank=True, setting default='' could cause type errors. While this is an edge case (as non-text fields with blank=True typically also have null=True or an explicit default, which are handled earlier in the condition chain), adding a check like isinstance(unique_constraint_field, (models.CharField, models.TextField)) would make the code more defensive and consistent with the pattern in utils/field_mapping.py lines 133-134.

Suggested change
default = ''
else:
default = empty
if isinstance(unique_constraint_field, (models.CharField, models.TextField)):
default = ''
else:
default = empty

Copilot uses AI. Check for mistakes.
ids_in_qs = {frozenset(v.queryset.values_list(flat=True)) for v in validators if hasattr(v, "queryset")}
assert ids_in_qs == {frozenset([1]), frozenset([3])}

def test_blank_uqnique_constraint_fields_are_not_required(self):
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.

Typo in test method name: "uqnique" should be "unique".

Suggested change
def test_blank_uqnique_constraint_fields_are_not_required(self):
def test_blank_unique_constraint_fields_are_not_required(self):

Copilot uses AI. Check for mistakes.
Comment on lines +489 to +505
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

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.
Copy link
Collaborator

@auvipy auvipy left a comment

Choose a reason for hiding this comment

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

please also fix the merge conflicts

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3.15 is raising required error on model blank fields

5 participants