Skip to content

This issue was moved to a discussion.

You can continue the conversation there. Go to discussion →

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

Generic pydantic model causes inconsistent OpenAPI spec generation #653

Closed
chbndrhnns opened this issue Oct 25, 2019 · 9 comments
Closed

Comments

@chbndrhnns
Copy link

chbndrhnns commented Oct 25, 2019

Describe the bug
I am wrapping my responses in a generic model like below (full code in toggle section).

class Response(GenericModel, Generic[DataT]):
    """Wrapper for responses"""
    data: Optional[DataT]

I see two issues:

  • The /docs endpoint renders things differently for a contained list or a contained single element.
  • PyCharm warns on type mismatch according to the annotations

I am wondering if I am using the GenericModel in a wrong way or if there is a bug?

Click to toggle
from typing import TypeVar, Generic, Optional, List

import uvicorn
from fastapi import FastAPI
from pydantic import BaseModel
from pydantic.generics import GenericModel

DataT = TypeVar('DataT')


class Response(GenericModel, Generic[DataT]):
    """Wrapper for responses"""
    data: Optional[DataT]


class ProjectOut(BaseModel):
    """Project model (out)"""
    id: int


app = FastAPI()

out = ProjectOut(id=1)


@app.get(
    '/projects/{item_id}',
    response_model=Response[ProjectOut]
)
def get_single() -> Response[ProjectOut]:
    return Response[ProjectOut](data=out)


@app.get(
    '/projects',
    response_model=Response[List[ProjectOut]])
def get_projects() -> Response[List[ProjectOut]]:
    return Response[List[ProjectOut]](data=[out])


if __name__ == '__main__':
    uvicorn.run(
        f"{__name__}:app",
        host='0.0.0.0',
        port=8888,
        reload=True,
        debug=True,
        log_level='info')

To Reproduce

Expected behavior

  • The list response is shown as Response[List[ProjectOut]]
  • I see no warnings in PyCharm when I am using annotations correctly.

Screenshots

Screenshot 2019-10-25 08 34 07

Screenshot 2019-10-25 08 33 07

Environment:

  • OS: macOS
  • FastAPI Version: 0.42
  • Python version: 3.7.4

Additional context
Add any other context about the problem here.

@chbndrhnns chbndrhnns added the bug Something isn't working label Oct 25, 2019
@dmontagu
Copy link
Collaborator

dmontagu commented Oct 25, 2019

As of pydantic v1, there is support for overriding the naming behavior of a generic class; see bottom of the section on generic models in the docs https://pydantic-docs.helpmanual.io/usage/models/#generic-models.

In the meantime until we have pydantic v1 support, your best bet is probably:

ResponseListProjectOut = Response[List[ProjectOut]]
ResponseListProjectOut.__name__ = "Response[List[ProjectOut]]"  # or whatever

...

@app.get('/projects', response_model=ResponseListProjectOut)
def get_projects() -> ResponseListProjectOut:
    return ResponseListProjectOut(data=[out])

I'm working on a "fix" to the type thing now. PyCharm doesn't make it easy though since it is basically impossible to hide method definitions from it. But I think annotating __class_getitem__ to return Type[Any] fixes it.

@tiangolo
Copy link
Owner

Also have in mind that your path operation function might return something different, like a DB model, a list of dicts, etc. And it will still be validated/parsed/converted using the type declared in response_model.

In those cases, you would have to declare the return type of your function as such for mypy to like it, even though your API is actually returning something else.

Also, you're probably never calling your path operation function directly, FastAPI calls it for you, so you will probably never get a lot of benefit from annotating it's return type.

My suggestion is, annotate everything else for your own sanity, but the path operation function... meh, no benefit from that.

@chbndrhnns
Copy link
Author

I am quite new to the annotations thing and did think through the implications. Thanks for the hint to skip annotations for the path operations.

@tiangolo
Copy link
Owner

Sure! 😄

@github-actions
Copy link
Contributor

Assuming the original issue was solved, it will be automatically closed now. But feel free to add more comments or create new issues.

@KiraPC
Copy link

KiraPC commented Dec 1, 2021

Hello everyone.

I have a similar issue to the one presented here.

Basically I'm using a pydantic BaseModel to represent the Pagination response. This class extends generic with a TypeVar.

T = TypeVar("T")


class PaginationModel(BaseModel, Generic[T]):
    total: int = Field(description="Total number of elements on DB")
    index: int = Field(description="The current page index")
    size: int = Field(description="The size of the page")
    results: List[T]

Then I use as Generic input the object that should be in the results list

class MyMockModel(BaseModel):
    foo: int
    bar: int

Then use it as route response model

def get_foo_bar(response_model=PaginationModel[MyMockModel]):
     ...

When I print the openapi spec, I expect to have something like

{
    "total": 0,
    "index": 0,
    "size": 20,
    "result": [
        {
            "foo": 0,
            "bar": 0
        }
    ]
}

but what actually I get is

{
   "foo": 0,
   "bar": 0
}

I'm doing somethings wrong, or it is a bug?

@fhessel
Copy link

fhessel commented Dec 2, 2021

@KiraPC – I just stumbled upon the same issue as you and Google led me here, so I'll write down the solution to that:

Instead of deriving your PaginationModel from pydantic.BaseModel you have to use pydantic.generics.GenericModel, so the following gives you the expected result:

from pydantic import BaseModel
from pydantic.generics import GenericModel

Model = TypeVar("Model", bound=BaseModel)

class PaginationModel(GenericModel, Generic[Model]):
    index: int
    size: int
    total: int
    items: List[Model]

@KiraPC
Copy link

KiraPC commented Dec 2, 2021

@KiraPC – I just stumbled upon the same issue as you and Google led me here, so I'll write down the solution to that:

Instead of deriving your PaginationModel from pydantic.BaseModel you have to use pydantic.generics.GenericModel, so the following gives you the expected result:

from pydantic import BaseModel
from pydantic.generics import GenericModel

Model = TypeVar("Model", bound=BaseModel)

class PaginationModel(GenericModel, Generic[Model]):
    index: int
    size: int
    total: int
    items: List[Model]

Oh, thank you man.

This was very useful.

@tiangolo tiangolo added question Question or problem answered reviewed and removed bug Something isn't working labels Feb 22, 2023
@tiangolo tiangolo changed the title [BUG] Generic pydantic model causes inconsistent OpenAPI spec generation Generic pydantic model causes inconsistent OpenAPI spec generation Feb 24, 2023
@tiangolo tiangolo reopened this Feb 28, 2023
@github-actions
Copy link
Contributor

Assuming the original need was handled, this will be automatically closed now. But feel free to add more comments or create new issues or PRs.

@tiangolo tiangolo reopened this Feb 28, 2023
Repository owner locked and limited conversation to collaborators Feb 28, 2023
@tiangolo tiangolo converted this issue into discussion #7960 Feb 28, 2023

This issue was moved to a discussion.

You can continue the conversation there. Go to discussion →

Projects
None yet
Development

No branches or pull requests

5 participants