-
-
Notifications
You must be signed in to change notification settings - Fork 6.4k
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
Comments
params: MyModel = Depends()
seems to force swagger ui docs to require a request body for GET methods.params: MyModel = Depends()
seems to force OpenAPI docs to require a request body for GET methods.
params: MyModel = Depends()
seems to force OpenAPI docs to require a request body for GET methods.params: MyModel = Depends()
forces OpenAPI docs GET methods to require a request body.
I faced a similar issue, the only way I found to make it work in the way you described was to explicitly pass a 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 |
params: MyModel = Depends()
forces OpenAPI docs GET methods to require a request body.params: MyModel = Depends()
forces OpenAPI docs GET methods to require a request body.
Update: As of https://github.com/fastapi/fastapi/releases/tag/0.115.0 what I wrote below is no longer the case. 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:
|
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 |
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? |
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(). |
Here is a way to think about what you are trying to do in your code and what the workaround might look like: It's important to keep the concepts of "query parameters" and "results of a dependency injection function" separate What's happening here is the default dependency injection function is not doing what you expected for deeply nested I would actually say that your "working" endpoint 1 is only working partially - If you look at the Swagger docs, You can correct this by writing your own dependency injection function, rather than using what you get with a bare Depends(), which is what I'm going to do below. Here's a handy glue function: This function takes info out of a particular field in a def query_kwargs(
class_: type[BaseModel], # The Pydantic class we're making in the dependency injection
field: str # The name of the field we're trying to pull out of the query string
):
field_info = class_.model_fields[field]
is_required = field_info.is_required()
description = field_info.description
default = field_info.get_default()
result = {
'default': default,
'required': is_required,
'description': description,
}
if alias := field_info.serialization_alias:
json_schema = class_.model_json_schema()['properties'][alias]
else:
json_schema = class_.model_json_schema()['properties'][field]
result['json_schema_extra'] = json_schema
return result (The above will at least do the trick for basic field types, not fields that are nested models. If you run Here is your simple class with a custom dependency function: class MyModelData1(BaseModel):
archive: Optional[str] = Field(None, description="Archive name")
archive_type: Optional[str] = Field(None, description="Archive type")
@classmethod
def from_query(cls):
"""Class method to build an instance from the HTTP query string"""
# Use my handy function above to generate the args to Query
q_archive = query_kwargs(cls, 'archive')
q_archive_type = query_kwargs(cls, 'archive_type')
# Create a function that parses info out of the query explicitly, rather
# that implicitly
def dependency_to_inject(
# Be sure these type hints match the type annotation in the class
# field definitions or FastAPI could become confused in some cases!
archive: Optional[str] = Query(**q_archive),
archive_type: Optional[str] = Query(**q_archive_type),
) -> Self:
return cls(archive=archive, archive_type=archive_type)
return Depends(dependency_to_inject) And then swap in this new function for your bare Depends() as below. (I also changed the response to echo back the params object that was built to make it easier to see what's happening. @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 = MyModelData1.from_query() # <---- Call the new function here!
):
# Change the response to return the params as decoded to see what happened!
return JSONResponse(content=params.model_dump()) Go run this new version and play with the MyModelData1 field definitions a little - update the description, default, Now, let's move on to your deeply nested example. The first thing I wanted to know when I tried to build a dependency function for this version was: That's a domain-specific answer, and I basically just punted on it for the code below. I said, OK, the "id" in the MyModelData2 object and the "id" in the MetaDataGet were just going to be the same value pulled from the query, since they have the same name, alias, and type. I also wasn't quite sure what do to with the array of DeepNestedModelGet objects -- I just assumed the user is required to send the same number of "name" and "version" files to make each element of the array have enough data and didn't even error check it. Even with those simplifications, the dependency function is quite a bit more complicated: I need to pull fields out of the query and build the model tree from the bottom up: class MyModelData2(BaseModel):
id: Optional[str] = Field(None, alias="_id")
details: Optional[DetailsModelGet]
meta: Optional[MetadataGet]
@classmethod
def from_query(cls):
"""Class method to build an instance from a query string"""
q_id = query_kwargs(cls, "id")
# Plain fields of member 'details'
q_some_data2 = query_kwargs(DetailsModelGet, "some_data2")
# Plain fields of member 'details.some_data' -- except! We actually
# want a list of these objects in the next level up so patch the
# json schema up to make these arrays that can be in query string
# more than once
q_name = query_kwargs(DeepNestedModelGet, "name")
q_version = query_kwargs(DeepNestedModelGet, "version")
q_name['json_schema_extra'] = {
'type': 'array',
'items': q_name['json_schema_extra']
}
q_version['json_schema_extra'] = {
'type': 'array',
'items': q_version['json_schema_extra']
}
# Plain fields of member "meta"
# Note: id field name is already taken for this query so ???
q_foreign_key = query_kwargs(MetadataGet, "foreign_key")
def dependency_to_inject(
id: Optional[str] = Query(**q_id),
some_data2: Optional[list[Optional[str]]] = Query(**q_some_data2),
# These are now lists of the original field type
name: Optional[list[Optional[str]]] = Query(**q_name),
version: Optional[list[Optional[str]]] = Query(**q_version),
foreign_key: Optional[str] = Query(**q_foreign_key),
):
if name is not None and version is not None:
some_data = [
DeepNestedModelGet(
name=n,
version=v
)
for n,v in zip(name, version)
]
else:
some_data = None
result = cls(
_id=id,
details = DetailsModelGet(
some_data =some_data,
some_data2 = some_data2
),
meta = MetadataGet(
_id = id,
_foreign_key = foreign_key,
)
)
return result
return Depends(dependency_to_inject) Now, change the definition of get_data2 to mimic what we did in get_data1 -- use the new .from_query() method But, I hope you also see drawbacks of the above code: I made a bunch of assumptions about how you wanted the HTTP query string formatted to even begin to write this function (FastAPI usually hides that). I just used the leaf-level field names as the query field names. I don't have good separation of concerns between the various class definitions in part 2 - the Data2 decoder This code required full-stack understanding of OpenAPI, Pydantic, and FastAPI to get to this still half-baked result. |
First check
Example
Here's a self-contained minimal, reproducible, example with my use case:
Description
/data1/
./data2/
.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 identicalDepends()
structure sodump_model()
method can be used to pass very large models to the documentation as query parameters for the GET method which are optional.Environment
Additional context
I've gone so far as to take
List[str]
which becameOptional[List[str]
in the Get version of the model, to an extreme ofOptional[List[Optional[str]]]
andList[DetailsModelGet]
which was already all optional fields intoOptional[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
The text was updated successfully, but these errors were encountered: