--- /dev/null
+# Generate Clients
+
+As **FastAPI** is based on the OpenAPI specification, you get automatic compatibility with many tools, including the automatic API docs (provided by Swagger UI).
+
+One particular advantage that is not necessarily obvious is that you can **generate clients** (sometimes called <abbr title="Software Development Kits">**SDKs**</abbr> ) for your API, for many different **programming languages**.
+
+## OpenAPI Client Generators
+
+There are many tools to generate clients from **OpenAPI**.
+
+A common tool is <a href="https://openapi-generator.tech/" class="external-link" target="_blank">OpenAPI Generator</a>.
+
+If you are building a **frontend**, a very interesting alternative is <a href="https://github.com/ferdikoomen/openapi-typescript-codegen" class="external-link" target="_blank">openapi-typescript-codegen</a>.
+
+## Generate a TypeScript Frontend Client
+
+Let's start with a simple FastAPI application:
+
+=== "Python 3.6 and above"
+
+ ```Python hl_lines="9-11 14-15 18 19 23"
+ {!> ../../../docs_src/generate_clients/tutorial001.py!}
+ ```
+
+=== "Python 3.9 and above"
+
+ ```Python hl_lines="7-9 12-13 16-17 21"
+ {!> ../../../docs_src/generate_clients/tutorial001_py39.py!}
+ ```
+
+Notice that the *path operations* define the models they use for request payload and response payload, using the models `Item` and `ResponseMessage`.
+
+### API Docs
+
+If you go to the API docs, you will see that it has the **schemas** for the data to be sent in requests and received in responses:
+
+<img src="/img/tutorial/generate-clients/image01.png">
+
+You can see those schemas because they were declared with the models in the app.
+
+That information is available in the app's **OpenAPI schema**, and then shown in the API docs (by Swagger UI).
+
+And that same information from the models that is included in OpenAPI is what can be used to **generate the client code**.
+
+### Generate a TypeScript Client
+
+Now that we have the app with the models, we can generate the client code for the frontend.
+
+#### Install `openapi-typescript-codegen`
+
+You can install `openapi-typescript-codegen` in your frontend code with:
+
+<div class="termy">
+
+```console
+$ npm install openapi-typescript-codegen --save-dev
+
+---> 100%
+```
+
+</div>
+
+#### Generate Client Code
+
+To generate the client code you can use the command line application `openapi` that would now be installed.
+
+Because it is installed in the local project, you probably wouldn't be able to call that command directly, but you would put it on your `package.json` file.
+
+It could look like this:
+
+```JSON hl_lines="7"
+{
+ "name": "frontend-app",
+ "version": "1.0.0",
+ "description": "",
+ "main": "index.js",
+ "scripts": {
+ "generate-client": "openapi --input http://localhost:8000/openapi.json --output ./src/client --client axios"
+ },
+ "author": "",
+ "license": "",
+ "devDependencies": {
+ "openapi-typescript-codegen": "^0.20.1",
+ "typescript": "^4.6.2"
+ }
+}
+```
+
+After having that NPM `generate-client` script there, you can run it with:
+
+<div class="termy">
+
+```console
+$ npm run generate-client
+
+frontend-app@1.0.0 generate-client /home/user/code/frontend-app
+> openapi --input http://localhost:8000/openapi.json --output ./src/client --client axios
+```
+
+</div>
+
+That command will generate code in `./src/client` and will use `axios` (the frontend HTTP library) internally.
+
+### Try Out the Client Code
+
+Now you can import and use the client code, it could look like this, notice that you get autocompletion for the methods:
+
+<img src="/img/tutorial/generate-clients/image02.png">
+
+You will also get autocompletion for the payload to send:
+
+<img src="/img/tutorial/generate-clients/image03.png">
+
+!!! tip
+ Notice the autocompletion for `name` and `price`, that was defined in the FastAPI application, in the `Item` model.
+
+You will have inline errors for the data that you send:
+
+<img src="/img/tutorial/generate-clients/image04.png">
+
+The response object will also have autocompletion:
+
+<img src="/img/tutorial/generate-clients/image05.png">
+
+## FastAPI App with Tags
+
+In many cases your FastAPI app will be bigger, and you will probably use tags to separate different groups of *path operations*.
+
+For example, you could have a section for **items** and another section for **users**, and they could be separated by tags:
+
+
+=== "Python 3.6 and above"
+
+ ```Python hl_lines="23 28 36"
+ {!> ../../../docs_src/generate_clients/tutorial002.py!}
+ ```
+
+=== "Python 3.9 and above"
+
+ ```Python hl_lines="21 26 34"
+ {!> ../../../docs_src/generate_clients/tutorial002_py39.py!}
+ ```
+
+### Generate a TypeScript Client with Tags
+
+If you generate a client for a FastAPI app using tags, it will normally also separate the client code based on the tags.
+
+This way you will be able to have things ordered and grouped correctly for the client code:
+
+<img src="/img/tutorial/generate-clients/image06.png">
+
+In this case you have:
+
+* `ItemsService`
+* `UsersService`
+
+### Client Method Names
+
+Right now the generated method names like `createItemItemsPost` don't look very clean:
+
+```TypeScript
+ItemsService.createItemItemsPost({name: "Plumbus", price: 5})
+```
+
+...that's because the client generator uses the OpenAPI internal **operation ID** for each *path operation*.
+
+OpenAPI requires that each operation ID is unique across all the *path operations*, so FastAPI uses the **function name**, the **path**, and the **HTTP method/operation** to generate that operation ID, because that way it can make sure that the operation IDs are unique.
+
+But I'll show you how to improve that next. 🤓
+
+## Custom Operation IDs and Better Method Names
+
+You can **modify** the way these operation IDs are **generated** to make them simpler and have **simpler method names** in the clients.
+
+In this case you will have to ensure that each operation ID is **unique** in some other way.
+
+For example, you could make sure that each *path operation* has a tag, and then generate the operation ID based on the **tag** and the *path operation* **name** (the function name).
+
+### Custom Generate Unique ID Function
+
+FastAPI uses a **unique ID** for each *path operation*, it is used for the **operation ID** and also for the names of any needed custom models, for requests or responses.
+
+You can customize that function. It takes an `APIRoute` and outputs a string.
+
+For example, here it is using the first tag (you will probably have only one tag) and the *path operation* name (the function name).
+
+You can then pass that custom function to **FastAPI** as the `generate_unique_id_function` parameter:
+
+=== "Python 3.6 and above"
+
+ ```Python hl_lines="8-9 12"
+ {!> ../../../docs_src/generate_clients/tutorial003.py!}
+ ```
+
+=== "Python 3.9 and above"
+
+ ```Python hl_lines="6-7 10"
+ {!> ../../../docs_src/generate_clients/tutorial003_py39.py!}
+ ```
+
+### Generate a TypeScript Client with Custom Operation IDs
+
+Now if you generate the client again, you will see that it has the improved method names:
+
+<img src="/img/tutorial/generate-clients/image07.png">
+
+As you see, the method names now have the tag and then the function name, now they don't include information from the URL path and the HTTP operation.
+
+### Preprocess the OpenAPI Specification for the Client Generator
+
+The generated code still has some **duplicated information**.
+
+We already know that this method is related to the **items** because that word is in the `ItemsService` (taken from the tag), but we still have the tag name prefixed in the method name too. 😕
+
+We will probably still want to keep it for OpenAPI in general, as that will ensure that the operation IDs are **unique**.
+
+But for the generated client we could **modify** the OpenAPI operation IDs right before generating the clients, just to make those method names nicer and **cleaner**.
+
+We could download the OpenAPI JSON to a file `openapi.json` and then we could **remove that prefixed tag** with a script like this:
+
+```Python
+{!../../../docs_src/generate_clients/tutorial004.py!}
+```
+
+With that, the operation IDs would be renamed from things like `items-get_items` to just `get_items`, that way the client generator can generate simpler method names.
+
+### Generate a TypeScript Client with the Preprocessed OpenAPI
+
+Now as the end result is in a file `openapi.json`, you would modify the `package.json` to use that local file, for example:
+
+```JSON hl_lines="7"
+{
+ "name": "frontend-app",
+ "version": "1.0.0",
+ "description": "",
+ "main": "index.js",
+ "scripts": {
+ "generate-client": "openapi --input ./openapi.json --output ./src/client --client axios"
+ },
+ "author": "",
+ "license": "",
+ "devDependencies": {
+ "openapi-typescript-codegen": "^0.20.1",
+ "typescript": "^4.6.2"
+ }
+}
+```
+
+After generating the new client, you would now have **clean method names**, with all the **autocompletion**, **inline errors**, etc:
+
+<img src="/img/tutorial/generate-clients/image08.png">
+
+## Benefits
+
+When using the automatically generated clients you would **autocompletion** for:
+
+* Methods.
+* Request payloads in the body, query parameters, etc.
+* Response payloads.
+
+You would also have **inline errors** for everything.
+
+And whenever you update the backend code, and **regenerate** the frontend, it would have any new *path operations* available as methods, the old ones removed, and any other change would be reflected on the generated code. 🤓
+
+This also means that if something changed it will be **reflected** on the client code automatically. And if you **build** the client it will error out if you have any **mismatch** in the data used.
+
+So, you would **detect many errors** very early in the development cycle instead of having to wait for the errors to show up to your final users in production and then trying to debug where the problem is. ✨
- advanced/extending-openapi.md
- advanced/openapi-callbacks.md
- advanced/wsgi.md
+ - advanced/generate-clients.md
- async.md
- Deployment:
- deployment/index.md
--- /dev/null
+from typing import List
+
+from fastapi import FastAPI
+from pydantic import BaseModel
+
+app = FastAPI()
+
+
+class Item(BaseModel):
+ name: str
+ price: float
+
+
+class ResponseMessage(BaseModel):
+ message: str
+
+
+@app.post("/items/", response_model=ResponseMessage)
+async def create_item(item: Item):
+ return {"message": "item received"}
+
+
+@app.get("/items/", response_model=List[Item])
+async def get_items():
+ return [
+ {"name": "Plumbus", "price": 3},
+ {"name": "Portal Gun", "price": 9001},
+ ]
--- /dev/null
+from fastapi import FastAPI
+from pydantic import BaseModel
+
+app = FastAPI()
+
+
+class Item(BaseModel):
+ name: str
+ price: float
+
+
+class ResponseMessage(BaseModel):
+ message: str
+
+
+@app.post("/items/", response_model=ResponseMessage)
+async def create_item(item: Item):
+ return {"message": "item received"}
+
+
+@app.get("/items/", response_model=list[Item])
+async def get_items():
+ return [
+ {"name": "Plumbus", "price": 3},
+ {"name": "Portal Gun", "price": 9001},
+ ]
--- /dev/null
+from typing import List
+
+from fastapi import FastAPI
+from pydantic import BaseModel
+
+app = FastAPI()
+
+
+class Item(BaseModel):
+ name: str
+ price: float
+
+
+class ResponseMessage(BaseModel):
+ message: str
+
+
+class User(BaseModel):
+ username: str
+ email: str
+
+
+@app.post("/items/", response_model=ResponseMessage, tags=["items"])
+async def create_item(item: Item):
+ return {"message": "Item received"}
+
+
+@app.get("/items/", response_model=List[Item], tags=["items"])
+async def get_items():
+ return [
+ {"name": "Plumbus", "price": 3},
+ {"name": "Portal Gun", "price": 9001},
+ ]
+
+
+@app.post("/users/", response_model=ResponseMessage, tags=["users"])
+async def create_user(user: User):
+ return {"message": "User received"}
--- /dev/null
+from fastapi import FastAPI
+from pydantic import BaseModel
+
+app = FastAPI()
+
+
+class Item(BaseModel):
+ name: str
+ price: float
+
+
+class ResponseMessage(BaseModel):
+ message: str
+
+
+class User(BaseModel):
+ username: str
+ email: str
+
+
+@app.post("/items/", response_model=ResponseMessage, tags=["items"])
+async def create_item(item: Item):
+ return {"message": "Item received"}
+
+
+@app.get("/items/", response_model=list[Item], tags=["items"])
+async def get_items():
+ return [
+ {"name": "Plumbus", "price": 3},
+ {"name": "Portal Gun", "price": 9001},
+ ]
+
+
+@app.post("/users/", response_model=ResponseMessage, tags=["users"])
+async def create_user(user: User):
+ return {"message": "User received"}
--- /dev/null
+from typing import List
+
+from fastapi import FastAPI
+from fastapi.routing import APIRoute
+from pydantic import BaseModel
+
+
+def custom_generate_unique_id(route: APIRoute):
+ return f"{route.tags[0]}-{route.name}"
+
+
+app = FastAPI(generate_unique_id_function=custom_generate_unique_id)
+
+
+class Item(BaseModel):
+ name: str
+ price: float
+
+
+class ResponseMessage(BaseModel):
+ message: str
+
+
+class User(BaseModel):
+ username: str
+ email: str
+
+
+@app.post("/items/", response_model=ResponseMessage, tags=["items"])
+async def create_item(item: Item):
+ return {"message": "Item received"}
+
+
+@app.get("/items/", response_model=List[Item], tags=["items"])
+async def get_items():
+ return [
+ {"name": "Plumbus", "price": 3},
+ {"name": "Portal Gun", "price": 9001},
+ ]
+
+
+@app.post("/users/", response_model=ResponseMessage, tags=["users"])
+async def create_user(user: User):
+ return {"message": "User received"}
--- /dev/null
+from fastapi import FastAPI
+from fastapi.routing import APIRoute
+from pydantic import BaseModel
+
+
+def custom_generate_unique_id(route: APIRoute):
+ return f"{route.tags[0]}-{route.name}"
+
+
+app = FastAPI(generate_unique_id_function=custom_generate_unique_id)
+
+
+class Item(BaseModel):
+ name: str
+ price: float
+
+
+class ResponseMessage(BaseModel):
+ message: str
+
+
+class User(BaseModel):
+ username: str
+ email: str
+
+
+@app.post("/items/", response_model=ResponseMessage, tags=["items"])
+async def create_item(item: Item):
+ return {"message": "Item received"}
+
+
+@app.get("/items/", response_model=list[Item], tags=["items"])
+async def get_items():
+ return [
+ {"name": "Plumbus", "price": 3},
+ {"name": "Portal Gun", "price": 9001},
+ ]
+
+
+@app.post("/users/", response_model=ResponseMessage, tags=["users"])
+async def create_user(user: User):
+ return {"message": "User received"}
--- /dev/null
+import json
+from pathlib import Path
+
+file_path = Path("./openapi.json")
+openapi_content = json.loads(file_path.read_text())
+
+for path_data in openapi_content["paths"].values():
+ for operation in path_data.values():
+ tag = operation["tags"][0]
+ operation_id = operation["operationId"]
+ to_remove = f"{tag}-"
+ new_operation_id = operation_id[len(to_remove) :]
+ operation["operationId"] = new_operation_id
+
+file_path.write_text(json.dumps(openapi_content))
from fastapi.openapi.utils import get_openapi
from fastapi.params import Depends
from fastapi.types import DecoratedCallable
+from fastapi.utils import generate_unique_id
from starlette.applications import Starlette
from starlette.datastructures import State
from starlette.exceptions import ExceptionMiddleware, HTTPException
deprecated: Optional[bool] = None,
include_in_schema: bool = True,
swagger_ui_parameters: Optional[Dict[str, Any]] = None,
+ generate_unique_id_function: Callable[[routing.APIRoute], str] = Default(
+ generate_unique_id
+ ),
**extra: Any,
) -> None:
self._debug: bool = debug
+ self.title = title
+ self.description = description
+ self.version = version
+ self.terms_of_service = terms_of_service
+ self.contact = contact
+ self.license_info = license_info
+ self.openapi_url = openapi_url
+ self.openapi_tags = openapi_tags
+ self.root_path_in_servers = root_path_in_servers
+ self.docs_url = docs_url
+ self.redoc_url = redoc_url
+ self.swagger_ui_oauth2_redirect_url = swagger_ui_oauth2_redirect_url
+ self.swagger_ui_init_oauth = swagger_ui_init_oauth
+ self.swagger_ui_parameters = swagger_ui_parameters
+ self.servers = servers or []
+ self.extra = extra
+ self.openapi_version = "3.0.2"
+ self.openapi_schema: Optional[Dict[str, Any]] = None
+ if self.openapi_url:
+ assert self.title, "A title must be provided for OpenAPI, e.g.: 'My API'"
+ assert self.version, "A version must be provided for OpenAPI, e.g.: '2.1.0'"
+ # TODO: remove when discarding the openapi_prefix parameter
+ if openapi_prefix:
+ logger.warning(
+ '"openapi_prefix" has been deprecated in favor of "root_path", which '
+ "follows more closely the ASGI standard, is simpler, and more "
+ "automatic. Check the docs at "
+ "https://fastapi.tiangolo.com/advanced/sub-applications/"
+ )
+ self.root_path = root_path or openapi_prefix
self.state: State = State()
+ self.dependency_overrides: Dict[Callable[..., Any], Callable[..., Any]] = {}
self.router: routing.APIRouter = routing.APIRouter(
routes=routes,
dependency_overrides_provider=self,
deprecated=deprecated,
include_in_schema=include_in_schema,
responses=responses,
+ generate_unique_id_function=generate_unique_id_function,
)
self.exception_handlers: Dict[
Union[int, Type[Exception]],
[] if middleware is None else list(middleware)
)
self.middleware_stack: ASGIApp = self.build_middleware_stack()
-
- self.title = title
- self.description = description
- self.version = version
- self.terms_of_service = terms_of_service
- self.contact = contact
- self.license_info = license_info
- self.servers = servers or []
- self.openapi_url = openapi_url
- self.openapi_tags = openapi_tags
- # TODO: remove when discarding the openapi_prefix parameter
- if openapi_prefix:
- logger.warning(
- '"openapi_prefix" has been deprecated in favor of "root_path", which '
- "follows more closely the ASGI standard, is simpler, and more "
- "automatic. Check the docs at "
- "https://fastapi.tiangolo.com/advanced/sub-applications/"
- )
- self.root_path = root_path or openapi_prefix
- self.root_path_in_servers = root_path_in_servers
- self.docs_url = docs_url
- self.redoc_url = redoc_url
- self.swagger_ui_oauth2_redirect_url = swagger_ui_oauth2_redirect_url
- self.swagger_ui_init_oauth = swagger_ui_init_oauth
- self.swagger_ui_parameters = swagger_ui_parameters
- self.extra = extra
- self.dependency_overrides: Dict[Callable[..., Any], Callable[..., Any]] = {}
-
- self.openapi_version = "3.0.2"
-
- if self.openapi_url:
- assert self.title, "A title must be provided for OpenAPI, e.g.: 'My API'"
- assert self.version, "A version must be provided for OpenAPI, e.g.: '2.1.0'"
- self.openapi_schema: Optional[Dict[str, Any]] = None
self.setup()
def build_middleware_stack(self) -> ASGIApp:
),
name: Optional[str] = None,
openapi_extra: Optional[Dict[str, Any]] = None,
+ generate_unique_id_function: Callable[[routing.APIRoute], str] = Default(
+ generate_unique_id
+ ),
) -> None:
self.router.add_api_route(
path,
response_class=response_class,
name=name,
openapi_extra=openapi_extra,
+ generate_unique_id_function=generate_unique_id_function,
)
def api_route(
response_class: Type[Response] = Default(JSONResponse),
name: Optional[str] = None,
openapi_extra: Optional[Dict[str, Any]] = None,
+ generate_unique_id_function: Callable[[routing.APIRoute], str] = Default(
+ generate_unique_id
+ ),
) -> Callable[[DecoratedCallable], DecoratedCallable]:
def decorator(func: DecoratedCallable) -> DecoratedCallable:
self.router.add_api_route(
response_class=response_class,
name=name,
openapi_extra=openapi_extra,
+ generate_unique_id_function=generate_unique_id_function,
)
return func
include_in_schema: bool = True,
default_response_class: Type[Response] = Default(JSONResponse),
callbacks: Optional[List[BaseRoute]] = None,
+ generate_unique_id_function: Callable[[routing.APIRoute], str] = Default(
+ generate_unique_id
+ ),
) -> None:
self.router.include_router(
router,
include_in_schema=include_in_schema,
default_response_class=default_response_class,
callbacks=callbacks,
+ generate_unique_id_function=generate_unique_id_function,
)
def get(
name: Optional[str] = None,
callbacks: Optional[List[BaseRoute]] = None,
openapi_extra: Optional[Dict[str, Any]] = None,
+ generate_unique_id_function: Callable[[routing.APIRoute], str] = Default(
+ generate_unique_id
+ ),
) -> Callable[[DecoratedCallable], DecoratedCallable]:
return self.router.get(
path,
name=name,
callbacks=callbacks,
openapi_extra=openapi_extra,
+ generate_unique_id_function=generate_unique_id_function,
)
def put(
name: Optional[str] = None,
callbacks: Optional[List[BaseRoute]] = None,
openapi_extra: Optional[Dict[str, Any]] = None,
+ generate_unique_id_function: Callable[[routing.APIRoute], str] = Default(
+ generate_unique_id
+ ),
) -> Callable[[DecoratedCallable], DecoratedCallable]:
return self.router.put(
path,
name=name,
callbacks=callbacks,
openapi_extra=openapi_extra,
+ generate_unique_id_function=generate_unique_id_function,
)
def post(
name: Optional[str] = None,
callbacks: Optional[List[BaseRoute]] = None,
openapi_extra: Optional[Dict[str, Any]] = None,
+ generate_unique_id_function: Callable[[routing.APIRoute], str] = Default(
+ generate_unique_id
+ ),
) -> Callable[[DecoratedCallable], DecoratedCallable]:
return self.router.post(
path,
name=name,
callbacks=callbacks,
openapi_extra=openapi_extra,
+ generate_unique_id_function=generate_unique_id_function,
)
def delete(
name: Optional[str] = None,
callbacks: Optional[List[BaseRoute]] = None,
openapi_extra: Optional[Dict[str, Any]] = None,
+ generate_unique_id_function: Callable[[routing.APIRoute], str] = Default(
+ generate_unique_id
+ ),
) -> Callable[[DecoratedCallable], DecoratedCallable]:
return self.router.delete(
path,
name=name,
callbacks=callbacks,
openapi_extra=openapi_extra,
+ generate_unique_id_function=generate_unique_id_function,
)
def options(
name: Optional[str] = None,
callbacks: Optional[List[BaseRoute]] = None,
openapi_extra: Optional[Dict[str, Any]] = None,
+ generate_unique_id_function: Callable[[routing.APIRoute], str] = Default(
+ generate_unique_id
+ ),
) -> Callable[[DecoratedCallable], DecoratedCallable]:
return self.router.options(
path,
name=name,
callbacks=callbacks,
openapi_extra=openapi_extra,
+ generate_unique_id_function=generate_unique_id_function,
)
def head(
name: Optional[str] = None,
callbacks: Optional[List[BaseRoute]] = None,
openapi_extra: Optional[Dict[str, Any]] = None,
+ generate_unique_id_function: Callable[[routing.APIRoute], str] = Default(
+ generate_unique_id
+ ),
) -> Callable[[DecoratedCallable], DecoratedCallable]:
return self.router.head(
path,
name=name,
callbacks=callbacks,
openapi_extra=openapi_extra,
+ generate_unique_id_function=generate_unique_id_function,
)
def patch(
name: Optional[str] = None,
callbacks: Optional[List[BaseRoute]] = None,
openapi_extra: Optional[Dict[str, Any]] = None,
+ generate_unique_id_function: Callable[[routing.APIRoute], str] = Default(
+ generate_unique_id
+ ),
) -> Callable[[DecoratedCallable], DecoratedCallable]:
return self.router.patch(
path,
name=name,
callbacks=callbacks,
openapi_extra=openapi_extra,
+ generate_unique_id_function=generate_unique_id_function,
)
def trace(
name: Optional[str] = None,
callbacks: Optional[List[BaseRoute]] = None,
openapi_extra: Optional[Dict[str, Any]] = None,
+ generate_unique_id_function: Callable[[routing.APIRoute], str] = Default(
+ generate_unique_id
+ ),
) -> Callable[[DecoratedCallable], DecoratedCallable]:
return self.router.trace(
path,
name=name,
callbacks=callbacks,
openapi_extra=openapi_extra,
+ generate_unique_id_function=generate_unique_id_function,
)
import http.client
import inspect
+import warnings
from enum import Enum
from typing import Any, Dict, List, Optional, Sequence, Set, Tuple, Type, Union, cast
return request_body_oai
-def generate_operation_id(*, route: routing.APIRoute, method: str) -> str:
+def generate_operation_id(
+ *, route: routing.APIRoute, method: str
+) -> str: # pragma: nocover
+ warnings.warn(
+ "fastapi.openapi.utils.generate_operation_id() was deprecated, "
+ "it is not used internally, and will be removed soon",
+ DeprecationWarning,
+ stacklevel=2,
+ )
if route.operation_id:
return route.operation_id
path: str = route.path_format
def get_openapi_operation_metadata(
- *, route: routing.APIRoute, method: str
+ *, route: routing.APIRoute, method: str, operation_ids: Set[str]
) -> Dict[str, Any]:
operation: Dict[str, Any] = {}
if route.tags:
operation["summary"] = generate_operation_summary(route=route, method=method)
if route.description:
operation["description"] = route.description
- operation["operationId"] = generate_operation_id(route=route, method=method)
+ operation_id = route.operation_id or route.unique_id
+ if operation_id in operation_ids:
+ message = (
+ f"Duplicate Operation ID {operation_id} for function "
+ + f"{route.endpoint.__name__}"
+ )
+ file_name = getattr(route.endpoint, "__globals__", {}).get("__file__")
+ if file_name:
+ message += f" at {file_name}"
+ warnings.warn(message)
+ operation_ids.add(operation_id)
+ operation["operationId"] = operation_id
if route.deprecated:
operation["deprecated"] = route.deprecated
return operation
def get_openapi_path(
- *, route: routing.APIRoute, model_name_map: Dict[type, str]
+ *, route: routing.APIRoute, model_name_map: Dict[type, str], operation_ids: Set[str]
) -> Tuple[Dict[str, Any], Dict[str, Any], Dict[str, Any]]:
path = {}
security_schemes: Dict[str, Any] = {}
route_response_media_type: Optional[str] = current_response_class.media_type
if route.include_in_schema:
for method in route.methods:
- operation = get_openapi_operation_metadata(route=route, method=method)
+ operation = get_openapi_operation_metadata(
+ route=route, method=method, operation_ids=operation_ids
+ )
parameters: List[Dict[str, Any]] = []
flat_dependant = get_flat_dependant(route.dependant, skip_repeats=True)
security_definitions, operation_security = get_openapi_security_definitions(
cb_security_schemes,
cb_definitions,
) = get_openapi_path(
- route=callback, model_name_map=model_name_map
+ route=callback,
+ model_name_map=model_name_map,
+ operation_ids=operation_ids,
)
callbacks[callback.name] = {callback.path: cb_path}
operation["callbacks"] = callbacks
output["servers"] = servers
components: Dict[str, Dict[str, Any]] = {}
paths: Dict[str, Dict[str, Any]] = {}
+ operation_ids: Set[str] = set()
flat_models = get_flat_models_from_routes(routes)
model_name_map = get_model_name_map(flat_models)
definitions = get_model_definitions(
)
for route in routes:
if isinstance(route, routing.APIRoute):
- result = get_openapi_path(route=route, model_name_map=model_name_map)
+ result = get_openapi_path(
+ route=route, model_name_map=model_name_map, operation_ids=operation_ids
+ )
if result:
path, security_schemes, path_definitions = result
if path:
from fastapi.utils import (
create_cloned_field,
create_response_field,
- generate_operation_id_for_path,
+ generate_unique_id,
get_value_or_default,
)
from pydantic import BaseModel
dependency_overrides_provider: Optional[Any] = None,
callbacks: Optional[List[BaseRoute]] = None,
openapi_extra: Optional[Dict[str, Any]] = None,
+ generate_unique_id_function: Union[
+ Callable[["APIRoute"], str], DefaultPlaceholder
+ ] = Default(generate_unique_id),
) -> None:
- # normalise enums e.g. http.HTTPStatus
- if isinstance(status_code, IntEnum):
- status_code = int(status_code)
self.path = path
self.endpoint = endpoint
+ self.response_model = response_model
+ self.summary = summary
+ self.response_description = response_description
+ self.deprecated = deprecated
+ self.operation_id = operation_id
+ self.response_model_include = response_model_include
+ self.response_model_exclude = response_model_exclude
+ self.response_model_by_alias = response_model_by_alias
+ self.response_model_exclude_unset = response_model_exclude_unset
+ self.response_model_exclude_defaults = response_model_exclude_defaults
+ self.response_model_exclude_none = response_model_exclude_none
+ self.include_in_schema = include_in_schema
+ self.response_class = response_class
+ self.dependency_overrides_provider = dependency_overrides_provider
+ self.callbacks = callbacks
+ self.openapi_extra = openapi_extra
+ self.generate_unique_id_function = generate_unique_id_function
+ self.tags = tags or []
+ self.responses = responses or {}
self.name = get_name(endpoint) if name is None else name
self.path_regex, self.path_format, self.param_convertors = compile_path(path)
if methods is None:
methods = ["GET"]
self.methods: Set[str] = set([method.upper() for method in methods])
- self.unique_id = generate_operation_id_for_path(
- name=self.name, path=self.path_format, method=list(methods)[0]
- )
- self.response_model = response_model
+ if isinstance(generate_unique_id_function, DefaultPlaceholder):
+ current_generate_unique_id: Callable[
+ ["APIRoute"], str
+ ] = generate_unique_id_function.value
+ else:
+ current_generate_unique_id = generate_unique_id_function
+ self.unique_id = self.operation_id or current_generate_unique_id(self)
+ # normalize enums e.g. http.HTTPStatus
+ if isinstance(status_code, IntEnum):
+ status_code = int(status_code)
+ self.status_code = status_code
if self.response_model:
assert (
status_code not in STATUS_CODES_WITH_NO_BODY
else:
self.response_field = None # type: ignore
self.secure_cloned_response_field = None
- self.status_code = status_code
- self.tags = tags or []
if dependencies:
self.dependencies = list(dependencies)
else:
self.dependencies = []
- self.summary = summary
self.description = description or inspect.cleandoc(self.endpoint.__doc__ or "")
# if a "form feed" character (page break) is found in the description text,
# truncate description text to the content preceding the first "form feed"
self.description = self.description.split("\f")[0]
- self.response_description = response_description
- self.responses = responses or {}
response_fields = {}
for additional_status_code, response in self.responses.items():
assert isinstance(response, dict), "An additional response must be a dict"
self.response_fields: Dict[Union[int, str], ModelField] = response_fields
else:
self.response_fields = {}
- self.deprecated = deprecated
- self.operation_id = operation_id
- self.response_model_include = response_model_include
- self.response_model_exclude = response_model_exclude
- self.response_model_by_alias = response_model_by_alias
- self.response_model_exclude_unset = response_model_exclude_unset
- self.response_model_exclude_defaults = response_model_exclude_defaults
- self.response_model_exclude_none = response_model_exclude_none
- self.include_in_schema = include_in_schema
- self.response_class = response_class
assert callable(endpoint), "An endpoint must be a callable"
self.dependant = get_dependant(path=self.path_format, call=self.endpoint)
get_parameterless_sub_dependant(depends=depends, path=self.path_format),
)
self.body_field = get_body_field(dependant=self.dependant, name=self.unique_id)
- self.dependency_overrides_provider = dependency_overrides_provider
- self.callbacks = callbacks
self.app = request_response(self.get_route_handler())
- self.openapi_extra = openapi_extra
def get_route_handler(self) -> Callable[[Request], Coroutine[Any, Any, Response]]:
return get_request_handler(
on_shutdown: Optional[Sequence[Callable[[], Any]]] = None,
deprecated: Optional[bool] = None,
include_in_schema: bool = True,
+ generate_unique_id_function: Callable[[APIRoute], str] = Default(
+ generate_unique_id
+ ),
) -> None:
super().__init__(
routes=routes, # type: ignore # in Starlette
self.dependency_overrides_provider = dependency_overrides_provider
self.route_class = route_class
self.default_response_class = default_response_class
+ self.generate_unique_id_function = generate_unique_id_function
def add_api_route(
self,
route_class_override: Optional[Type[APIRoute]] = None,
callbacks: Optional[List[BaseRoute]] = None,
openapi_extra: Optional[Dict[str, Any]] = None,
+ generate_unique_id_function: Union[
+ Callable[[APIRoute], str], DefaultPlaceholder
+ ] = Default(generate_unique_id),
) -> None:
route_class = route_class_override or self.route_class
responses = responses or {}
current_callbacks = self.callbacks.copy()
if callbacks:
current_callbacks.extend(callbacks)
+ current_generate_unique_id = get_value_or_default(
+ generate_unique_id_function, self.generate_unique_id_function
+ )
route = route_class(
self.prefix + path,
endpoint=endpoint,
dependency_overrides_provider=self.dependency_overrides_provider,
callbacks=current_callbacks,
openapi_extra=openapi_extra,
+ generate_unique_id_function=current_generate_unique_id,
)
self.routes.append(route)
name: Optional[str] = None,
callbacks: Optional[List[BaseRoute]] = None,
openapi_extra: Optional[Dict[str, Any]] = None,
+ generate_unique_id_function: Callable[[APIRoute], str] = Default(
+ generate_unique_id
+ ),
) -> Callable[[DecoratedCallable], DecoratedCallable]:
def decorator(func: DecoratedCallable) -> DecoratedCallable:
self.add_api_route(
name=name,
callbacks=callbacks,
openapi_extra=openapi_extra,
+ generate_unique_id_function=generate_unique_id_function,
)
return func
callbacks: Optional[List[BaseRoute]] = None,
deprecated: Optional[bool] = None,
include_in_schema: bool = True,
+ generate_unique_id_function: Callable[[APIRoute], str] = Default(
+ generate_unique_id
+ ),
) -> None:
if prefix:
assert prefix.startswith("/"), "A path prefix must start with '/'"
current_callbacks.extend(callbacks)
if route.callbacks:
current_callbacks.extend(route.callbacks)
+ current_generate_unique_id = get_value_or_default(
+ route.generate_unique_id_function,
+ router.generate_unique_id_function,
+ generate_unique_id_function,
+ self.generate_unique_id_function,
+ )
self.add_api_route(
prefix + route.path,
route.endpoint,
route_class_override=type(route),
callbacks=current_callbacks,
openapi_extra=route.openapi_extra,
+ generate_unique_id_function=current_generate_unique_id,
)
elif isinstance(route, routing.Route):
methods = list(route.methods or []) # type: ignore # in Starlette
name: Optional[str] = None,
callbacks: Optional[List[BaseRoute]] = None,
openapi_extra: Optional[Dict[str, Any]] = None,
+ generate_unique_id_function: Callable[[APIRoute], str] = Default(
+ generate_unique_id
+ ),
) -> Callable[[DecoratedCallable], DecoratedCallable]:
return self.api_route(
path=path,
name=name,
callbacks=callbacks,
openapi_extra=openapi_extra,
+ generate_unique_id_function=generate_unique_id_function,
)
def put(
name: Optional[str] = None,
callbacks: Optional[List[BaseRoute]] = None,
openapi_extra: Optional[Dict[str, Any]] = None,
+ generate_unique_id_function: Callable[[APIRoute], str] = Default(
+ generate_unique_id
+ ),
) -> Callable[[DecoratedCallable], DecoratedCallable]:
return self.api_route(
path=path,
name=name,
callbacks=callbacks,
openapi_extra=openapi_extra,
+ generate_unique_id_function=generate_unique_id_function,
)
def post(
name: Optional[str] = None,
callbacks: Optional[List[BaseRoute]] = None,
openapi_extra: Optional[Dict[str, Any]] = None,
+ generate_unique_id_function: Callable[[APIRoute], str] = Default(
+ generate_unique_id
+ ),
) -> Callable[[DecoratedCallable], DecoratedCallable]:
return self.api_route(
path=path,
name=name,
callbacks=callbacks,
openapi_extra=openapi_extra,
+ generate_unique_id_function=generate_unique_id_function,
)
def delete(
name: Optional[str] = None,
callbacks: Optional[List[BaseRoute]] = None,
openapi_extra: Optional[Dict[str, Any]] = None,
+ generate_unique_id_function: Callable[[APIRoute], str] = Default(
+ generate_unique_id
+ ),
) -> Callable[[DecoratedCallable], DecoratedCallable]:
return self.api_route(
path=path,
name=name,
callbacks=callbacks,
openapi_extra=openapi_extra,
+ generate_unique_id_function=generate_unique_id_function,
)
def options(
name: Optional[str] = None,
callbacks: Optional[List[BaseRoute]] = None,
openapi_extra: Optional[Dict[str, Any]] = None,
+ generate_unique_id_function: Callable[[APIRoute], str] = Default(
+ generate_unique_id
+ ),
) -> Callable[[DecoratedCallable], DecoratedCallable]:
return self.api_route(
path=path,
name=name,
callbacks=callbacks,
openapi_extra=openapi_extra,
+ generate_unique_id_function=generate_unique_id_function,
)
def head(
name: Optional[str] = None,
callbacks: Optional[List[BaseRoute]] = None,
openapi_extra: Optional[Dict[str, Any]] = None,
+ generate_unique_id_function: Callable[[APIRoute], str] = Default(
+ generate_unique_id
+ ),
) -> Callable[[DecoratedCallable], DecoratedCallable]:
return self.api_route(
path=path,
name=name,
callbacks=callbacks,
openapi_extra=openapi_extra,
+ generate_unique_id_function=generate_unique_id_function,
)
def patch(
name: Optional[str] = None,
callbacks: Optional[List[BaseRoute]] = None,
openapi_extra: Optional[Dict[str, Any]] = None,
+ generate_unique_id_function: Callable[[APIRoute], str] = Default(
+ generate_unique_id
+ ),
) -> Callable[[DecoratedCallable], DecoratedCallable]:
return self.api_route(
path=path,
name=name,
callbacks=callbacks,
openapi_extra=openapi_extra,
+ generate_unique_id_function=generate_unique_id_function,
)
def trace(
name: Optional[str] = None,
callbacks: Optional[List[BaseRoute]] = None,
openapi_extra: Optional[Dict[str, Any]] = None,
+ generate_unique_id_function: Callable[[APIRoute], str] = Default(
+ generate_unique_id
+ ),
) -> Callable[[DecoratedCallable], DecoratedCallable]:
return self.api_route(
name=name,
callbacks=callbacks,
openapi_extra=openapi_extra,
+ generate_unique_id_function=generate_unique_id_function,
)
import functools
import re
+import warnings
from dataclasses import is_dataclass
from enum import Enum
-from typing import Any, Dict, Optional, Set, Type, Union, cast
+from typing import TYPE_CHECKING, Any, Dict, Optional, Set, Type, Union, cast
import fastapi
from fastapi.datastructures import DefaultPlaceholder, DefaultType
from pydantic.schema import model_process_schema
from pydantic.utils import lenient_issubclass
+if TYPE_CHECKING: # pragma: nocover
+ from .routing import APIRoute
+
def get_model_definitions(
*,
return new_field
-def generate_operation_id_for_path(*, name: str, path: str, method: str) -> str:
+def generate_operation_id_for_path(
+ *, name: str, path: str, method: str
+) -> str: # pragma: nocover
+ warnings.warn(
+ "fastapi.utils.generate_operation_id_for_path() was deprecated, "
+ "it is not used internally, and will be removed soon",
+ DeprecationWarning,
+ stacklevel=2,
+ )
operation_id = name + path
operation_id = re.sub("[^0-9a-zA-Z_]", "_", operation_id)
operation_id = operation_id + "_" + method.lower()
return operation_id
+def generate_unique_id(route: "APIRoute") -> str:
+ operation_id = route.name + route.path_format
+ operation_id = re.sub("[^0-9a-zA-Z_]", "_", operation_id)
+ assert route.methods
+ operation_id = operation_id + "_" + list(route.methods)[0].lower()
+ return operation_id
+
+
def deep_dict_update(main_dict: Dict[Any, Any], update_dict: Dict[Any, Any]) -> None:
for key in update_dict:
if (
--- /dev/null
+import warnings
+from typing import List
+
+from fastapi import APIRouter, FastAPI
+from fastapi.routing import APIRoute
+from fastapi.testclient import TestClient
+from pydantic import BaseModel
+
+
+def custom_generate_unique_id(route: APIRoute):
+ return f"foo_{route.name}"
+
+
+def custom_generate_unique_id2(route: APIRoute):
+ return f"bar_{route.name}"
+
+
+def custom_generate_unique_id3(route: APIRoute):
+ return f"baz_{route.name}"
+
+
+class Item(BaseModel):
+ name: str
+ price: float
+
+
+class Message(BaseModel):
+ title: str
+ description: str
+
+
+def test_top_level_generate_unique_id():
+ app = FastAPI(generate_unique_id_function=custom_generate_unique_id)
+ router = APIRouter()
+
+ @app.post("/", response_model=List[Item], responses={404: {"model": List[Message]}})
+ def post_root(item1: Item, item2: Item):
+ return item1, item2 # pragma: nocover
+
+ @router.post(
+ "/router", response_model=List[Item], responses={404: {"model": List[Message]}}
+ )
+ def post_router(item1: Item, item2: Item):
+ return item1, item2 # pragma: nocover
+
+ app.include_router(router)
+ client = TestClient(app)
+ response = client.get("/openapi.json")
+ data = response.json()
+ assert data == {
+ "openapi": "3.0.2",
+ "info": {"title": "FastAPI", "version": "0.1.0"},
+ "paths": {
+ "/": {
+ "post": {
+ "summary": "Post Root",
+ "operationId": "foo_post_root",
+ "requestBody": {
+ "content": {
+ "application/json": {
+ "schema": {
+ "$ref": "#/components/schemas/Body_foo_post_root"
+ }
+ }
+ },
+ "required": True,
+ },
+ "responses": {
+ "200": {
+ "description": "Successful Response",
+ "content": {
+ "application/json": {
+ "schema": {
+ "title": "Response Foo Post Root",
+ "type": "array",
+ "items": {"$ref": "#/components/schemas/Item"},
+ }
+ }
+ },
+ },
+ "404": {
+ "description": "Not Found",
+ "content": {
+ "application/json": {
+ "schema": {
+ "title": "Response 404 Foo Post Root",
+ "type": "array",
+ "items": {
+ "$ref": "#/components/schemas/Message"
+ },
+ }
+ }
+ },
+ },
+ "422": {
+ "description": "Validation Error",
+ "content": {
+ "application/json": {
+ "schema": {
+ "$ref": "#/components/schemas/HTTPValidationError"
+ }
+ }
+ },
+ },
+ },
+ }
+ },
+ "/router": {
+ "post": {
+ "summary": "Post Router",
+ "operationId": "foo_post_router",
+ "requestBody": {
+ "content": {
+ "application/json": {
+ "schema": {
+ "$ref": "#/components/schemas/Body_foo_post_router"
+ }
+ }
+ },
+ "required": True,
+ },
+ "responses": {
+ "200": {
+ "description": "Successful Response",
+ "content": {
+ "application/json": {
+ "schema": {
+ "title": "Response Foo Post Router",
+ "type": "array",
+ "items": {"$ref": "#/components/schemas/Item"},
+ }
+ }
+ },
+ },
+ "404": {
+ "description": "Not Found",
+ "content": {
+ "application/json": {
+ "schema": {
+ "title": "Response 404 Foo Post Router",
+ "type": "array",
+ "items": {
+ "$ref": "#/components/schemas/Message"
+ },
+ }
+ }
+ },
+ },
+ "422": {
+ "description": "Validation Error",
+ "content": {
+ "application/json": {
+ "schema": {
+ "$ref": "#/components/schemas/HTTPValidationError"
+ }
+ }
+ },
+ },
+ },
+ }
+ },
+ },
+ "components": {
+ "schemas": {
+ "Body_foo_post_root": {
+ "title": "Body_foo_post_root",
+ "required": ["item1", "item2"],
+ "type": "object",
+ "properties": {
+ "item1": {"$ref": "#/components/schemas/Item"},
+ "item2": {"$ref": "#/components/schemas/Item"},
+ },
+ },
+ "Body_foo_post_router": {
+ "title": "Body_foo_post_router",
+ "required": ["item1", "item2"],
+ "type": "object",
+ "properties": {
+ "item1": {"$ref": "#/components/schemas/Item"},
+ "item2": {"$ref": "#/components/schemas/Item"},
+ },
+ },
+ "HTTPValidationError": {
+ "title": "HTTPValidationError",
+ "type": "object",
+ "properties": {
+ "detail": {
+ "title": "Detail",
+ "type": "array",
+ "items": {"$ref": "#/components/schemas/ValidationError"},
+ }
+ },
+ },
+ "Item": {
+ "title": "Item",
+ "required": ["name", "price"],
+ "type": "object",
+ "properties": {
+ "name": {"title": "Name", "type": "string"},
+ "price": {"title": "Price", "type": "number"},
+ },
+ },
+ "Message": {
+ "title": "Message",
+ "required": ["title", "description"],
+ "type": "object",
+ "properties": {
+ "title": {"title": "Title", "type": "string"},
+ "description": {"title": "Description", "type": "string"},
+ },
+ },
+ "ValidationError": {
+ "title": "ValidationError",
+ "required": ["loc", "msg", "type"],
+ "type": "object",
+ "properties": {
+ "loc": {
+ "title": "Location",
+ "type": "array",
+ "items": {"type": "string"},
+ },
+ "msg": {"title": "Message", "type": "string"},
+ "type": {"title": "Error Type", "type": "string"},
+ },
+ },
+ }
+ },
+ }
+
+
+def test_router_overrides_generate_unique_id():
+ app = FastAPI(generate_unique_id_function=custom_generate_unique_id)
+ router = APIRouter(generate_unique_id_function=custom_generate_unique_id2)
+
+ @app.post("/", response_model=List[Item], responses={404: {"model": List[Message]}})
+ def post_root(item1: Item, item2: Item):
+ return item1, item2 # pragma: nocover
+
+ @router.post(
+ "/router", response_model=List[Item], responses={404: {"model": List[Message]}}
+ )
+ def post_router(item1: Item, item2: Item):
+ return item1, item2 # pragma: nocover
+
+ app.include_router(router)
+ client = TestClient(app)
+ response = client.get("/openapi.json")
+ data = response.json()
+ assert data == {
+ "openapi": "3.0.2",
+ "info": {"title": "FastAPI", "version": "0.1.0"},
+ "paths": {
+ "/": {
+ "post": {
+ "summary": "Post Root",
+ "operationId": "foo_post_root",
+ "requestBody": {
+ "content": {
+ "application/json": {
+ "schema": {
+ "$ref": "#/components/schemas/Body_foo_post_root"
+ }
+ }
+ },
+ "required": True,
+ },
+ "responses": {
+ "200": {
+ "description": "Successful Response",
+ "content": {
+ "application/json": {
+ "schema": {
+ "title": "Response Foo Post Root",
+ "type": "array",
+ "items": {"$ref": "#/components/schemas/Item"},
+ }
+ }
+ },
+ },
+ "404": {
+ "description": "Not Found",
+ "content": {
+ "application/json": {
+ "schema": {
+ "title": "Response 404 Foo Post Root",
+ "type": "array",
+ "items": {
+ "$ref": "#/components/schemas/Message"
+ },
+ }
+ }
+ },
+ },
+ "422": {
+ "description": "Validation Error",
+ "content": {
+ "application/json": {
+ "schema": {
+ "$ref": "#/components/schemas/HTTPValidationError"
+ }
+ }
+ },
+ },
+ },
+ }
+ },
+ "/router": {
+ "post": {
+ "summary": "Post Router",
+ "operationId": "bar_post_router",
+ "requestBody": {
+ "content": {
+ "application/json": {
+ "schema": {
+ "$ref": "#/components/schemas/Body_bar_post_router"
+ }
+ }
+ },
+ "required": True,
+ },
+ "responses": {
+ "200": {
+ "description": "Successful Response",
+ "content": {
+ "application/json": {
+ "schema": {
+ "title": "Response Bar Post Router",
+ "type": "array",
+ "items": {"$ref": "#/components/schemas/Item"},
+ }
+ }
+ },
+ },
+ "404": {
+ "description": "Not Found",
+ "content": {
+ "application/json": {
+ "schema": {
+ "title": "Response 404 Bar Post Router",
+ "type": "array",
+ "items": {
+ "$ref": "#/components/schemas/Message"
+ },
+ }
+ }
+ },
+ },
+ "422": {
+ "description": "Validation Error",
+ "content": {
+ "application/json": {
+ "schema": {
+ "$ref": "#/components/schemas/HTTPValidationError"
+ }
+ }
+ },
+ },
+ },
+ }
+ },
+ },
+ "components": {
+ "schemas": {
+ "Body_bar_post_router": {
+ "title": "Body_bar_post_router",
+ "required": ["item1", "item2"],
+ "type": "object",
+ "properties": {
+ "item1": {"$ref": "#/components/schemas/Item"},
+ "item2": {"$ref": "#/components/schemas/Item"},
+ },
+ },
+ "Body_foo_post_root": {
+ "title": "Body_foo_post_root",
+ "required": ["item1", "item2"],
+ "type": "object",
+ "properties": {
+ "item1": {"$ref": "#/components/schemas/Item"},
+ "item2": {"$ref": "#/components/schemas/Item"},
+ },
+ },
+ "HTTPValidationError": {
+ "title": "HTTPValidationError",
+ "type": "object",
+ "properties": {
+ "detail": {
+ "title": "Detail",
+ "type": "array",
+ "items": {"$ref": "#/components/schemas/ValidationError"},
+ }
+ },
+ },
+ "Item": {
+ "title": "Item",
+ "required": ["name", "price"],
+ "type": "object",
+ "properties": {
+ "name": {"title": "Name", "type": "string"},
+ "price": {"title": "Price", "type": "number"},
+ },
+ },
+ "Message": {
+ "title": "Message",
+ "required": ["title", "description"],
+ "type": "object",
+ "properties": {
+ "title": {"title": "Title", "type": "string"},
+ "description": {"title": "Description", "type": "string"},
+ },
+ },
+ "ValidationError": {
+ "title": "ValidationError",
+ "required": ["loc", "msg", "type"],
+ "type": "object",
+ "properties": {
+ "loc": {
+ "title": "Location",
+ "type": "array",
+ "items": {"type": "string"},
+ },
+ "msg": {"title": "Message", "type": "string"},
+ "type": {"title": "Error Type", "type": "string"},
+ },
+ },
+ }
+ },
+ }
+
+
+def test_router_include_overrides_generate_unique_id():
+ app = FastAPI(generate_unique_id_function=custom_generate_unique_id)
+ router = APIRouter(generate_unique_id_function=custom_generate_unique_id2)
+
+ @app.post("/", response_model=List[Item], responses={404: {"model": List[Message]}})
+ def post_root(item1: Item, item2: Item):
+ return item1, item2 # pragma: nocover
+
+ @router.post(
+ "/router", response_model=List[Item], responses={404: {"model": List[Message]}}
+ )
+ def post_router(item1: Item, item2: Item):
+ return item1, item2 # pragma: nocover
+
+ app.include_router(router, generate_unique_id_function=custom_generate_unique_id3)
+ client = TestClient(app)
+ response = client.get("/openapi.json")
+ data = response.json()
+ assert data == {
+ "openapi": "3.0.2",
+ "info": {"title": "FastAPI", "version": "0.1.0"},
+ "paths": {
+ "/": {
+ "post": {
+ "summary": "Post Root",
+ "operationId": "foo_post_root",
+ "requestBody": {
+ "content": {
+ "application/json": {
+ "schema": {
+ "$ref": "#/components/schemas/Body_foo_post_root"
+ }
+ }
+ },
+ "required": True,
+ },
+ "responses": {
+ "200": {
+ "description": "Successful Response",
+ "content": {
+ "application/json": {
+ "schema": {
+ "title": "Response Foo Post Root",
+ "type": "array",
+ "items": {"$ref": "#/components/schemas/Item"},
+ }
+ }
+ },
+ },
+ "404": {
+ "description": "Not Found",
+ "content": {
+ "application/json": {
+ "schema": {
+ "title": "Response 404 Foo Post Root",
+ "type": "array",
+ "items": {
+ "$ref": "#/components/schemas/Message"
+ },
+ }
+ }
+ },
+ },
+ "422": {
+ "description": "Validation Error",
+ "content": {
+ "application/json": {
+ "schema": {
+ "$ref": "#/components/schemas/HTTPValidationError"
+ }
+ }
+ },
+ },
+ },
+ }
+ },
+ "/router": {
+ "post": {
+ "summary": "Post Router",
+ "operationId": "bar_post_router",
+ "requestBody": {
+ "content": {
+ "application/json": {
+ "schema": {
+ "$ref": "#/components/schemas/Body_bar_post_router"
+ }
+ }
+ },
+ "required": True,
+ },
+ "responses": {
+ "200": {
+ "description": "Successful Response",
+ "content": {
+ "application/json": {
+ "schema": {
+ "title": "Response Bar Post Router",
+ "type": "array",
+ "items": {"$ref": "#/components/schemas/Item"},
+ }
+ }
+ },
+ },
+ "404": {
+ "description": "Not Found",
+ "content": {
+ "application/json": {
+ "schema": {
+ "title": "Response 404 Bar Post Router",
+ "type": "array",
+ "items": {
+ "$ref": "#/components/schemas/Message"
+ },
+ }
+ }
+ },
+ },
+ "422": {
+ "description": "Validation Error",
+ "content": {
+ "application/json": {
+ "schema": {
+ "$ref": "#/components/schemas/HTTPValidationError"
+ }
+ }
+ },
+ },
+ },
+ }
+ },
+ },
+ "components": {
+ "schemas": {
+ "Body_bar_post_router": {
+ "title": "Body_bar_post_router",
+ "required": ["item1", "item2"],
+ "type": "object",
+ "properties": {
+ "item1": {"$ref": "#/components/schemas/Item"},
+ "item2": {"$ref": "#/components/schemas/Item"},
+ },
+ },
+ "Body_foo_post_root": {
+ "title": "Body_foo_post_root",
+ "required": ["item1", "item2"],
+ "type": "object",
+ "properties": {
+ "item1": {"$ref": "#/components/schemas/Item"},
+ "item2": {"$ref": "#/components/schemas/Item"},
+ },
+ },
+ "HTTPValidationError": {
+ "title": "HTTPValidationError",
+ "type": "object",
+ "properties": {
+ "detail": {
+ "title": "Detail",
+ "type": "array",
+ "items": {"$ref": "#/components/schemas/ValidationError"},
+ }
+ },
+ },
+ "Item": {
+ "title": "Item",
+ "required": ["name", "price"],
+ "type": "object",
+ "properties": {
+ "name": {"title": "Name", "type": "string"},
+ "price": {"title": "Price", "type": "number"},
+ },
+ },
+ "Message": {
+ "title": "Message",
+ "required": ["title", "description"],
+ "type": "object",
+ "properties": {
+ "title": {"title": "Title", "type": "string"},
+ "description": {"title": "Description", "type": "string"},
+ },
+ },
+ "ValidationError": {
+ "title": "ValidationError",
+ "required": ["loc", "msg", "type"],
+ "type": "object",
+ "properties": {
+ "loc": {
+ "title": "Location",
+ "type": "array",
+ "items": {"type": "string"},
+ },
+ "msg": {"title": "Message", "type": "string"},
+ "type": {"title": "Error Type", "type": "string"},
+ },
+ },
+ }
+ },
+ }
+
+
+def test_subrouter_top_level_include_overrides_generate_unique_id():
+ app = FastAPI(generate_unique_id_function=custom_generate_unique_id)
+ router = APIRouter()
+ sub_router = APIRouter(generate_unique_id_function=custom_generate_unique_id2)
+
+ @app.post("/", response_model=List[Item], responses={404: {"model": List[Message]}})
+ def post_root(item1: Item, item2: Item):
+ return item1, item2 # pragma: nocover
+
+ @router.post(
+ "/router", response_model=List[Item], responses={404: {"model": List[Message]}}
+ )
+ def post_router(item1: Item, item2: Item):
+ return item1, item2 # pragma: nocover
+
+ @sub_router.post(
+ "/subrouter",
+ response_model=List[Item],
+ responses={404: {"model": List[Message]}},
+ )
+ def post_subrouter(item1: Item, item2: Item):
+ return item1, item2 # pragma: nocover
+
+ router.include_router(sub_router)
+ app.include_router(router, generate_unique_id_function=custom_generate_unique_id3)
+ client = TestClient(app)
+ response = client.get("/openapi.json")
+ data = response.json()
+ assert data == {
+ "openapi": "3.0.2",
+ "info": {"title": "FastAPI", "version": "0.1.0"},
+ "paths": {
+ "/": {
+ "post": {
+ "summary": "Post Root",
+ "operationId": "foo_post_root",
+ "requestBody": {
+ "content": {
+ "application/json": {
+ "schema": {
+ "$ref": "#/components/schemas/Body_foo_post_root"
+ }
+ }
+ },
+ "required": True,
+ },
+ "responses": {
+ "200": {
+ "description": "Successful Response",
+ "content": {
+ "application/json": {
+ "schema": {
+ "title": "Response Foo Post Root",
+ "type": "array",
+ "items": {"$ref": "#/components/schemas/Item"},
+ }
+ }
+ },
+ },
+ "404": {
+ "description": "Not Found",
+ "content": {
+ "application/json": {
+ "schema": {
+ "title": "Response 404 Foo Post Root",
+ "type": "array",
+ "items": {
+ "$ref": "#/components/schemas/Message"
+ },
+ }
+ }
+ },
+ },
+ "422": {
+ "description": "Validation Error",
+ "content": {
+ "application/json": {
+ "schema": {
+ "$ref": "#/components/schemas/HTTPValidationError"
+ }
+ }
+ },
+ },
+ },
+ }
+ },
+ "/router": {
+ "post": {
+ "summary": "Post Router",
+ "operationId": "baz_post_router",
+ "requestBody": {
+ "content": {
+ "application/json": {
+ "schema": {
+ "$ref": "#/components/schemas/Body_baz_post_router"
+ }
+ }
+ },
+ "required": True,
+ },
+ "responses": {
+ "200": {
+ "description": "Successful Response",
+ "content": {
+ "application/json": {
+ "schema": {
+ "title": "Response Baz Post Router",
+ "type": "array",
+ "items": {"$ref": "#/components/schemas/Item"},
+ }
+ }
+ },
+ },
+ "404": {
+ "description": "Not Found",
+ "content": {
+ "application/json": {
+ "schema": {
+ "title": "Response 404 Baz Post Router",
+ "type": "array",
+ "items": {
+ "$ref": "#/components/schemas/Message"
+ },
+ }
+ }
+ },
+ },
+ "422": {
+ "description": "Validation Error",
+ "content": {
+ "application/json": {
+ "schema": {
+ "$ref": "#/components/schemas/HTTPValidationError"
+ }
+ }
+ },
+ },
+ },
+ }
+ },
+ "/subrouter": {
+ "post": {
+ "summary": "Post Subrouter",
+ "operationId": "bar_post_subrouter",
+ "requestBody": {
+ "content": {
+ "application/json": {
+ "schema": {
+ "$ref": "#/components/schemas/Body_bar_post_subrouter"
+ }
+ }
+ },
+ "required": True,
+ },
+ "responses": {
+ "200": {
+ "description": "Successful Response",
+ "content": {
+ "application/json": {
+ "schema": {
+ "title": "Response Bar Post Subrouter",
+ "type": "array",
+ "items": {"$ref": "#/components/schemas/Item"},
+ }
+ }
+ },
+ },
+ "404": {
+ "description": "Not Found",
+ "content": {
+ "application/json": {
+ "schema": {
+ "title": "Response 404 Bar Post Subrouter",
+ "type": "array",
+ "items": {
+ "$ref": "#/components/schemas/Message"
+ },
+ }
+ }
+ },
+ },
+ "422": {
+ "description": "Validation Error",
+ "content": {
+ "application/json": {
+ "schema": {
+ "$ref": "#/components/schemas/HTTPValidationError"
+ }
+ }
+ },
+ },
+ },
+ }
+ },
+ },
+ "components": {
+ "schemas": {
+ "Body_bar_post_subrouter": {
+ "title": "Body_bar_post_subrouter",
+ "required": ["item1", "item2"],
+ "type": "object",
+ "properties": {
+ "item1": {"$ref": "#/components/schemas/Item"},
+ "item2": {"$ref": "#/components/schemas/Item"},
+ },
+ },
+ "Body_baz_post_router": {
+ "title": "Body_baz_post_router",
+ "required": ["item1", "item2"],
+ "type": "object",
+ "properties": {
+ "item1": {"$ref": "#/components/schemas/Item"},
+ "item2": {"$ref": "#/components/schemas/Item"},
+ },
+ },
+ "Body_foo_post_root": {
+ "title": "Body_foo_post_root",
+ "required": ["item1", "item2"],
+ "type": "object",
+ "properties": {
+ "item1": {"$ref": "#/components/schemas/Item"},
+ "item2": {"$ref": "#/components/schemas/Item"},
+ },
+ },
+ "HTTPValidationError": {
+ "title": "HTTPValidationError",
+ "type": "object",
+ "properties": {
+ "detail": {
+ "title": "Detail",
+ "type": "array",
+ "items": {"$ref": "#/components/schemas/ValidationError"},
+ }
+ },
+ },
+ "Item": {
+ "title": "Item",
+ "required": ["name", "price"],
+ "type": "object",
+ "properties": {
+ "name": {"title": "Name", "type": "string"},
+ "price": {"title": "Price", "type": "number"},
+ },
+ },
+ "Message": {
+ "title": "Message",
+ "required": ["title", "description"],
+ "type": "object",
+ "properties": {
+ "title": {"title": "Title", "type": "string"},
+ "description": {"title": "Description", "type": "string"},
+ },
+ },
+ "ValidationError": {
+ "title": "ValidationError",
+ "required": ["loc", "msg", "type"],
+ "type": "object",
+ "properties": {
+ "loc": {
+ "title": "Location",
+ "type": "array",
+ "items": {"type": "string"},
+ },
+ "msg": {"title": "Message", "type": "string"},
+ "type": {"title": "Error Type", "type": "string"},
+ },
+ },
+ }
+ },
+ }
+
+
+def test_router_path_operation_overrides_generate_unique_id():
+ app = FastAPI(generate_unique_id_function=custom_generate_unique_id)
+ router = APIRouter(generate_unique_id_function=custom_generate_unique_id2)
+
+ @app.post("/", response_model=List[Item], responses={404: {"model": List[Message]}})
+ def post_root(item1: Item, item2: Item):
+ return item1, item2 # pragma: nocover
+
+ @router.post(
+ "/router",
+ response_model=List[Item],
+ responses={404: {"model": List[Message]}},
+ generate_unique_id_function=custom_generate_unique_id3,
+ )
+ def post_router(item1: Item, item2: Item):
+ return item1, item2 # pragma: nocover
+
+ app.include_router(router)
+ client = TestClient(app)
+ response = client.get("/openapi.json")
+ data = response.json()
+ assert data == {
+ "openapi": "3.0.2",
+ "info": {"title": "FastAPI", "version": "0.1.0"},
+ "paths": {
+ "/": {
+ "post": {
+ "summary": "Post Root",
+ "operationId": "foo_post_root",
+ "requestBody": {
+ "content": {
+ "application/json": {
+ "schema": {
+ "$ref": "#/components/schemas/Body_foo_post_root"
+ }
+ }
+ },
+ "required": True,
+ },
+ "responses": {
+ "200": {
+ "description": "Successful Response",
+ "content": {
+ "application/json": {
+ "schema": {
+ "title": "Response Foo Post Root",
+ "type": "array",
+ "items": {"$ref": "#/components/schemas/Item"},
+ }
+ }
+ },
+ },
+ "404": {
+ "description": "Not Found",
+ "content": {
+ "application/json": {
+ "schema": {
+ "title": "Response 404 Foo Post Root",
+ "type": "array",
+ "items": {
+ "$ref": "#/components/schemas/Message"
+ },
+ }
+ }
+ },
+ },
+ "422": {
+ "description": "Validation Error",
+ "content": {
+ "application/json": {
+ "schema": {
+ "$ref": "#/components/schemas/HTTPValidationError"
+ }
+ }
+ },
+ },
+ },
+ }
+ },
+ "/router": {
+ "post": {
+ "summary": "Post Router",
+ "operationId": "baz_post_router",
+ "requestBody": {
+ "content": {
+ "application/json": {
+ "schema": {
+ "$ref": "#/components/schemas/Body_baz_post_router"
+ }
+ }
+ },
+ "required": True,
+ },
+ "responses": {
+ "200": {
+ "description": "Successful Response",
+ "content": {
+ "application/json": {
+ "schema": {
+ "title": "Response Baz Post Router",
+ "type": "array",
+ "items": {"$ref": "#/components/schemas/Item"},
+ }
+ }
+ },
+ },
+ "404": {
+ "description": "Not Found",
+ "content": {
+ "application/json": {
+ "schema": {
+ "title": "Response 404 Baz Post Router",
+ "type": "array",
+ "items": {
+ "$ref": "#/components/schemas/Message"
+ },
+ }
+ }
+ },
+ },
+ "422": {
+ "description": "Validation Error",
+ "content": {
+ "application/json": {
+ "schema": {
+ "$ref": "#/components/schemas/HTTPValidationError"
+ }
+ }
+ },
+ },
+ },
+ }
+ },
+ },
+ "components": {
+ "schemas": {
+ "Body_baz_post_router": {
+ "title": "Body_baz_post_router",
+ "required": ["item1", "item2"],
+ "type": "object",
+ "properties": {
+ "item1": {"$ref": "#/components/schemas/Item"},
+ "item2": {"$ref": "#/components/schemas/Item"},
+ },
+ },
+ "Body_foo_post_root": {
+ "title": "Body_foo_post_root",
+ "required": ["item1", "item2"],
+ "type": "object",
+ "properties": {
+ "item1": {"$ref": "#/components/schemas/Item"},
+ "item2": {"$ref": "#/components/schemas/Item"},
+ },
+ },
+ "HTTPValidationError": {
+ "title": "HTTPValidationError",
+ "type": "object",
+ "properties": {
+ "detail": {
+ "title": "Detail",
+ "type": "array",
+ "items": {"$ref": "#/components/schemas/ValidationError"},
+ }
+ },
+ },
+ "Item": {
+ "title": "Item",
+ "required": ["name", "price"],
+ "type": "object",
+ "properties": {
+ "name": {"title": "Name", "type": "string"},
+ "price": {"title": "Price", "type": "number"},
+ },
+ },
+ "Message": {
+ "title": "Message",
+ "required": ["title", "description"],
+ "type": "object",
+ "properties": {
+ "title": {"title": "Title", "type": "string"},
+ "description": {"title": "Description", "type": "string"},
+ },
+ },
+ "ValidationError": {
+ "title": "ValidationError",
+ "required": ["loc", "msg", "type"],
+ "type": "object",
+ "properties": {
+ "loc": {
+ "title": "Location",
+ "type": "array",
+ "items": {"type": "string"},
+ },
+ "msg": {"title": "Message", "type": "string"},
+ "type": {"title": "Error Type", "type": "string"},
+ },
+ },
+ }
+ },
+ }
+
+
+def test_app_path_operation_overrides_generate_unique_id():
+ app = FastAPI(generate_unique_id_function=custom_generate_unique_id)
+ router = APIRouter(generate_unique_id_function=custom_generate_unique_id2)
+
+ @app.post(
+ "/",
+ response_model=List[Item],
+ responses={404: {"model": List[Message]}},
+ generate_unique_id_function=custom_generate_unique_id3,
+ )
+ def post_root(item1: Item, item2: Item):
+ return item1, item2 # pragma: nocover
+
+ @router.post(
+ "/router",
+ response_model=List[Item],
+ responses={404: {"model": List[Message]}},
+ )
+ def post_router(item1: Item, item2: Item):
+ return item1, item2 # pragma: nocover
+
+ app.include_router(router)
+ client = TestClient(app)
+ response = client.get("/openapi.json")
+ data = response.json()
+ assert data == {
+ "openapi": "3.0.2",
+ "info": {"title": "FastAPI", "version": "0.1.0"},
+ "paths": {
+ "/": {
+ "post": {
+ "summary": "Post Root",
+ "operationId": "baz_post_root",
+ "requestBody": {
+ "content": {
+ "application/json": {
+ "schema": {
+ "$ref": "#/components/schemas/Body_baz_post_root"
+ }
+ }
+ },
+ "required": True,
+ },
+ "responses": {
+ "200": {
+ "description": "Successful Response",
+ "content": {
+ "application/json": {
+ "schema": {
+ "title": "Response Baz Post Root",
+ "type": "array",
+ "items": {"$ref": "#/components/schemas/Item"},
+ }
+ }
+ },
+ },
+ "404": {
+ "description": "Not Found",
+ "content": {
+ "application/json": {
+ "schema": {
+ "title": "Response 404 Baz Post Root",
+ "type": "array",
+ "items": {
+ "$ref": "#/components/schemas/Message"
+ },
+ }
+ }
+ },
+ },
+ "422": {
+ "description": "Validation Error",
+ "content": {
+ "application/json": {
+ "schema": {
+ "$ref": "#/components/schemas/HTTPValidationError"
+ }
+ }
+ },
+ },
+ },
+ }
+ },
+ "/router": {
+ "post": {
+ "summary": "Post Router",
+ "operationId": "bar_post_router",
+ "requestBody": {
+ "content": {
+ "application/json": {
+ "schema": {
+ "$ref": "#/components/schemas/Body_bar_post_router"
+ }
+ }
+ },
+ "required": True,
+ },
+ "responses": {
+ "200": {
+ "description": "Successful Response",
+ "content": {
+ "application/json": {
+ "schema": {
+ "title": "Response Bar Post Router",
+ "type": "array",
+ "items": {"$ref": "#/components/schemas/Item"},
+ }
+ }
+ },
+ },
+ "404": {
+ "description": "Not Found",
+ "content": {
+ "application/json": {
+ "schema": {
+ "title": "Response 404 Bar Post Router",
+ "type": "array",
+ "items": {
+ "$ref": "#/components/schemas/Message"
+ },
+ }
+ }
+ },
+ },
+ "422": {
+ "description": "Validation Error",
+ "content": {
+ "application/json": {
+ "schema": {
+ "$ref": "#/components/schemas/HTTPValidationError"
+ }
+ }
+ },
+ },
+ },
+ }
+ },
+ },
+ "components": {
+ "schemas": {
+ "Body_bar_post_router": {
+ "title": "Body_bar_post_router",
+ "required": ["item1", "item2"],
+ "type": "object",
+ "properties": {
+ "item1": {"$ref": "#/components/schemas/Item"},
+ "item2": {"$ref": "#/components/schemas/Item"},
+ },
+ },
+ "Body_baz_post_root": {
+ "title": "Body_baz_post_root",
+ "required": ["item1", "item2"],
+ "type": "object",
+ "properties": {
+ "item1": {"$ref": "#/components/schemas/Item"},
+ "item2": {"$ref": "#/components/schemas/Item"},
+ },
+ },
+ "HTTPValidationError": {
+ "title": "HTTPValidationError",
+ "type": "object",
+ "properties": {
+ "detail": {
+ "title": "Detail",
+ "type": "array",
+ "items": {"$ref": "#/components/schemas/ValidationError"},
+ }
+ },
+ },
+ "Item": {
+ "title": "Item",
+ "required": ["name", "price"],
+ "type": "object",
+ "properties": {
+ "name": {"title": "Name", "type": "string"},
+ "price": {"title": "Price", "type": "number"},
+ },
+ },
+ "Message": {
+ "title": "Message",
+ "required": ["title", "description"],
+ "type": "object",
+ "properties": {
+ "title": {"title": "Title", "type": "string"},
+ "description": {"title": "Description", "type": "string"},
+ },
+ },
+ "ValidationError": {
+ "title": "ValidationError",
+ "required": ["loc", "msg", "type"],
+ "type": "object",
+ "properties": {
+ "loc": {
+ "title": "Location",
+ "type": "array",
+ "items": {"type": "string"},
+ },
+ "msg": {"title": "Message", "type": "string"},
+ "type": {"title": "Error Type", "type": "string"},
+ },
+ },
+ }
+ },
+ }
+
+
+def test_callback_override_generate_unique_id():
+ app = FastAPI(generate_unique_id_function=custom_generate_unique_id)
+ callback_router = APIRouter(generate_unique_id_function=custom_generate_unique_id2)
+
+ @callback_router.post(
+ "/post-callback",
+ response_model=List[Item],
+ responses={404: {"model": List[Message]}},
+ generate_unique_id_function=custom_generate_unique_id3,
+ )
+ def post_callback(item1: Item, item2: Item):
+ return item1, item2 # pragma: nocover
+
+ @app.post(
+ "/",
+ response_model=List[Item],
+ responses={404: {"model": List[Message]}},
+ generate_unique_id_function=custom_generate_unique_id3,
+ callbacks=callback_router.routes,
+ )
+ def post_root(item1: Item, item2: Item):
+ return item1, item2 # pragma: nocover
+
+ @app.post(
+ "/tocallback",
+ response_model=List[Item],
+ responses={404: {"model": List[Message]}},
+ )
+ def post_with_callback(item1: Item, item2: Item):
+ return item1, item2 # pragma: nocover
+
+ client = TestClient(app)
+ response = client.get("/openapi.json")
+ data = response.json()
+ assert data == {
+ "openapi": "3.0.2",
+ "info": {"title": "FastAPI", "version": "0.1.0"},
+ "paths": {
+ "/": {
+ "post": {
+ "summary": "Post Root",
+ "operationId": "baz_post_root",
+ "requestBody": {
+ "content": {
+ "application/json": {
+ "schema": {
+ "$ref": "#/components/schemas/Body_baz_post_root"
+ }
+ }
+ },
+ "required": True,
+ },
+ "responses": {
+ "200": {
+ "description": "Successful Response",
+ "content": {
+ "application/json": {
+ "schema": {
+ "title": "Response Baz Post Root",
+ "type": "array",
+ "items": {"$ref": "#/components/schemas/Item"},
+ }
+ }
+ },
+ },
+ "404": {
+ "description": "Not Found",
+ "content": {
+ "application/json": {
+ "schema": {
+ "title": "Response 404 Baz Post Root",
+ "type": "array",
+ "items": {
+ "$ref": "#/components/schemas/Message"
+ },
+ }
+ }
+ },
+ },
+ "422": {
+ "description": "Validation Error",
+ "content": {
+ "application/json": {
+ "schema": {
+ "$ref": "#/components/schemas/HTTPValidationError"
+ }
+ }
+ },
+ },
+ },
+ "callbacks": {
+ "post_callback": {
+ "/post-callback": {
+ "post": {
+ "summary": "Post Callback",
+ "operationId": "baz_post_callback",
+ "requestBody": {
+ "content": {
+ "application/json": {
+ "schema": {
+ "$ref": "#/components/schemas/Body_baz_post_callback"
+ }
+ }
+ },
+ "required": True,
+ },
+ "responses": {
+ "200": {
+ "description": "Successful Response",
+ "content": {
+ "application/json": {
+ "schema": {
+ "title": "Response Baz Post Callback",
+ "type": "array",
+ "items": {
+ "$ref": "#/components/schemas/Item"
+ },
+ }
+ }
+ },
+ },
+ "404": {
+ "description": "Not Found",
+ "content": {
+ "application/json": {
+ "schema": {
+ "title": "Response 404 Baz Post Callback",
+ "type": "array",
+ "items": {
+ "$ref": "#/components/schemas/Message"
+ },
+ }
+ }
+ },
+ },
+ "422": {
+ "description": "Validation Error",
+ "content": {
+ "application/json": {
+ "schema": {
+ "$ref": "#/components/schemas/HTTPValidationError"
+ }
+ }
+ },
+ },
+ },
+ }
+ }
+ }
+ },
+ }
+ },
+ "/tocallback": {
+ "post": {
+ "summary": "Post With Callback",
+ "operationId": "foo_post_with_callback",
+ "requestBody": {
+ "content": {
+ "application/json": {
+ "schema": {
+ "$ref": "#/components/schemas/Body_foo_post_with_callback"
+ }
+ }
+ },
+ "required": True,
+ },
+ "responses": {
+ "200": {
+ "description": "Successful Response",
+ "content": {
+ "application/json": {
+ "schema": {
+ "title": "Response Foo Post With Callback",
+ "type": "array",
+ "items": {"$ref": "#/components/schemas/Item"},
+ }
+ }
+ },
+ },
+ "404": {
+ "description": "Not Found",
+ "content": {
+ "application/json": {
+ "schema": {
+ "title": "Response 404 Foo Post With Callback",
+ "type": "array",
+ "items": {
+ "$ref": "#/components/schemas/Message"
+ },
+ }
+ }
+ },
+ },
+ "422": {
+ "description": "Validation Error",
+ "content": {
+ "application/json": {
+ "schema": {
+ "$ref": "#/components/schemas/HTTPValidationError"
+ }
+ }
+ },
+ },
+ },
+ }
+ },
+ },
+ "components": {
+ "schemas": {
+ "Body_baz_post_callback": {
+ "title": "Body_baz_post_callback",
+ "required": ["item1", "item2"],
+ "type": "object",
+ "properties": {
+ "item1": {"$ref": "#/components/schemas/Item"},
+ "item2": {"$ref": "#/components/schemas/Item"},
+ },
+ },
+ "Body_baz_post_root": {
+ "title": "Body_baz_post_root",
+ "required": ["item1", "item2"],
+ "type": "object",
+ "properties": {
+ "item1": {"$ref": "#/components/schemas/Item"},
+ "item2": {"$ref": "#/components/schemas/Item"},
+ },
+ },
+ "Body_foo_post_with_callback": {
+ "title": "Body_foo_post_with_callback",
+ "required": ["item1", "item2"],
+ "type": "object",
+ "properties": {
+ "item1": {"$ref": "#/components/schemas/Item"},
+ "item2": {"$ref": "#/components/schemas/Item"},
+ },
+ },
+ "HTTPValidationError": {
+ "title": "HTTPValidationError",
+ "type": "object",
+ "properties": {
+ "detail": {
+ "title": "Detail",
+ "type": "array",
+ "items": {"$ref": "#/components/schemas/ValidationError"},
+ }
+ },
+ },
+ "Item": {
+ "title": "Item",
+ "required": ["name", "price"],
+ "type": "object",
+ "properties": {
+ "name": {"title": "Name", "type": "string"},
+ "price": {"title": "Price", "type": "number"},
+ },
+ },
+ "Message": {
+ "title": "Message",
+ "required": ["title", "description"],
+ "type": "object",
+ "properties": {
+ "title": {"title": "Title", "type": "string"},
+ "description": {"title": "Description", "type": "string"},
+ },
+ },
+ "ValidationError": {
+ "title": "ValidationError",
+ "required": ["loc", "msg", "type"],
+ "type": "object",
+ "properties": {
+ "loc": {
+ "title": "Location",
+ "type": "array",
+ "items": {"type": "string"},
+ },
+ "msg": {"title": "Message", "type": "string"},
+ "type": {"title": "Error Type", "type": "string"},
+ },
+ },
+ }
+ },
+ }
+
+
+def test_warn_duplicate_operation_id():
+ def broken_operation_id(route: APIRoute):
+ return "foo"
+
+ app = FastAPI(generate_unique_id_function=broken_operation_id)
+
+ @app.post("/")
+ def post_root(item1: Item):
+ return item1 # pragma: nocover
+
+ @app.post("/second")
+ def post_second(item1: Item):
+ return item1 # pragma: nocover
+
+ @app.post("/third")
+ def post_third(item1: Item):
+ return item1 # pragma: nocover
+
+ client = TestClient(app)
+ with warnings.catch_warnings(record=True) as w:
+ warnings.simplefilter("always")
+ client.get("/openapi.json")
+ assert len(w) == 2
+ assert issubclass(w[-1].category, UserWarning)
+ assert "Duplicate Operation ID" in str(w[-1].message)
+import warnings
+
import pytest
from fastapi import APIRouter, Depends, FastAPI, Response
from fastapi.responses import JSONResponse
def test_openapi():
client = TestClient(app)
- response = client.get("/openapi.json")
+ with warnings.catch_warnings(record=True) as w:
+ warnings.simplefilter("always")
+ response = client.get("/openapi.json")
+ assert issubclass(w[-1].category, UserWarning)
+ assert "Duplicate Operation ID" in str(w[-1].message)
assert response.json() == openapi_schema
--- /dev/null
+from fastapi.testclient import TestClient
+
+from docs_src.generate_clients.tutorial003 import app
+
+client = TestClient(app)
+
+openapi_schema = {
+ "openapi": "3.0.2",
+ "info": {"title": "FastAPI", "version": "0.1.0"},
+ "paths": {
+ "/items/": {
+ "get": {
+ "tags": ["items"],
+ "summary": "Get Items",
+ "operationId": "items-get_items",
+ "responses": {
+ "200": {
+ "description": "Successful Response",
+ "content": {
+ "application/json": {
+ "schema": {
+ "title": "Response Items-Get Items",
+ "type": "array",
+ "items": {"$ref": "#/components/schemas/Item"},
+ }
+ }
+ },
+ }
+ },
+ },
+ "post": {
+ "tags": ["items"],
+ "summary": "Create Item",
+ "operationId": "items-create_item",
+ "requestBody": {
+ "content": {
+ "application/json": {
+ "schema": {"$ref": "#/components/schemas/Item"}
+ }
+ },
+ "required": True,
+ },
+ "responses": {
+ "200": {
+ "description": "Successful Response",
+ "content": {
+ "application/json": {
+ "schema": {
+ "$ref": "#/components/schemas/ResponseMessage"
+ }
+ }
+ },
+ },
+ "422": {
+ "description": "Validation Error",
+ "content": {
+ "application/json": {
+ "schema": {
+ "$ref": "#/components/schemas/HTTPValidationError"
+ }
+ }
+ },
+ },
+ },
+ },
+ },
+ "/users/": {
+ "post": {
+ "tags": ["users"],
+ "summary": "Create User",
+ "operationId": "users-create_user",
+ "requestBody": {
+ "content": {
+ "application/json": {
+ "schema": {"$ref": "#/components/schemas/User"}
+ }
+ },
+ "required": True,
+ },
+ "responses": {
+ "200": {
+ "description": "Successful Response",
+ "content": {
+ "application/json": {
+ "schema": {
+ "$ref": "#/components/schemas/ResponseMessage"
+ }
+ }
+ },
+ },
+ "422": {
+ "description": "Validation Error",
+ "content": {
+ "application/json": {
+ "schema": {
+ "$ref": "#/components/schemas/HTTPValidationError"
+ }
+ }
+ },
+ },
+ },
+ }
+ },
+ },
+ "components": {
+ "schemas": {
+ "HTTPValidationError": {
+ "title": "HTTPValidationError",
+ "type": "object",
+ "properties": {
+ "detail": {
+ "title": "Detail",
+ "type": "array",
+ "items": {"$ref": "#/components/schemas/ValidationError"},
+ }
+ },
+ },
+ "Item": {
+ "title": "Item",
+ "required": ["name", "price"],
+ "type": "object",
+ "properties": {
+ "name": {"title": "Name", "type": "string"},
+ "price": {"title": "Price", "type": "number"},
+ },
+ },
+ "ResponseMessage": {
+ "title": "ResponseMessage",
+ "required": ["message"],
+ "type": "object",
+ "properties": {"message": {"title": "Message", "type": "string"}},
+ },
+ "User": {
+ "title": "User",
+ "required": ["username", "email"],
+ "type": "object",
+ "properties": {
+ "username": {"title": "Username", "type": "string"},
+ "email": {"title": "Email", "type": "string"},
+ },
+ },
+ "ValidationError": {
+ "title": "ValidationError",
+ "required": ["loc", "msg", "type"],
+ "type": "object",
+ "properties": {
+ "loc": {
+ "title": "Location",
+ "type": "array",
+ "items": {"type": "string"},
+ },
+ "msg": {"title": "Message", "type": "string"},
+ "type": {"title": "Error Type", "type": "string"},
+ },
+ },
+ }
+ },
+}
+
+
+def test_openapi():
+ with client:
+ response = client.get("/openapi.json")
+
+ assert response.json() == openapi_schema
+
+
+def test_post_items():
+ response = client.post("/items/", json={"name": "Foo", "price": 5})
+ assert response.status_code == 200, response.text
+ assert response.json() == {"message": "Item received"}
+
+
+def test_post_users():
+ response = client.post(
+ "/users/", json={"username": "Foo", "email": "foo@example.com"}
+ )
+ assert response.status_code == 200, response.text
+ assert response.json() == {"message": "User received"}
+
+
+def test_get_items():
+ response = client.get("/items/")
+ assert response.status_code == 200, response.text
+ assert response.json() == [
+ {"name": "Plumbus", "price": 3},
+ {"name": "Portal Gun", "price": 9001},
+ ]