diff --git a/docs/en/docs/img/tutorial/metadata/image02.png b/docs/en/docs/img/tutorial/metadata/image02.png
new file mode 100644
index 000000000..7f3ab0a10
Binary files /dev/null and b/docs/en/docs/img/tutorial/metadata/image02.png differ
diff --git a/docs/en/docs/tutorial/metadata.md b/docs/en/docs/tutorial/metadata.md
index 666fa7648..b9120c82e 100644
--- a/docs/en/docs/tutorial/metadata.md
+++ b/docs/en/docs/tutorial/metadata.md
@@ -21,6 +21,58 @@ With this configuration, the automatic API docs would look like:
+## 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:
+
+
+
+### 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
By default, the OpenAPI schema is served at `/openapi.json`.
diff --git a/docs_src/metadata/tutorial004.py b/docs_src/metadata/tutorial004.py
new file mode 100644
index 000000000..465bd659d
--- /dev/null
+++ b/docs_src/metadata/tutorial004.py
@@ -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"}]
diff --git a/fastapi/applications.py b/fastapi/applications.py
index 39e694fae..3306aab3d 100644
--- a/fastapi/applications.py
+++ b/fastapi/applications.py
@@ -37,6 +37,7 @@ class FastAPI(Starlette):
description: str = "",
version: str = "0.1.0",
openapi_url: Optional[str] = "/openapi.json",
+ openapi_tags: Optional[List[Dict[str, Any]]] = None,
default_response_class: Type[Response] = JSONResponse,
docs_url: Optional[str] = "/docs",
redoc_url: Optional[str] = "/redoc",
@@ -70,6 +71,7 @@ class FastAPI(Starlette):
self.description = description
self.version = version
self.openapi_url = openapi_url
+ self.openapi_tags = openapi_tags
# TODO: remove when discarding the openapi_prefix parameter
if openapi_prefix:
logger.warning(
@@ -103,6 +105,7 @@ class FastAPI(Starlette):
description=self.description,
routes=self.routes,
openapi_prefix=openapi_prefix,
+ tags=self.openapi_tags,
)
return self.openapi_schema
diff --git a/fastapi/openapi/utils.py b/fastapi/openapi/utils.py
index bb2e7dff7..b6221ca20 100644
--- a/fastapi/openapi/utils.py
+++ b/fastapi/openapi/utils.py
@@ -317,12 +317,13 @@ def get_openapi(
openapi_version: str = "3.0.2",
description: str = None,
routes: Sequence[BaseRoute],
- openapi_prefix: str = ""
+ openapi_prefix: str = "",
+ tags: Optional[List[Dict[str, Any]]] = None
) -> Dict:
info = {"title": title, "version": version}
if description:
info["description"] = description
- output = {"openapi": openapi_version, "info": info}
+ output: Dict[str, Any] = {"openapi": openapi_version, "info": info}
components: Dict[str, Dict] = {}
paths: Dict[str, Dict] = {}
flat_models = get_flat_models_from_routes(routes)
@@ -352,4 +353,6 @@ def get_openapi(
if components:
output["components"] = components
output["paths"] = paths
+ if tags:
+ output["tags"] = tags
return jsonable_encoder(OpenAPI(**output), by_alias=True, exclude_none=True)
diff --git a/fastapi/routing.py b/fastapi/routing.py
index 71a2b4d04..b4560a8a4 100644
--- a/fastapi/routing.py
+++ b/fastapi/routing.py
@@ -12,7 +12,6 @@ from fastapi.dependencies.utils import (
)
from fastapi.encoders import DictIntStrAny, SetIntStr, jsonable_encoder
from fastapi.exceptions import RequestValidationError, WebSocketRequestValidationError
-from fastapi.logger import logger
from fastapi.openapi.constants import STATUS_CODES_WITH_NO_BODY
from fastapi.utils import (
PYDANTIC_1,
diff --git a/tests/test_tutorial/test_metadata/test_tutorial004.py b/tests/test_tutorial/test_metadata/test_tutorial004.py
new file mode 100644
index 000000000..1ec59d3fe
--- /dev/null
+++ b/tests/test_tutorial/test_metadata/test_tutorial004.py
@@ -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