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

get_model_definitions fails to retrieve already-registered dataclasses after converting them to pydantic dataclasses #5138

Closed
9 tasks done
ghost opened this issue Jul 13, 2022 · 4 comments
Labels
question Question or problem question-migrate

Comments

@ghost
Copy link

ghost commented Jul 13, 2022

First Check

  • I added a very descriptive title to this issue.
  • I used the GitHub search to find a similar issue and didn't find it.
  • I searched the FastAPI documentation, with the integrated search.
  • I already searched in Google "How to X in FastAPI" and didn't find any information.
  • I already read and followed all the tutorial in the docs and didn't find an answer.
  • I already checked if it is not related to FastAPI but to Pydantic.
  • I already checked if it is not related to FastAPI but to Swagger UI.
  • I already checked if it is not related to FastAPI but to ReDoc.

Commit to Help

  • I commit to help with one of those options 👆

Example Code

from dataclasses import dataclass
from typing import Dict

import uvicorn
from fastapi import FastAPI, status


@dataclass
class A:
    a: int


@dataclass
class B:
    a: A
    b: Dict[str, A]


app = FastAPI()


@app.get("/", responses={
    status.HTTP_200_OK: {
        "model": B,
        "description": "Some B"
    }
})
def foo() -> B:
    return B(
        a=A(1),
        b={"foo": A(2)}
    )


if __name__ == "__main__":
    uvicorn.run(f"{__loader__.name}:app")

# This code is complete, run as-is

Description

  • Paste the code above into a file and save it
  • Run the code above, which will start uvicorn listening on port 8000
  • Access http://localhost:8000/docs
  • See FastAPI is unable to load generate the Open API schema

image

  • See the Traceback in the terminal:
