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

feature: Add Serializer field name changing by Pydantic alias / validation_alias #24

Open
wants to merge 3 commits into
base: main
Choose a base branch
from

Conversation

g-ionov
Copy link

@g-ionov g-ionov commented May 30, 2024

Hello!

In my case I use .drf_serializer() to create auto generation of documentation for API query parameters by drf_spectacular.

There are scenarios where QuerySet filters are passed as a query parameter that can look bad, such as main_model_field__related_field__icontains. When further interacting with the object I want to use this field name, but pass a prettier version to the API, for example field.

In Pydantic I use validation_alias for this purpose, which allows me to pass field during BaseModel initialization, but instance will contain main_model_field__related_field__icontains.

Example:

from typing import Annotated

from pydantic import BaseModel, Field


class Test(BaseModel):
        main_model_field__related_field__icontains: Annotated[str, Field(validation_alias="field")]


Test(field="word")

# >>>  Test(main_model_field__related_field__icontains='word')

Unfortunately, this version of drf_pydantic does not provide tools for parsing alias / validation_alias / serialization_alias, so in this Pull Request I propose my solution, which creates Serializers for inputs that change field names depending on the presence of alias / validation_alias

Also, this solution can be useful when convert camelCase to snake_case.

Example:

from typing import Annotated

from pydantic import BaseModel, Field


class Test(BaseModel):
        my_value: Annotated[str, Field(validation_alias="myValue")]


Test(myValue="word")

# >>> Test(my_value='word')

(Serializer source doesn`t work at this situation)

Copy link

codecov bot commented May 30, 2024

Codecov Report

All modified and coverable lines are covered by tests ✅

Project coverage is 98.93%. Comparing base (03bd861) to head (a658528).

Additional details and impacted files
@@            Coverage Diff             @@
##             main      #24      +/-   ##
==========================================
+ Coverage   98.83%   98.93%   +0.10%     
==========================================
  Files           5        5              
  Lines         171      188      +17     
==========================================
+ Hits          169      186      +17     
  Misses          2        2              

☔ View full report in Codecov by Sentry.
📢 Have feedback on the report? Share it here.

@georgebv
Copy link
Owner

Thank you for the contribution. There are three alias features in pydantic: alias, validation_alias, and serialization_alias. We need priority for handling these because source in DRF serves different purpose from alias in pydantic. I suggest the following:

  1. alias if provided
  2. validation_alias or serialization_alias if only one is provided or if both provided and equal; otherwie ignore

@g-ionov
Copy link
Author

g-ionov commented May 31, 2024

Thanks for the quick reply!

I made some changes based on your comment, and added Serializer source creation (when needed). The new solution pretty much replicates the way aliases work in Pydantic. The alias processing is not explicitly specified, because alias contains validation_alias and serialization_alias.

The use cases are described in the new tests.

The only thing that might not look good is changing the arguments in _convert_field. This is due to the need for a possible field renaming that happens in create_serializer_from_model, after which you need to set the source based on the old field name inside _convert_field. Can we leave it like this or you can suggest another solution?

@georgebv
Copy link
Owner

georgebv commented Jun 4, 2024

I wanted to confirm my understanding of source in DRF. It's essentially the same thing as alias in pydantic - meaning, it's used for both validation and serialization. Is this right? If so, I'm not convinced we should be modifying field_name because in that scenario resulting DRF model would have different field names and, depending on use case, that could be a deal breaker (e.g., attribute access).

I think any *alias fields should only affect the source keyword. If you want to change attribute name (field_name) in the generated model I lean towards using an annotation since there is no direct parity between pydantic and DRF. I envision something like:

class Foo(BaseModel):
    bar: Annotated[
        str,
        Field(alias="spam"),  # sets 'source'
        DrfFieldNameOverride("baz"),  # sets attribute name
    ]

resulting in

class FooSerializer(serializers.Serializer):
    baz = serializers.CharField(source="spam")

With that in mind, I suggest moving source resolution to _convert_field where you could aggregate all non-null *alias values into a set and then:

  • if set size is 0 do nothing
  • if set size is 1 set it to source
  • if set size is >2 raise an exception

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.

2 participants