Skip to content

Support multi content type in request body and responses #214

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

Open
wants to merge 4 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
84 changes: 83 additions & 1 deletion docs/Usage/Request.md
Original file line number Diff line number Diff line change
Expand Up @@ -103,6 +103,88 @@ def get_book(raw: BookRaw):
return "ok"
```

## Multiple content types in the request body

```python
from typing import Union

from flask import Request
from pydantic import BaseModel

from flask_openapi3 import OpenAPI

app = OpenAPI(__name__)


class DogBody(BaseModel):
a: int = None
b: str = None

model_config = {
"openapi_extra": {
"content_type": "application/vnd.dog+json"
}
}


class CatBody(BaseModel):
c: int = None
d: str = None

model_config = {
"openapi_extra": {
"content_type": "application/vnd.cat+json"
}
}


class BsonModel(BaseModel):
e: int = None
f: str = None

model_config = {
"openapi_extra": {
"content_type": "application/bson"
}
}


class ContentTypeModel(BaseModel):
model_config = {
"openapi_extra": {
"content_type": "text/csv"
}
}


@app.post("/a", responses={200: DogBody | CatBody | ContentTypeModel | BsonModel})
def index_a(body: DogBody | CatBody | ContentTypeModel | BsonModel):
"""
multiple content types examples.

This may be confusing, if the content-type is application/json, the type of body will be auto parsed to
DogBody or CatBody, otherwise it cannot be parsed to ContentTypeModel or BsonModel.
The body is equivalent to the request variable in Flask, and you can use body.data, body.text, etc ...
"""
print(body)
if isinstance(body, Request):
if body.mimetype == "text/csv":
# processing csv data
...
elif body.mimetype == "application/bson":
# processing bson data
...
else:
# DogBody or CatBody
...
return {"hello": "world"}
```

The effect in swagger:

![](../assets/Snipaste_2025-01-14_10-44-00.png)


## Request model

First, you need to define a [pydantic](https://github.com/pydantic/pydantic) model:
Expand All @@ -125,7 +207,7 @@ class BookQuery(BaseModel):
author: str = Field(None, description='Author', json_schema_extra={"deprecated": True})
```

Magic:
The effect in swagger:

![](../assets/Snipaste_2022-09-04_10-10-03.png)

Expand Down
116 changes: 116 additions & 0 deletions docs/Usage/Response.md
Original file line number Diff line number Diff line change
Expand Up @@ -56,6 +56,122 @@ def hello(path: HelloPath):

![image-20210526104627124](../assets/image-20210526104627124.png)

*Sometimes you may need more description fields about the response, such as description, headers and links.

You can use the following form:

```python
@app.get(
"/test",
responses={
"201": {
"model": BaseResponse,
"description": "Custom description",
"headers": {
"location": {
"description": "URL of the new resource",
"schema": {"type": "string"}
}
},
"links": {
"dummy": {
"description": "dummy link"
}
}
}
}
)
def endpoint_test():
...
```

The effect in swagger:

![](../assets/Snipaste_2025-01-14_11-08-40.png)


## Multiple content types in the responses

```python
from typing import Union

from flask import Request
from pydantic import BaseModel

from flask_openapi3 import OpenAPI

app = OpenAPI(__name__)


class DogBody(BaseModel):
a: int = None
b: str = None

model_config = {
"openapi_extra": {
"content_type": "application/vnd.dog+json"
}
}


class CatBody(BaseModel):
c: int = None
d: str = None

model_config = {
"openapi_extra": {
"content_type": "application/vnd.cat+json"
}
}


class BsonModel(BaseModel):
e: int = None
f: str = None

model_config = {
"openapi_extra": {
"content_type": "application/bson"
}
}


class ContentTypeModel(BaseModel):
model_config = {
"openapi_extra": {
"content_type": "text/csv"
}
}


@app.post("/a", responses={200: DogBody | CatBody | ContentTypeModel | BsonModel})
def index_a(body: DogBody | CatBody | ContentTypeModel | BsonModel):
"""
multiple content types examples.

This may be confusing, if the content-type is application/json, the type of body will be auto parsed to
DogBody or CatBody, otherwise it cannot be parsed to ContentTypeModel or BsonModel.
The body is equivalent to the request variable in Flask, and you can use body.data, body.text, etc ...
"""
print(body)
if isinstance(body, Request):
if body.mimetype == "text/csv":
# processing csv data
...
elif body.mimetype == "application/bson":
# processing bson data
...
else:
# DogBody or CatBody
...
return {"hello": "world"}
```

The effect in swagger:

![](../assets/Snipaste_2025-01-14_10-49-19.png)


## More information about OpenAPI responses

