mirror of https://github.com/tiangolo/fastapi.git
✨ Add support for tag metadata in OpenAPI (#1348)
* Allow to add OpenAPI tag descriptions * fix type hint * fix type hint 2 * refactor test to assure 100% coverage * 📝 Update tags metadata example * 📝 Update docs for tags metadata * ✅ Move tags metadata test to tutorial subdir * 🎨 Update format in applications * 🍱 Update docs UI image based on new example * 🎨 Apply formatting after solving conflicts Co-authored-by: Sebastián Ramírez <tiangolo@gmail.com>
This commit is contained in:
parent
3651b8a30f
commit
a071ddf3cd
Binary file not shown.
|
After Width: | Height: | Size: 47 KiB |
|
|
@ -21,6 +21,58 @@ With this configuration, the automatic API docs would look like:
|
||||||
|
|
||||||
<img src="/img/tutorial/metadata/image01.png">
|
<img src="/img/tutorial/metadata/image01.png">
|
||||||
|
|
||||||
|
## Tag descriptions
|
||||||
|
|
||||||
|
You can also add additional metadata for the different tags used to group your path operations with the parameter `openapi_tags`.
|
||||||
|
|
||||||
|
It takes a list containing one dictionary for each tag.
|
||||||
|
|
||||||
|
Each dictionary can contain:
|
||||||
|
|
||||||
|
* `name` (**required**): a `str` with the same tag name you use in the `tags` parameter in your *path operations* and `APIRouter`s.
|
||||||
|
* `description`: a `str` with a short description for the tag. It can have Markdown and will be shown in the docs UI.
|
||||||
|
* `externalDocs`: a `dict` describing external documentation with:
|
||||||
|
* `description`: a `str` with a short description for the external docs.
|
||||||
|
* `url` (**required**): a `str` with the URL for the external documentation.
|
||||||
|
|
||||||
|
### Create metadata for tags
|
||||||
|
|
||||||
|
Let's try that in an example with tags for `users` and `items`.
|
||||||
|
|
||||||
|
Create metadata for your tags and pass it to the `openapi_tags` parameter:
|
||||||
|
|
||||||
|
```Python hl_lines="3 4 5 6 7 8 9 10 11 12 13 14 15 16 18"
|
||||||
|
{!../../../docs_src/metadata/tutorial004.py!}
|
||||||
|
```
|
||||||
|
|
||||||
|
Notice that you can use Markdown inside of the descriptions, for example "login" will be shown in bold (**login**) and "fancy" will be shown in italics (_fancy_).
|
||||||
|
|
||||||
|
!!! tip
|
||||||
|
You don't have to add metadata for all the tags that you use.
|
||||||
|
|
||||||
|
### Use your tags
|
||||||
|
|
||||||
|
Use the `tags` parameter with your *path operations* (and `APIRouter`s) to assign them to different tags:
|
||||||
|
|
||||||
|
```Python hl_lines="21 26"
|
||||||
|
{!../../../docs_src/metadata/tutorial004.py!}
|
||||||
|
```
|
||||||
|
|
||||||
|
!!! info
|
||||||
|
Read more about tags in [Path Operation Configuration](../path-operation-configuration/#tags){.internal-link target=_blank}.
|
||||||
|
|
||||||
|
### Check the docs
|
||||||
|
|
||||||
|
Now, if you check the docs, they will show all the additional metadata:
|
||||||
|
|
||||||
|
<img src="/img/tutorial/metadata/image02.png">
|
||||||
|
|
||||||
|
### Order of tags
|
||||||
|
|
||||||
|
The order of each tag metadata dictionary also defines the order shown in the docs UI.
|
||||||
|
|
||||||
|
For example, even though `users` would go after `items` in alphabetical order, it is shown before them, because we added their metadata as the first dictionary in the list.
|
||||||
|
|
||||||
## OpenAPI URL
|
## OpenAPI URL
|
||||||
|
|
||||||
By default, the OpenAPI schema is served at `/openapi.json`.
|
By default, the OpenAPI schema is served at `/openapi.json`.
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,28 @@
|
||||||
|
from fastapi import FastAPI
|
||||||
|
|
||||||
|
tags_metadata = [
|
||||||
|
{
|
||||||
|
"name": "users",
|
||||||
|
"description": "Operations with users. The **login** logic is also here.",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "items",
|
||||||
|
"description": "Manage items. So _fancy_ they have their own docs.",
|
||||||
|
"externalDocs": {
|
||||||
|
"description": "Items external docs",
|
||||||
|
"url": "https://fastapi.tiangolo.com/",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
]
|
||||||
|
|
||||||
|
app = FastAPI(openapi_tags=tags_metadata)
|
||||||
|
|
||||||
|
|
||||||
|
@app.get("/users/", tags=["users"])
|
||||||
|
async def get_users():
|
||||||
|
return [{"name": "Harry"}, {"name": "Ron"}]
|
||||||
|
|
||||||
|
|
||||||
|
@app.get("/items/", tags=["items"])
|
||||||
|
async def get_items():
|
||||||
|
return [{"name": "wand"}, {"name": "flying broom"}]
|
||||||
|
|
@ -37,6 +37,7 @@ class FastAPI(Starlette):
|
||||||
description: str = "",
|
description: str = "",
|
||||||
version: str = "0.1.0",
|
version: str = "0.1.0",
|
||||||
openapi_url: Optional[str] = "/openapi.json",
|
openapi_url: Optional[str] = "/openapi.json",
|
||||||
|
openapi_tags: Optional[List[Dict[str, Any]]] = None,
|
||||||
default_response_class: Type[Response] = JSONResponse,
|
default_response_class: Type[Response] = JSONResponse,
|
||||||
docs_url: Optional[str] = "/docs",
|
docs_url: Optional[str] = "/docs",
|
||||||
redoc_url: Optional[str] = "/redoc",
|
redoc_url: Optional[str] = "/redoc",
|
||||||
|
|
@ -70,6 +71,7 @@ class FastAPI(Starlette):
|
||||||
self.description = description
|
self.description = description
|
||||||
self.version = version
|
self.version = version
|
||||||
self.openapi_url = openapi_url
|
self.openapi_url = openapi_url
|
||||||
|
self.openapi_tags = openapi_tags
|
||||||
# TODO: remove when discarding the openapi_prefix parameter
|
# TODO: remove when discarding the openapi_prefix parameter
|
||||||
if openapi_prefix:
|
if openapi_prefix:
|
||||||
logger.warning(
|
logger.warning(
|
||||||
|
|
@ -103,6 +105,7 @@ class FastAPI(Starlette):
|
||||||
description=self.description,
|
description=self.description,
|
||||||
routes=self.routes,
|
routes=self.routes,
|
||||||
openapi_prefix=openapi_prefix,
|
openapi_prefix=openapi_prefix,
|
||||||
|
tags=self.openapi_tags,
|
||||||
)
|
)
|
||||||
return self.openapi_schema
|
return self.openapi_schema
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -317,12 +317,13 @@ def get_openapi(
|
||||||
openapi_version: str = "3.0.2",
|
openapi_version: str = "3.0.2",
|
||||||
description: str = None,
|
description: str = None,
|
||||||
routes: Sequence[BaseRoute],
|
routes: Sequence[BaseRoute],
|
||||||
openapi_prefix: str = ""
|
openapi_prefix: str = "",
|
||||||
|
tags: Optional[List[Dict[str, Any]]] = None
|
||||||
) -> Dict:
|
) -> Dict:
|
||||||
info = {"title": title, "version": version}
|
info = {"title": title, "version": version}
|
||||||
if description:
|
if description:
|
||||||
info["description"] = description
|
info["description"] = description
|
||||||
output = {"openapi": openapi_version, "info": info}
|
output: Dict[str, Any] = {"openapi": openapi_version, "info": info}
|
||||||
components: Dict[str, Dict] = {}
|
components: Dict[str, Dict] = {}
|
||||||
paths: Dict[str, Dict] = {}
|
paths: Dict[str, Dict] = {}
|
||||||
flat_models = get_flat_models_from_routes(routes)
|
flat_models = get_flat_models_from_routes(routes)
|
||||||
|
|
@ -352,4 +353,6 @@ def get_openapi(
|
||||||
if components:
|
if components:
|
||||||
output["components"] = components
|
output["components"] = components
|
||||||
output["paths"] = paths
|
output["paths"] = paths
|
||||||
|
if tags:
|
||||||
|
output["tags"] = tags
|
||||||
return jsonable_encoder(OpenAPI(**output), by_alias=True, exclude_none=True)
|
return jsonable_encoder(OpenAPI(**output), by_alias=True, exclude_none=True)
|
||||||
|
|
|
||||||
|
|
@ -12,7 +12,6 @@ from fastapi.dependencies.utils import (
|
||||||
)
|
)
|
||||||
from fastapi.encoders import DictIntStrAny, SetIntStr, jsonable_encoder
|
from fastapi.encoders import DictIntStrAny, SetIntStr, jsonable_encoder
|
||||||
from fastapi.exceptions import RequestValidationError, WebSocketRequestValidationError
|
from fastapi.exceptions import RequestValidationError, WebSocketRequestValidationError
|
||||||
from fastapi.logger import logger
|
|
||||||
from fastapi.openapi.constants import STATUS_CODES_WITH_NO_BODY
|
from fastapi.openapi.constants import STATUS_CODES_WITH_NO_BODY
|
||||||
from fastapi.utils import (
|
from fastapi.utils import (
|
||||||
PYDANTIC_1,
|
PYDANTIC_1,
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,65 @@
|
||||||
|
from fastapi.testclient import TestClient
|
||||||
|
|
||||||
|
from metadata.tutorial004 import app
|
||||||
|
|
||||||
|
client = TestClient(app)
|
||||||
|
|
||||||
|
openapi_schema = {
|
||||||
|
"openapi": "3.0.2",
|
||||||
|
"info": {"title": "FastAPI", "version": "0.1.0"},
|
||||||
|
"paths": {
|
||||||
|
"/users/": {
|
||||||
|
"get": {
|
||||||
|
"tags": ["users"],
|
||||||
|
"summary": "Get Users",
|
||||||
|
"operationId": "get_users_users__get",
|
||||||
|
"responses": {
|
||||||
|
"200": {
|
||||||
|
"description": "Successful Response",
|
||||||
|
"content": {"application/json": {"schema": {}}},
|
||||||
|
}
|
||||||
|
},
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"/items/": {
|
||||||
|
"get": {
|
||||||
|
"tags": ["items"],
|
||||||
|
"summary": "Get Items",
|
||||||
|
"operationId": "get_items_items__get",
|
||||||
|
"responses": {
|
||||||
|
"200": {
|
||||||
|
"description": "Successful Response",
|
||||||
|
"content": {"application/json": {"schema": {}}},
|
||||||
|
}
|
||||||
|
},
|
||||||
|
}
|
||||||
|
},
|
||||||
|
},
|
||||||
|
"tags": [
|
||||||
|
{
|
||||||
|
"name": "users",
|
||||||
|
"description": "Operations with users. The **login** logic is also here.",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "items",
|
||||||
|
"description": "Manage items. So _fancy_ they have their own docs.",
|
||||||
|
"externalDocs": {
|
||||||
|
"description": "Items external docs",
|
||||||
|
"url": "https://fastapi.tiangolo.com/",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
],
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
def test_openapi_schema():
|
||||||
|
response = client.get("/openapi.json")
|
||||||
|
assert response.status_code == 200, response.text
|
||||||
|
assert response.json() == openapi_schema
|
||||||
|
|
||||||
|
|
||||||
|
def test_path_operations():
|
||||||
|
response = client.get("/items/")
|
||||||
|
assert response.status_code == 200, response.text
|
||||||
|
response = client.get("/users/")
|
||||||
|
assert response.status_code == 200, response.text
|
||||||
Loading…
Reference in New Issue