Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Set pk_field=UUIDField for UUID foreign keys #8791

Draft
wants to merge 3 commits into
base: master
Choose a base branch
from

Conversation

r-thomson
Copy link

@r-thomson r-thomson commented Dec 5, 2022

Updates ModelSerializer’s field generation to set pk_field=UUIDField() if the related field is PrimaryKeyRelatedField and the foreign key field on the model is a UUIDField.

See this discussion for an explanation of why I think this change should be made

Compatibility

This could potentially break existing code if that code was relying on getting a UUID instance in serializer.data. This doesn’t apply once the data has been passed to the renderer, since AFAIK all built-in renderers will stringify UUIDs.

Alternatives

A potential alternative to this implementation would be for the behavior to be moved into PrimaryKeyRelatedField itself. In that case, the pk_field setting would apply even when used outside of ModelSerializer (unless overridden).

@r-thomson r-thomson marked this pull request as ready for review December 12, 2022 01:03
@auvipy
Copy link
Member

auvipy commented Dec 12, 2022

have to think about any breaking changes btw.

@kevin-brown
Copy link
Member

have to think about any breaking changes btw.

I would expect the obvious breaking change to be that anyone who was expecting a UUID to be returned within the serializer data will now have to handle a str and convert back/forth as needed. Given that most JSON renderers handle UUID as str already, I wouldn't expect this to be a widespread issue.

If this was recently introduced, my gut would call this is a bugfix that isn't subject to any deprecations. Given that (I assume) it has been in the wild for years, that makes it hard to claim as a bugfix instead of expected behaviour.

@r-thomson
Copy link
Author

I would expect the obvious breaking change to be that anyone who was expecting a UUID to be returned within the serializer data will now have to handle a str and convert back/forth as needed.

This is definitely a possibility; I discovered the behavior myself when I was writing a test case like serializer.data['foreign_key'] == uuid_str (which would be False on master and True here).

Copy link
Member

@tfranzel tfranzel left a comment

Choose a reason for hiding this comment

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

On a larger scale, one could pose the question whether this is worth the trouble. I'm pretty sure people will come out of the woodwork, saying that the change broke them because they relied on this seasoned implementation detail. Given that UUID("str") == UUID(UUID("str")), I guess it will likely go by unnoticed for most people, but you never know.

Clarification: My statement relates to modified serializer methods. As previously stated, the change is transparent to the actual API due to the renderer doing the string conversion anyway.

@@ -1296,6 +1296,10 @@ def build_relational_field(self, field_name, relation_info):
field_kwargs['slug_field'] = to_field
field_class = self.serializer_related_to_field

# `pk_field` is only valid for PrimaryKeyRelatedField
if not issubclass(field_class, PrimaryKeyRelatedField):
field_kwargs.pop('pk_field', None)
Copy link
Member

Choose a reason for hiding this comment

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

The test suite lands here only for field_class==HyperlinkedRelatedField

It seems this change is neither covered by the new tests nor is it required as the new tests do pass without this addition.

@@ -265,6 +265,8 @@ def get_relation_kwargs(field_name, relation_info):
kwargs.pop('queryset', None)
if model_field.null:
kwargs['allow_null'] = True
if isinstance(model_field.target_field, models.UUIDField):
Copy link
Member

Choose a reason for hiding this comment

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

I find it a bit awkward to introduce special handling for one field type while everything else is basically ignored. Sure, PKs are for the most part either AutoField or UUIDField, but that that is not a sure thing. For arguments sake this might very well be a DateTimeField.

Having more specific types seems reasonable, but then it should be done consistently and across the board imho.

Copy link
Author

Choose a reason for hiding this comment

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

Agreed, I was only thinking of integer, string, and UUID keys at the time but I can definitely see the value in supporting more fields.

Is there any reason we wouldn’t want to use ModelSerializer.serializer_field_mapping here?

Copy link
Member

Choose a reason for hiding this comment

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

This is just something that I noticed and thought it would be worth mentioning. Someone more knowledgeable on these parts should probably chime in on that question. @auvipy?

Copy link
Member

Choose a reason for hiding this comment

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

my thought here is, we should initially consider prospective primary key types / fields mapping here initiallly

Copy link
Member

Choose a reason for hiding this comment

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

I meant most commonly used primary key types checking / special could be done initially

Copy link
Member

Choose a reason for hiding this comment

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

Is there any reason we wouldn’t want to use ModelSerializer.serializer_field_mapping here?

We can use / try it here as well

Copy link
Author

Choose a reason for hiding this comment

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

Looked into this a bit— since some of the field types also need kwargs, we'd probably want to use build_field() instead of looking at the field mapping directly.

We could just make build_field() and friends classmethods so you could call ModelSerializer.build_field(...), but it'd also be nice if we passed the instance of ModelSerializer into get_relation_kwargs() so we could respect those methods being overriden on subclasses. At that point, though, perhaps it would make more sense for get_relation_kwargs to just be a method of ModelSerializer.