- [OpenAPI Responses Object](https://spec.openapis.org/oas/v3.1.0#responses-object), it includes the Response Object.
Expand Down
23 changes: 23 additions & 0 deletions docs/Usage/Route_Operation.md
Original file line number Diff line number Diff line change
Expand Up @@ -287,6 +287,29 @@ class BookListAPIView:
app.register_api_view(api_view)
```

## request_body_description

A brief description of the request body.

```python
from flask_openapi3 import OpenAPI

app = OpenAPI(__name__)

@app.post(
"/",
request_body_description="A brief description of the request body."
)
def create_book(body: Bookbody):
...
```

![](../assets/Snipaste_2025-01-14_10-56-40.png)

## request_body_required

Determines if the request body is required in the request.

## doc_ui

You can pass `doc_ui=False` to disable the `OpenAPI spec` when init `OpenAPI `.
Expand Down
Binary file added docs/assets/Snipaste_2025-01-14_10-44-00.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added docs/assets/Snipaste_2025-01-14_10-49-19.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added docs/assets/Snipaste_2025-01-14_10-56-40.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added docs/assets/Snipaste_2025-01-14_11-08-40.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
81 changes: 81 additions & 0 deletions examples/multi_content_type.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,81 @@
# -*- coding: utf-8 -*-
# @Author : llc
# @Time : 2024/12/27 15:30
from flask import Request
from pydantic import BaseModel

from flask_openapi3 import OpenAPI

app = OpenAPI(__name__)


class DogBody(BaseModel):
a: int = None
b: str = None

model_config = {
"openapi_extra": {
"content_type": "application/vnd.dog+json"
}
}


class CatBody(BaseModel):
c: int = None
d: str = None

model_config = {
"openapi_extra": {
"content_type": "application/vnd.cat+json"
}
}


class BsonModel(BaseModel):
e: int = None
f: str = None

model_config = {
"openapi_extra": {
"content_type": "application/bson"
}
}


class ContentTypeModel(BaseModel):
model_config = {
"openapi_extra": {
"content_type": "text/csv"
}
}


@app.post("/a", responses={200: DogBody | CatBody | ContentTypeModel | BsonModel})
def index_a(body: DogBody | CatBody | ContentTypeModel | BsonModel):
"""
multiple content types examples.

This may be confusing, if the content-type is application/json, the type of body will be auto parsed to
DogBody or CatBody, otherwise it cannot be parsed to ContentTypeModel or BsonModel.
The body is equivalent to the request variable in Flask, and you can use body.data, body.text, etc ...
"""
print(body)
if isinstance(body, Request):
if body.mimetype == "text/csv":
# processing csv data
...
elif body.mimetype == "application/bson":
# processing bson data
from bson import BSON

obj = BSON(body.data).decode()
new_body = body.model_validate(obj=obj)
print(new_body)
else:
# DogBody or CatBody
...
return {"hello": "world"}


if __name__ == '__main__':
app.run(debug=True)
11 changes: 10 additions & 1 deletion flask_openapi3/blueprint.py
Original file line number Diff line number Diff line change
Expand Up @@ -121,6 +121,8 @@ def _collect_openapi_info(
security: Optional[list[dict[str, list[Any]]]] = None,
servers: Optional[list[Server]] = None,
openapi_extensions: Optional[dict[str, Any]] = None,
request_body_description: Optional[str] = None,
request_body_required: Optional[bool] = True,
doc_ui: bool = True,
method: str = HTTPMethod.GET
) -> ParametersTuple:
Expand All @@ -140,6 +142,8 @@ def _collect_openapi_info(
security: A declaration of which security mechanisms can be used for this operation.
servers: An alternative server array to service this operation.
openapi_extensions: Allows extensions to the OpenAPI Schema.
request_body_description: A brief description of the request body.
request_body_required: Determines if the request body is required in the request.
doc_ui: Declares this operation to be shown. Default to True.
"""
if self.doc_ui is True and doc_ui is True:
Expand Down Expand Up @@ -193,6 +197,11 @@ def _collect_openapi_info(
parse_method(uri, method, self.paths, operation)

# Parse parameters
return parse_parameters(func, components_schemas=self.components_schemas, operation=operation)
return parse_parameters(
func, components_schemas=self.components_schemas,
operation=operation,
request_body_description=request_body_description,
request_body_required=request_body_required
)
else:
return parse_parameters(func, doc_ui=False)
11 changes: 10 additions & 1 deletion flask_openapi3/openapi.py
Original file line number Diff line number Diff line change
Expand Up @@ -380,6 +380,8 @@ def _collect_openapi_info(
security: Optional[list[dict[str, list[Any]]]] = None,
servers: Optional[list[Server]] = None,
openapi_extensions: Optional[dict[str, Any]] = None,
request_body_description: Optional[str] = None,
request_body_required: Optional[bool] = True,
doc_ui: bool = True,
method: str = HTTPMethod.GET
) -> ParametersTuple:
Expand All @@ -399,6 +401,8 @@ def _collect_openapi_info(
security: A declaration of which security mechanisms can be used for this operation.
servers: An alternative server array to service this operation.
openapi_extensions: Allows extensions to the OpenAPI Schema.
request_body_description: A brief description of the request body.
request_body_required: Determines if the request body is required in the request.
doc_ui: Declares this operation to be shown. Default to True.
method: HTTP method for the operation. Defaults to GET.
"""
Expand Down Expand Up @@ -450,6 +454,11 @@ def _collect_openapi_info(
parse_method(uri, method, self.paths, operation)

# Parse parameters
return parse_parameters(func, components_schemas=self.components_schemas, operation=operation)
return parse_parameters(
func, components_schemas=self.components_schemas,
operation=operation,
request_body_description=request_body_description,
request_body_required=request_body_required
)
else:
return parse_parameters(func, doc_ui=False)
Loading