django-ninja icon indicating copy to clipboard operation
django-ninja copied to clipboard

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

Open eldardamari opened this issue 3 years ago • 11 comments

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 avatar Jun 11 '22 10:06 eldardamari

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}

vitalik avatar Jun 12 '22 13:06 vitalik

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



eldardamari avatar Jun 12 '22 15:06 eldardamari

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()

vitalik avatar Jun 13 '22 06:06 vitalik

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?

migueltorrescosta avatar Jul 25 '23 10:07 migueltorrescosta

@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),  # <---------- !
]

vitalik avatar Jul 25 '23 11:07 vitalik

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

migueltorrescosta avatar Jul 25 '23 12:07 migueltorrescosta

@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

evanheckert avatar Feb 07 '24 05:02 evanheckert

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__"

vitalik avatar Feb 07 '24 10:02 vitalik

@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?

evanheckert avatar Feb 12 '24 06:02 evanheckert

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 avatar Mar 21 '24 17:03 pmdevita

Actually there's another issue here for this exact problem https://github.com/vitalik/django-ninja/issues/828

pmdevita avatar Mar 21 '24 17:03 pmdevita