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

[BUG] Failing to override Pydantic alias_gnerator config, to convert fields to camelCase in response body #469

Open
eldardamari opened this issue Jun 11, 2022 · 11 comments

Comments

@eldardamari
Copy link

Describe the bug
Failing to apply Pydantic alias_gnerator config, to convert fields to camelCase in response body.

api = NinjaAPI()


def to_camel(string: str) -> str:
    return ''.join(word.capitalize() for word in string.split('_'))


class AddResult(Schema):
    total_sum: int

    class Config(Schema.Config):
        alias_generator = to_camel
        allow_population_by_field_name = True


@api.get("/add", response=AddResult)
def add(request, a: int, b: int):
    return AddResult(total_sum=a + b)

Expected response:

{
  "totalSum": 3
}

Docs

image

Versions (please complete the following information):

  • Python version: [3.9]
  • Django version: [4.0.4]
  • Django-Ninja version: [0.18.0]
@eldardamari eldardamari changed the title [BUG] [BUG] Failing to override Pydantic alias_gnerator config, to convert fields to camelCase in response body Jun 11, 2022
@vitalik
Copy link
Owner

vitalik commented Jun 12, 2022

Hi @eldardamari

Pydantic's by_alias is not default, you need to enforce it:

class AddResult(Schema):
    total_sum: int

    class Config:
        alias_generator = to_camel
        allow_population_by_field_name = True


@api.get("/add", response=AddResult, by_alias=True)   # <--- !!! by_alias=True
def add(request, a: int, b: int):
    return {'total_sum': a + b}

@eldardamari
Copy link
Author

Thanks @vitalik!
How can I add by_alias to specific api Router?

app_router = Router()

@app_router.get('/data', by_alias=True) 
def get_data():
  // code

@app_router.get('/') 
def get_index():
  // code

@vitalik
Copy link
Owner

vitalik commented Jun 13, 2022

I guess you can extend Router class:

class MyRouter(Router):
    def api_operation(self, *a, **kw):
           kw['by_alias'] = True
           return super().api_operation(*a, **kw)

router = MyRouter()

@migueltorrescosta
Copy link

I guess you can extend Router class:

class MyRouter(Router):
    def api_operation(self, *a, **kw):
           kw['by_alias'] = True
           return super().api_operation(*a, **kw)

router = MyRouter()

Is there a similar solution that would change the default value of by_alias to all routers (i.e. to the NinjaAPI object` ) rather than applying it on a router by router basis?

@vitalik
Copy link
Owner

vitalik commented Jul 25, 2023

@migueltorrescosta

I guess you can try the following - in urls.py(!!!) before include api to urls

def set_all_by_alias(api):
    for _pth, router in api._routers:
          for view in router.path_operations.values():
               for op in view.operations:
                     op.by_alias = True

set_all_by_alias(api)

urlpatterns = [
    ...
    path("api/", api.urls),  # <---------- !
]

@migueltorrescosta
Copy link

@vitalik Thank you for the prompt response, it worked without any changes to your suggestion 🎉

@evanheckert
Copy link

@vitalik Thank you for this! Are we able to set a universal alias_generator function as well in the urls.py? Would be great to avoid adding these to Config entries on every Schema:

class ProjectSiteSchema(ModelSchema):
    class Meta:
        model = ProjectSite
        fields = "__all__"

    class Config:
        alias_generator = to_camel
        populate_by_name = True

@vitalik
Copy link
Owner

vitalik commented Feb 7, 2024

Hi @eldardamari

I assume you use ninja 1.x (pydantic 2.x)

The new pydantic changes the place to configure to model_config (docs):

class ProjectSiteSchema(ModelSchema):
    model_config = dict(alias_generator=to_camel, populate_by_name=True)
    class Meta:
        model = ProjectSite
        fields = "__all__"

@evanheckert
Copy link

@vitalik weirdly, this works for almost everything. Here's the example value from the auto-generated docs:

  "sites": [
    {
      "id": 0,
      "created_by_id": 0,
      "name": "string",
      "addressCity": "string",
      "addressState": "string",
      "addressZip": "string",
      "addressStreet1": "string",
      "addressStreet2": "string",
      "createdAt": "2024-02-12T06:19:10.600Z"
    }
  ],

How do I make sure it includes created_by_id?

@pmdevita
Copy link

pmdevita commented Mar 21, 2024

It looks like setting the model_config does correctly alias any fields declared on the ModelSchema subclass but it isn't correctly aliasing any fields autogenerated from the Django model.

image
image
page_id and selected_page_id were not aliased but another_example was.

This is because in get_schema_field in ninja/orm/fields.py, all Django model fields that are foreign keys are automatically given an alias matching their attribute name (selected_line -> selected_line_id) and this is overriding the alias_generator.

image

This actually presents a small problem, the serialization alias needs to match the attribute's name on the Django model, which is why this alias is generated in the first place. We need two different serialization aliases, which doesn't seem possible so I'm not sure what the alternative is here.

Any ideas?

EDIT: Idea here

@pmdevita
Copy link

Actually there's another issue here for this exact problem #828

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

No branches or pull requests

5 participants