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] Using Nested Pydantic models and params: MyModel = Depends() forces OpenAPI docs GET methods to require a request body. #11037

Open
8 of 9 tasks
TrevorBenson opened this issue Jan 27, 2024 · 6 comments

Comments

@TrevorBenson
Copy link
Sponsor

TrevorBenson commented Jan 27, 2024

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.
    • Not familiar 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.
  • After submitting this, I commit to:
    • Read open issues with questions until I find 2 issues where I can help someone and add a comment to help there.
    • Or, I already hit the "watch" button in this repository to receive notifications and I commit to help at least 2 people that ask questions in the future.
    • Implement a Pull Request for a confirmed bug.

Example

Here's a self-contained minimal, reproducible, example with my use case:

from fastapi import APIRouter, Depends, FastAPI, Query
from fastapi.responses import JSONResponse
from typing import List, Optional
from pydantic import BaseModel, Field
import uvicorn

class MyModelData1(BaseModel):
    archive: Optional[str] = Field(None, description="Archive name")
    archive_type: Optional[str] = Field(None, description="Archive type")

class MetadataGet(BaseModel):
    id: Optional[str] = Field(None, alias="_id")
    foreign_key: Optional[str] = Field(None, alias="_foreign_key")

class DeepNestedModelGet(BaseModel):
    name: Optional[str]
    version: Optional[str]

class DetailsModelGet(BaseModel):
    some_data: Optional[List[Optional[DeepNestedModelGet]]]
    some_data2: Optional[List[Optional[str]]]

class MyModelData2(BaseModel):
    id: Optional[str] = Field(None, alias="_id")
    details: Optional[DetailsModelGet]
    meta: Optional[MetadataGet]

def get_documents(collection, sort_by, sort_order, page, page_size, **additional_filters):
    return {
        "collection": collection,
    }

app = FastAPI()
router1 = APIRouter(prefix="/data1", tags=["Data1"])
router2 = APIRouter(prefix="/data2", tags=["Data2"])

@router1.get("/", description="Retrieve all documents.")
def get_data1(
    sort_by: str = Query(None, description="Sort by this field"),
    sort_order: str = Query(None, description="Sort order"),
    page: int = Query(1, description="Page number"),
    page_size: int = Query(100, description="Number of documents per page"),
    params: MyModelData1 = Depends(),
):
    document = get_documents(
        "data1",
        sort_by,
        sort_order,
        page,
        page_size,
        **params.model_dump()
    )
    return JSONResponse(content=document)

@router2.get("/")
def get_data2(
    sort_by: str = Query(None, description="Sort by this field"),
    sort_order: str = Query(None, description="Sort order"),
    page: int = Query(1, description="Page number"),
    page_size: int = Query(100, description="Number of documents per page"),
    params: MyModelData2 = Depends(),
):
    """Get all software."""
    document = get_documents(
        "data2",
        sort_by,
        sort_order,
        page,
        page_size,
        **params.model_dump(),
    )
    return JSONResponse(content=document)

app.include_router(router1)
app.include_router(router2)



if __name__ == "__main__":
    uvicorn.run(app, host="127.0.0.1", port=5000)

Description

  • Open the browser and call the GET endpoint /data1/.
  • No request body is required for the GET method
  • Open the browser and call the GET endpoint /data2/.
  • A request body is required for the GET method and provided
  • If the user does not delete the request body since it is invalid a TypeError: Failed to execute 'fetch' on 'Window': Request with GET/HEAD method cannot have body. is returned.

The solution you would like

The request body is not a required part of the GET /data2/ UI documentation when using an identical Depends() structure so dump_model() method can be used to pass very large models to the documentation as query parameters for the GET method which are optional.

Environment

  • OS: Linux
  • FastAPI Version: 0.109.0
  • Python version: 3.9.18

Additional context

I've gone so far as to take List[str] which became Optional[List[str] in the Get version of the model, to an extreme of Optional[List[Optional[str]]] and List[DetailsModelGet] which was already all optional fields into Optional[List[Optional[DetailsModelGet]]]. Pushing Optional down to the last type and making "deeply optional" fields in case somehow this would resolve the issue. I cannot seem to find any instance of how to get the nested models not to result in a required request body in the docs GET method except to remove the nested (optional) models entirely and use a single-layer Pydantic model.

