From 8de6a6ca60ade50360e3065369940ea375bde421 Mon Sep 17 00:00:00 2001 From: Sergei Shishov Date: Sat, 2 Aug 2025 15:45:34 +0300 Subject: [PATCH 1/5] fix(blank/unique): add `default` handling for `blank=True` field Signed-off-by: Sergei Shishov --- rest_framework/serializers.py | 2 ++ tests/test_validators.py | 66 +++++++++++++++++++++++++++++++++++ 2 files changed, 68 insertions(+) diff --git a/rest_framework/serializers.py b/rest_framework/serializers.py index 8fe284bc84..f23d663ccb 100644 --- a/rest_framework/serializers.py +++ b/rest_framework/serializers.py @@ -1496,6 +1496,8 @@ 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: + default = '' else: default = empty diff --git a/tests/test_validators.py b/tests/test_validators.py index c594eecbe5..8dda7f3a98 100644 --- a/tests/test_validators.py +++ b/tests/test_validators.py @@ -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') + + class NullUniquenessTogetherModel(models.Model): """ Used to ensure that null values are not included when checking @@ -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 @@ -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 + def test_ignore_validation_for_unchanged_fields(self): """ If all fields in the unique together constraint are unchanged, @@ -589,6 +631,18 @@ 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 2 blank fields + models.UniqueConstraint(name='unique_constraint', fields=('age', 'tag'), condition=~models.Q(models.Q(title='') & models.Q(tag='True'))) + ] + + class UniqueConstraintNullableModel(models.Model): title = models.CharField(max_length=100) age = models.IntegerField(null=True) @@ -607,6 +661,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 @@ -714,6 +774,12 @@ def test_single_field_uniq_validators(self): 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): + serializer = UniqueConstraintBlankSerializer(data={'age': 25}) + self.assertTrue(serializer.is_valid(), serializer.errors) + 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) From 1e23e2b3afdacbaf28ed4f2c0a023aa658035983 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Asif=20Saif=20Uddin=20=7B=22Auvi=22=3A=22=E0=A6=85?= =?UTF-8?q?=E0=A6=AD=E0=A6=BF=22=7D?= Date: Thu, 2 Apr 2026 14:06:40 +0600 Subject: [PATCH 2/5] Update tests/test_validators.py Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- tests/test_validators.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/test_validators.py b/tests/test_validators.py index 8dda7f3a98..938fb044ee 100644 --- a/tests/test_validators.py +++ b/tests/test_validators.py @@ -638,7 +638,7 @@ class UniqueConstraintBlankModel(models.Model): class Meta: constraints = [ - # Unique constraint on 2 blank fields + # 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'))) ] From 2c5f53c66b75b309b488aff791cb01cf68b1f027 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Asif=20Saif=20Uddin=20=7B=22Auvi=22=3A=22=E0=A6=85?= =?UTF-8?q?=E0=A6=AD=E0=A6=BF=22=7D?= Date: Thu, 2 Apr 2026 14:07:34 +0600 Subject: [PATCH 3/5] Update rest_framework/serializers.py Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- rest_framework/serializers.py | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/rest_framework/serializers.py b/rest_framework/serializers.py index f23d663ccb..867c286d40 100644 --- a/rest_framework/serializers.py +++ b/rest_framework/serializers.py @@ -1497,9 +1497,10 @@ def get_uniqueness_extra_kwargs(self, field_names, declared_fields, extra_kwargs elif unique_constraint_field.null: default = None elif unique_constraint_field.blank: - default = '' - else: - default = empty + 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 From c6bea61e964802ea40dbcc6a82f31a094f8c9190 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Asif=20Saif=20Uddin=20=7B=22Auvi=22=3A=22=E0=A6=85?= =?UTF-8?q?=E0=A6=AD=E0=A6=BF=22=7D?= Date: Thu, 2 Apr 2026 14:07:57 +0600 Subject: [PATCH 4/5] Update tests/test_validators.py Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- tests/test_validators.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/test_validators.py b/tests/test_validators.py index 938fb044ee..59ad5e1036 100644 --- a/tests/test_validators.py +++ b/tests/test_validators.py @@ -774,7 +774,7 @@ def test_single_field_uniq_validators(self): 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): + def test_blank_unique_constraint_fields_are_not_required(self): serializer = UniqueConstraintBlankSerializer(data={'age': 25}) self.assertTrue(serializer.is_valid(), serializer.errors) result = serializer.save() From ca7209447dba09943862e4c45e80cf2566211324 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Asif=20Saif=20Uddin=20=7B=22Auvi=22=3A=22=E0=A6=85?= =?UTF-8?q?=E0=A6=AD=E0=A6=BF=22=7D?= Date: Thu, 2 Apr 2026 14:35:29 +0600 Subject: [PATCH 5/5] Apply suggestions from code review Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- rest_framework/serializers.py | 2 ++ tests/test_validators.py | 9 +++++---- 2 files changed, 7 insertions(+), 4 deletions(-) diff --git a/rest_framework/serializers.py b/rest_framework/serializers.py index 32ed8a15b4..9f57858534 100644 --- a/rest_framework/serializers.py +++ b/rest_framework/serializers.py @@ -1528,6 +1528,8 @@ def get_uniqueness_extra_kwargs(self, field_names, declared_fields, extra_kwargs default = '' else: default = empty + else: + default = empty if unique_constraint_name in model_fields: # The corresponding field is present in the serializer diff --git a/tests/test_validators.py b/tests/test_validators.py index 0480cf3c11..5ab0f054c9 100644 --- a/tests/test_validators.py +++ b/tests/test_validators.py @@ -640,10 +640,11 @@ 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'))) - ] + name='unique_constraint', + fields=('age', 'tag'), + condition=~models.Q(tag=''), + ) + ] class UniqueConstraintReadOnlyFieldModel(models.Model):