INFO:     127.0.0.1:33668 - "GET /openapi.json HTTP/1.1" 500 Internal Server Error
ERROR:    Exception in ASGI application
Traceback (most recent call last):
  File "/home/renatoc/.conda/envs/vibe-env/lib/python3.8/site-packages/uvicorn/protocols/http/h11_impl.py", line 396, in run_asgi
    result = await app(self.scope, self.receive, self.send)
  File "/home/renatoc/.conda/envs/vibe-env/lib/python3.8/site-packages/uvicorn/middleware/proxy_headers.py", line 45, in __call__
    return await self.app(scope, receive, send)
  File "/home/renatoc/.conda/envs/vibe-env/lib/python3.8/site-packages/fastapi/applications.py", line 269, in __call__
    await super().__call__(scope, receive, send)
  File "/home/renatoc/.conda/envs/vibe-env/lib/python3.8/site-packages/starlette/applications.py", line 124, in __call__
    await self.middleware_stack(scope, receive, send)
  File "/home/renatoc/.conda/envs/vibe-env/lib/python3.8/site-packages/starlette/middleware/errors.py", line 184, in __call__
    raise exc
  File "/home/renatoc/.conda/envs/vibe-env/lib/python3.8/site-packages/starlette/middleware/errors.py", line 162, in __call__
    await self.app(scope, receive, _send)
  File "/home/renatoc/.conda/envs/vibe-env/lib/python3.8/site-packages/starlette/exceptions.py", line 93, in __call__
    raise exc
  File "/home/renatoc/.conda/envs/vibe-env/lib/python3.8/site-packages/starlette/exceptions.py", line 82, in __call__
    await self.app(scope, receive, sender)
  File "/home/renatoc/.conda/envs/vibe-env/lib/python3.8/site-packages/fastapi/middleware/asyncexitstack.py", line 21, in __call__
    raise e
  File "/home/renatoc/.conda/envs/vibe-env/lib/python3.8/site-packages/fastapi/middleware/asyncexitstack.py", line 18, in __call__
    await self.app(scope, receive, send)
  File "/home/renatoc/.conda/envs/vibe-env/lib/python3.8/site-packages/starlette/routing.py", line 670, in __call__
    await route.handle(scope, receive, send)
  File "/home/renatoc/.conda/envs/vibe-env/lib/python3.8/site-packages/starlette/routing.py", line 266, in handle
    await self.app(scope, receive, send)
  File "/home/renatoc/.conda/envs/vibe-env/lib/python3.8/site-packages/starlette/routing.py", line 65, in app
    response = await func(request)
  File "/home/renatoc/.conda/envs/vibe-env/lib/python3.8/site-packages/fastapi/applications.py", line 224, in openapi
    return JSONResponse(self.openapi())
  File "/home/renatoc/.conda/envs/vibe-env/lib/python3.8/site-packages/fastapi/applications.py", line 199, in openapi
    self.openapi_schema = get_openapi(
  File "/home/renatoc/.conda/envs/vibe-env/lib/python3.8/site-packages/fastapi/openapi/utils.py", line 418, in get_openapi
    definitions = get_model_definitions(
  File "/home/renatoc/.conda/envs/vibe-env/lib/python3.8/site-packages/fastapi/utils.py", line 32, in get_model_definitions
    model_name = model_name_map[model]
KeyError: <class 'pydantic.dataclasses.A'>

Operating System

Linux

Operating System Details

No response

FastAPI Version

0.78.0

Python Version

3.8.13

Additional Context

This bug happens because whatever is converting bare dataclasses to pydantic dataclasses isn't caching the output of the conversion, which means that, in dataclass B, the dataclass A referenced in field a is different to the target of the mapping in field b.

In a naïve converter, this is what happens:

Python 3.8.13 | packaged by conda-forge | (default, Mar 25 2022, 06:04:10) 
[GCC 10.3.0] on linux
Type "help", "copyright", "credits" or "license" for more information.
>>> from dataclasses import dataclass
>>> from pydantic.dataclasses import dataclass as pydataclass
>>> @dataclass
... class A:
...   a: int
... 
>>> first = pydataclass(A)
>>> second = pydataclass(A)
>>> first == second
False
>>> id(first), id(second)
(93867703664320, 93867703667648)
>>> hash(first), hash(second)
(5866731479020, 5866731479228)

I don't think this is a bug in pydantic, though, as FastAPI is the library that claims dataclasses are supported.

I could try contributing a PR, if someone could point me to where the pydantic dataclass is generated.

As an additional note, if I don't fill the responses dict, this bug doesn't happen.

@ghost ghost added the question Question or problem label Jul 13, 2022
@JarroVGIT
Copy link
Contributor

Kudo's on the comprehensive report and research! (Y)

I did some digging as well as to your last question (where is this is happening) and I am very confused. The response_fields are created upon app startup (when APIRouter is initiated). Here, a ModelField() object is made based on your responses parameter. This ModelField() still holds reference to <class '__main__.B'> in it's type_ param.

However, when the application is being called (for example, because you are going to /docs), it is running at some point (fastapi/utils.py).get_flat_models_from_routes(). Here, the route its responses are evaluated. Before it is added to a list called 'responses_from_routes' the evaluation of .type on the ModelField is still <class '__main__.B'>. But right there after, the evaluation changes to <class 'pydantic.dataclasses._Pydantic_B_4839801328'>. Is this some side effect of extending a list from dict_values()? (that is what is happening)

I am wrecking my brain on how that is possible...

@ghost
Copy link
Author

ghost commented Jul 13, 2022

Thanks for the explanation.

After filing this issue, I took a look at FastAPI's code once more, and even though I claimed FastAPI wasn't caching the converted dataclasses, it seems to be caching them.

So, yeah, I'm confused as well.

@JarroVGIT
Copy link
Contributor

JarroVGIT commented Jul 19, 2022

Another person on SO has a simpler form to test this issue:

import dataclasses
from fastapi import FastAPI

app = FastAPI()

@dataclasses.dataclass
class Response1:
    yo: str

@app.post('/one', response_model=Response1)
def get_responses():
    pass

@app.post('/two', response_model=Response1)  # When I remove this "Response", or I create a second class it works.
def send_responses():
    pass

if __name__ == "__main__":
    import uvicorn
    uvicorn.run(app, host="0.0.0.0", port=8000)

Stacktrace:

Traceback (most recent call last):
  File "/<FOLDER>.venv/lib/python3.10/site-packages/uvicorn/protocols/http/httptools_impl.py", line 401, in run_asgi
    result = await app(self.scope, self.receive, self.send)
  File "/<FOLDER>.venv/lib/python3.10/site-packages/uvicorn/middleware/proxy_headers.py", line 78, in __call__
    return await self.app(scope, receive, send)
  File "/<FOLDER>.venv/lib/python3.10/site-packages/fastapi/applications.py", line 269, in __call__
    await super().__call__(scope, receive, send)
  File "/<FOLDER>.venv/lib/python3.10/site-packages/starlette/applications.py", line 124, in __call__
    await self.middleware_stack(scope, receive, send)
  File "/<FOLDER>.venv/lib/python3.10/site-packages/starlette/middleware/errors.py", line 184, in __call__
    raise exc
  File "/<FOLDER>.venv/lib/python3.10/site-packages/starlette/middleware/errors.py", line 162, in __call__
    await self.app(scope, receive, _send)
  File "/<FOLDER>.venv/lib/python3.10/site-packages/starlette/exceptions.py", line 93, in __call__
    raise exc
  File "/<FOLDER>.venv/lib/python3.10/site-packages/starlette/exceptions.py", line 82, in __call__
    await self.app(scope, receive, sender)
  File "/<FOLDER>.venv/lib/python3.10/site-packages/fastapi/middleware/asyncexitstack.py", line 21, in __call__
    raise e
  File "/<FOLDER>.venv/lib/python3.10/site-packages/fastapi/middleware/asyncexitstack.py", line 18, in __call__
    await self.app(scope, receive, send)
  File "/<FOLDER>.venv/lib/python3.10/site-packages/starlette/routing.py", line 670, in __call__
    await route.handle(scope, receive, send)
  File "/<FOLDER>.venv/lib/python3.10/site-packages/starlette/routing.py", line 266, in handle
    await self.app(scope, receive, send)
  File "/<FOLDER>.venv/lib/python3.10/site-packages/starlette/routing.py", line 65, in app
    response = await func(request)
  File "/<FOLDER>.venv/lib/python3.10/site-packages/fastapi/applications.py", line 224, in openapi
    return JSONResponse(self.openapi())
  File "/<FOLDER>.venv/lib/python3.10/site-packages/fastapi/applications.py", line 199, in openapi
    self.openapi_schema = get_openapi(
  File "/<FOLDER>.venv/lib/python3.10/site-packages/fastapi/openapi/utils.py", line 414, in get_openapi
    definitions = get_model_definitions(
  File "/<FOLDER>.venv/lib/python3.10/site-packages/fastapi/utils.py", line 39, in get_model_definitions
    model_name = model_name_map[model]
KeyError: <class 'pydantic.dataclasses.Response1'>

@l-monninger
Copy link

Any progress on this issue? I've noticed the behavior with pydantic models as well. However, it seems very unusual. Even if two environments supposedly have the same dependencies installed one might always run without issue while the other might always fail.

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

This issue was moved to a discussion.

You can continue the conversation there. Go to discussion →

Labels
question Question or problem question-migrate
Projects
None yet
Development

No branches or pull requests

3 participants