@r-thomson
Copy link
Author

Given that UUID("str") == UUID(UUID("str")), I guess it will likely go by unnoticed for most people, but you never know.

uuid.UUID actually doesn’t compose like that (always seemed like an oversight to me).

>>> UUID(UUID('051c9030-0fb2-4f1d-a36d-5d69e3c9794d'))
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
  File ".../python3.10/uuid.py", line 174, in __init__
    hex = hex.replace('urn:', '').replace('uuid:', '')
AttributeError: 'UUID' object has no attribute 'replace'

@tfranzel
Copy link
Member

my bad @r-thomson, I could have sworn I used that before. Anyways, that would be one more con point then.

@stale
Copy link

stale bot commented Feb 18, 2023

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.

@stale stale bot added the stale label Feb 18, 2023
@auvipy auvipy removed the stale label Feb 19, 2023
@@ -265,6 +265,8 @@ def get_relation_kwargs(field_name, relation_info):
kwargs.pop('queryset', None)
if model_field.null:
kwargs['allow_null'] = True
if isinstance(model_field.target_field, models.UUIDField):
Copy link
Member

Choose a reason for hiding this comment

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

my thought here is, we should initially consider prospective primary key types / fields mapping here initiallly

@stale
Copy link

stale bot commented Jun 10, 2023

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.

@stale stale bot added the stale label Jun 10, 2023
@@ -265,6 +265,8 @@ def get_relation_kwargs(field_name, relation_info):
kwargs.pop('queryset', None)
if model_field.null:
kwargs['allow_null'] = True
if isinstance(model_field.target_field, models.UUIDField):
Copy link
Member

Choose a reason for hiding this comment

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

I meant most commonly used primary key types checking / special could be done initially

@@ -265,6 +265,8 @@ def get_relation_kwargs(field_name, relation_info):
kwargs.pop('queryset', None)
if model_field.null:
kwargs['allow_null'] = True
if isinstance(model_field.target_field, models.UUIDField):
Copy link
Member

Choose a reason for hiding this comment

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

Is there any reason we wouldn’t want to use ModelSerializer.serializer_field_mapping here?

We can use / try it here as well

@stale stale bot removed stale labels Jun 13, 2023
@auvipy
Copy link
Member

auvipy commented Jun 13, 2023

Alternatives

A potential alternative to this implementation would be for the behavior to be moved into PrimaryKeyRelatedField itself. In that case, the pk_field setting would apply even when used outside of ModelSerializer (unless overridden).

Can you please explain this with some example codes?

@auvipy auvipy added this to the 3.15 milestone Jun 13, 2023
@r-thomson
Copy link
Author

Alternatives

A potential alternative to this implementation would be for the behavior to be moved into PrimaryKeyRelatedField itself. In that case, the pk_field setting would apply even when used outside of ModelSerializer (unless overridden).

Can you please explain this with some example codes?

As this PR is right now, the pk_field kwarg is set by ModelSerializer when it generates serializer fields. If the field is explicitly declared on the serializer (field = PrimaryKeyRelatedField(...)), then the current implementation doesn’t do anything different than what’s in master.

class ExampleSerializer(serializers.ModelSerializer):
    class Meta:
        model = PrimaryModel
        fields = ('related',)

"""
ExampleSerializer():
    related = PrimaryKeyRelatedField(pk_field=<django.db.models.fields.UUIDField>, queryset=RelatedModel.objects.all())
"""

The alternative would be to have PrimaryKeyRelatedField itself figure out the value for pk_field. Omitting the kwarg would be interpreted as “select the appropriate field based on the model in the queryset kwarg”. If you made that change then you could get this behavior without using ModelSerializer.

class ExampleSerializer(Serializer):
    related = PrimaryKeyRelatedField(queryset=RelatedModel.objects.all())

@auvipy
Copy link
Member

auvipy commented Jun 15, 2023

have to think about any breaking changes btw.

I would expect the obvious breaking change to be that anyone who was expecting a UUID to be returned within the serializer data will now have to handle a str and convert back/forth as needed. Given that most JSON renderers handle UUID as str already, I wouldn't expect this to be a widespread issue.

If this was recently introduced, my gut would call this is a bugfix that isn't subject to any deprecations. Given that (I assume) it has been in the wild for years, that makes it hard to claim as a bugfix instead of expected behaviour.

should we accept this and explicitly document it?

@auvipy
Copy link
Member

auvipy commented Jun 15, 2023

i think we should experiment more with this for now to reach a concrete consensus

@auvipy auvipy removed this from the 3.15 milestone Jul 11, 2023
@stale
Copy link

stale bot commented Sep 17, 2023

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.

@stale stale bot added the stale label Sep 17, 2023
@r-thomson r-thomson marked this pull request as draft May 7, 2024 20:20
@stale stale bot removed the stale label May 7, 2024
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

Successfully merging this pull request may close these issues.

None yet

4 participants