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