Originally posted by @stevesuh in #7275

@TrevorBenson TrevorBenson changed the title ### Using Nested Pydantic models and params: MyModel = Depends() seems to force swagger ui docs to require a request body for GET methods. ### Using Nested Pydantic models and params: MyModel = Depends() seems to force OpenAPI docs to require a request body for GET methods. Jan 29, 2024
@TrevorBenson TrevorBenson changed the title ### Using Nested Pydantic models and params: MyModel = Depends() seems to force OpenAPI docs to require a request body for GET methods. ### Using Nested Pydantic models and params: MyModel = Depends() forces OpenAPI docs GET methods to require a request body. Jan 29, 2024
@juampivallejo
Copy link

I faced a similar issue, the only way I found to make it work in the way you described was to explicitly pass a None default for the Optional nested model.
Like this:

class MyModelData1(BaseModel):
    archive: Optional[str] = Field(None, description="Archive name")
    archive_type: Optional[str] = Field(None, description="Archive type")

class MetadataGet(BaseModel):
    id: Optional[str] = Field(None, alias="_id")
    foreign_key: Optional[str] = Field(None, alias="_foreign_key")

class DeepNestedModelGet(BaseModel):
    name: Optional[str] = None
    version: Optional[str] = None

class DetailsModelGet(BaseModel):
    some_data: Optional[List[DeepNestedModelGet]] = None
    some_data2: Optional[List[Optional[str]]] = None

class MyModelData2(BaseModel):
    id: Optional[str] = Field(None, alias="_id")
    details: DetailsModelGet = Depends()
    meta: MetadataGet = Depends()

In this case, the request body is not required for the GET call.

@TrevorBenson
Copy link
Sponsor Author

I stumbled onto this (or something very similar) the night I first posted this. However, I didn't update the issue because

  1. While the body was no longer required, the docs then contained 2 Required fields, args and kwargs.
    Screenshot from 2024-02-07 13-15-00
  2. The Try it out option seems to be broken, as clicking Execute does not return a TypeError, however no response is received and a LOADING is constantly spinning, even after clicking the cancel button to try and back out.
    Screenshot from 2024-02-07 13-16-25

The args and kwargs instead of actual params for the nested models (preferred) would be acceptable if not required or breaking the functionality for testing the API. I would prefer some form of the dump_model() defining the params as optional since that is how they are listed. I suppose not documenting nested models as queryable params may be an intentional choice. 🤷

@TrevorBenson TrevorBenson changed the title ### Using Nested Pydantic models and params: MyModel = Depends() forces OpenAPI docs GET methods to require a request body. [BUG] Using Nested Pydantic models and params: MyModel = Depends() forces OpenAPI docs GET methods to require a request body. Feb 7, 2024
@sheluchin
Copy link

I believe using Pydantic models for GET params is not officially supported, and whatever of it works is only the case by accident. Using nested Pydantic models to define GET params is particularly problematic; there are many mentions of this if you search the issues and discussions. See this comment by @tiangolo.

The roadmap has plans for official support:

Support for Pydantic models for Query(), Form(), etc.

#10556 (comment)
#318 (comment)

@TrevorBenson
Copy link
Sponsor Author

I believe using Pydantic models for GET params is not officially supported, and whatever of it works is only the case by accident. Using nested Pydantic models to define GET params is particularly problematic; there are many mentions of this if you search the issues and discussions. See this comment by @tiangolo.

The roadmap has plans for official support:

Support for Pydantic models for Query(), Form(), etc.

#10556 (comment) #318 (comment)

That does appear to be the case. Thank you for the links, I had overlooked the comment in the closed discussion when it was marked answered w/ Pydantic v2 support, as well as the Roadmap as it did not explicitly mention Depends() for get methods or the resulting request body.

@sheluchin
Copy link

Since this comes up so often (judging by the number of issues and discussions), maybe it would be good to explicitly mention in the Query Parameters docs that using Pydantic models is not yet supported?

@sonnix85
Copy link

sonnix85 commented Mar 3, 2024

Hi, if you want to store data in a class, consider using dataclasses with query parameters. For the body, if there are only optional fields and any data will be sent, you should use Depends().

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

4 participants