Skip to content

Fix blank=True fields in unique constraints being treated as required - #10039

Open
MehrazRumman wants to merge 2 commits into
encode:mainfrom
MehrazRumman:fix/blank-unique-constraint-fields-not-required
Open

Fix blank=True fields in unique constraints being treated as required#10039
MehrazRumman wants to merge 2 commits into
encode:mainfrom
MehrazRumman:fix/blank-unique-constraint-fields-not-required

Conversation

@MehrazRumman

Copy link
Copy Markdown
Contributor

Fixes #9750
Supersedes #9751

Description

Since 3.15, ModelSerializer marks every field that takes part in a unique_together or UniqueConstraint as required unless it has a default or is nullable. A CharField/TextField with blank=True therefore became required, even though Django would happily save it as an empty string. This broke a common pattern where a conditional constraint such as condition=~Q(char_field='') deliberately excludes blank values from uniqueness.

This PR adds a blank=True branch to get_uniqueness_extra_kwargs, alongside the existing null=True branch. Text fields that allow blank now get default='', so they stay optional while uniqueness is still validated against the empty string, exactly as the database would.

Only CharField and TextField subclasses are affected. Other field types with blank=True but no default or null still fall through to required=True, because the database would reject a missing value for those.

Changes

  • rest_framework/serializers.py: set default='' for blank text fields in uniqueness constraints instead of required=True.
  • tests/test_validators.py: new BlankUniquenessTogetherModel and UniqueConstraintBlankModel (the latter mirrors the model from the issue) with tests covering:
    • blank constraint fields are not required and save as ''
    • omitted or empty values still trigger unique_together validation when a duplicate exists
    • a conditional constraint excluding '' allows repeated blank values and still rejects non-blank duplicates
  • docs/api-guide/validators.md: document that null=True and blank text fields are not treated as required by ModelSerializer.

@auvipy
auvipy self-requested a review September 10, 2026 13:48

@auvipy auvipy left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

did you check this comment of the previous pr #9751 (review) ?

@MehrazRumman

Copy link
Copy Markdown
Contributor Author

I am checking the comments !

@MehrazRumman

Copy link
Copy Markdown
Contributor Author

Yes, thanks for pointing to it. That review asked for a test on UniqueConstraintBlankSerializer showing that a conditional constraint ~Q(tag='') accepts multiple rows with tag=''.

This PR already included that scenario, and I have now renamed the test to test_multiple_blank_values_are_allowed to match the suggestion and made it a bit stronger: it creates a row with age=1, tag='', then validates and saves two more rows with the same age (one sending tag='', one omitting tag), and asserts all three exist. It also keeps the companion test that a non-blank duplicate tag for the same age is still rejected.

The BlankUniquenessTogetherModel tests remain to cover plain unique_together, where '' is a real value and duplicates must still be rejected.

@MehrazRumman
MehrazRumman requested a review from auvipy September 10, 2026 14:00
@auvipy auvipy added this to the 3.18 milestone Sep 10, 2026
@auvipy auvipy added the Bug label Sep 10, 2026
@auvipy

auvipy commented Sep 10, 2026

Copy link
Copy Markdown
Collaborator

can you please cross check if this issues is related to this #7489 ? or should be addressed in a separate pr

@MehrazRumman

MehrazRumman commented Sep 10, 2026

Copy link
Copy Markdown
Contributor Author

@auvipy I checked #7489 and reproduced it on the current branch. It is a separate, pre-existing bug in UniqueTogetherValidator: on a full (non-partial) update that omits a constraint field which has a model default, Field.get_default() puts the default into attrs, so filter_queryset validates against the default instead of the instance's current value. Partial updates are unaffected because get_default() raises SkipField there. The failing test from #7490 still fails today, and it fails independently of this PR.

This PR only changes get_uniqueness_extra_kwargs so that blank=True text fields get default='' instead of required=True on create, the same path null=True and has_default() fields already take. It does not touch the validator's update logic, and #9750 is a create-time regression (before 3.15 UniqueConstraint was not processed at all, so the field was never required).

Because the two fields with defaults now share the same path, fixing #7489 in the validator would automatically cover blank fields too. I think #7489 should be handled in its own PR since it needs a decision on whether an omitted constraint field on PUT should mean "keep the instance value" or "reset to the default", which affects what gets saved as well as what gets validated. I'm happy to open that follow-up PR if you'd like.

Comment thread tests/test_validators.py
Comment thread rest_framework/serializers.py
@MehrazRumman
MehrazRumman requested a review from n1rjal September 10, 2026 15:57
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Development

Successfully merging this pull request may close these issues.

3.15 is raising required error on model blank fields

3 participants