diff --git a/README.md b/README.md
index 6fbd5e2..0a43dc7 100644
--- a/README.md
+++ b/README.md
@@ -183,6 +183,14 @@ Click **Try it out** to execute the request directly from the browser and see th
+Swagger UI also ships with a dark theme (toggle in the top-right corner):
+
+
+
+Prefer a clean, read-only view? FastHTTP also serves [ReDoc](https://github.com/Redocly/redoc) at `/redoc`:
+
+
+
### Upgrade the example
Now modify `main.py` to get more out of FastHTTP. Each upgrade below builds on the previous one.
diff --git a/docs/en/openapi.md b/docs/en/openapi.md
index 3a43b87..bc48c5f 100644
--- a/docs/en/openapi.md
+++ b/docs/en/openapi.md
@@ -109,6 +109,7 @@ app.web_run(base_url="/api")
This serves the documentation endpoints at:
- `http://127.0.0.1:8000/api/docs`
+- `http://127.0.0.1:8000/api/redoc`
- `http://127.0.0.1:8000/api/openapi.json`
- `http://127.0.0.1:8000/api/request`
@@ -134,6 +135,18 @@ async def get_data(resp: Response) -> dict:

+### Dark Mode
+
+Swagger UI includes a dark theme toggle in the top-right corner:
+
+
+
+### ReDoc
+
+A clean, read-only alternative view is served at `/redoc`:
+
+
+
### 404 Page

diff --git a/docs/en/tutorial/openapi/swagger-ui.md b/docs/en/tutorial/openapi/swagger-ui.md
index 22a4cb1..a5fd139 100644
--- a/docs/en/tutorial/openapi/swagger-ui.md
+++ b/docs/en/tutorial/openapi/swagger-ui.md
@@ -24,13 +24,15 @@ app.web_run()
After running, open in browser:
- **Swagger UI**: `http://127.0.0.1:8000/docs`
+- **ReDoc**: `http://127.0.0.1:8000/redoc`
- **OpenAPI Schema**: `http://127.0.0.1:8000/openapi.json`
## Available Endpoints
| Endpoint | Description |
|----------|-------------|
-| `/docs` | Swagger UI interface |
+| `/docs` | Swagger UI interface (dark mode toggle in the top-right corner) |
+| `/redoc` | ReDoc — clean, read-only alternative view |
| `/openapi.json` | OpenAPI schema in JSON |
| `/request` | Proxy for executing requests |
diff --git a/docs/photo/redoc.png b/docs/photo/redoc.png
new file mode 100644
index 0000000..2357fe0
Binary files /dev/null and b/docs/photo/redoc.png differ
diff --git a/docs/photo/swagger_dark.png b/docs/photo/swagger_dark.png
new file mode 100644
index 0000000..5e2bf33
Binary files /dev/null and b/docs/photo/swagger_dark.png differ
diff --git a/docs/ru/openapi.md b/docs/ru/openapi.md
index 87d0c25..34d785b 100644
--- a/docs/ru/openapi.md
+++ b/docs/ru/openapi.md
@@ -109,6 +109,7 @@ app.web_run(base_url="/api")
В этом случае endpoints документации будут доступны по адресам:
- `http://127.0.0.1:8000/api/docs`
+- `http://127.0.0.1:8000/api/redoc`
- `http://127.0.0.1:8000/api/openapi.json`
- `http://127.0.0.1:8000/api/request`
@@ -134,6 +135,18 @@ async def get_data(resp: Response) -> dict:

+### Тёмная тема
+
+В Swagger UI есть переключатель тёмной темы в правом верхнем углу:
+
+
+
+### ReDoc
+
+Альтернативный read-only просмотр схемы доступен по адресу `/redoc`:
+
+
+
### Страница 404

diff --git a/docs/ru/tutorial/openapi/swagger-ui.md b/docs/ru/tutorial/openapi/swagger-ui.md
index c69116b..fdf31e4 100644
--- a/docs/ru/tutorial/openapi/swagger-ui.md
+++ b/docs/ru/tutorial/openapi/swagger-ui.md
@@ -24,15 +24,17 @@ app.web_run()
После запуска откройте в браузере:
- **Swagger UI**: `http://127.0.0.1:8000/docs`
+- **ReDoc**: `http://127.0.0.1:8000/redoc`
- **OpenAPI схема**: `http://127.0.0.1:8000/openapi.json`
## Доступные endpoints
-| Endpoint | Описание |
-| --------------- | ------------------------------ |
-| `/docs` | Интерфейс Swagger UI |
-| `/openapi.json` | OpenAPI схема в JSON |
-| `/request` | Прокси для выполнения запросов |
+| Endpoint | Описание |
+| --------------- | ---------------------------------------------- |
+| `/docs` | Интерфейс Swagger UI (тёмная тема — переключатель справа сверху) |
+| `/redoc` | ReDoc — альтернативный read-only просмотр |
+| `/openapi.json` | OpenAPI схема в JSON |
+| `/request` | Прокси для выполнения запросов |
## Использование Swagger UI
diff --git a/examples/openapi/pydantic_schema_example.py b/examples/openapi/pydantic_schema_example.py
new file mode 100644
index 0000000..3363ed8
--- /dev/null
+++ b/examples/openapi/pydantic_schema_example.py
@@ -0,0 +1,84 @@
+"""
+Demonstrates OpenAPI schema generation built entirely on Pydantic:
+enums, nested models, Field descriptions/examples, and error models
+are all picked up automatically via `model_json_schema()` — nothing
+is hand-mapped.
+
+Run it, then open:
+- http://127.0.0.1:8010/docs (Swagger UI — filter box, dark mode toggle,
+ persisted auth, request snippets)
+- http://127.0.0.1:8010/redoc (ReDoc — cleaner read-only view)
+- http://127.0.0.1:8010/openapi.json
+"""
+
+from enum import Enum
+
+from pydantic import BaseModel, Field
+
+from fasthttp import FastHTTP
+from fasthttp.response import Response
+
+
+class Role(str, Enum):
+ """Enum fields resolve to a shared `$ref` + `enum` list, not a plain string."""
+
+ admin = "admin"
+ editor = "editor"
+ viewer = "viewer"
+
+
+class Address(BaseModel):
+ """Nested models are discovered automatically and added to components/schemas."""
+
+ city: str = Field(description="City name", examples=["Berlin"])
+ zip_code: str = Field(description="Postal code", examples=["10115"])
+
+
+class CreateUserRequest(BaseModel):
+ name: str = Field(description="Full name", examples=["Ada Lovelace"])
+ role: Role = Role.viewer
+ address: Address
+
+
+class UserResponse(BaseModel):
+ id: int
+ name: str
+ role: Role
+ address: Address
+
+
+class ErrorResponse(BaseModel):
+ detail: str = Field(description="Human-readable error message")
+
+
+app = FastHTTP(
+ title="Pydantic-native OpenAPI demo",
+ version="1.0.0",
+)
+
+
+@app.post(
+ url="https://jsonplaceholder.typicode.com/users",
+ request_model=CreateUserRequest,
+ response_model=UserResponse,
+ responses={404: {"model": ErrorResponse}},
+ tags=["users"],
+)
+async def create_user(resp: Response) -> UserResponse:
+ """Create a user (nested model + enum in both request and response)."""
+ return resp.json()
+
+
+@app.get(
+ url="https://jsonplaceholder.typicode.com/users",
+ response_model=list[UserResponse],
+ params={"_limit": 5},
+ tags=["users"],
+)
+async def list_users(resp: Response) -> list[UserResponse]:
+ """List users — `_limit` query param is pre-filled as an example in Swagger UI."""
+ return resp.json()
+
+
+if __name__ == "__main__":
+ app.web_run(port=8010)
diff --git a/fasthttp/app.py b/fasthttp/app.py
index eb3bc44..215b2bc 100644
--- a/fasthttp/app.py
+++ b/fasthttp/app.py
@@ -41,7 +41,7 @@
SessionMiddleware,
)
from .openapi.generator import generate_openapi_schema
-from .openapi.swagger import get_not_found_html, get_swagger_html
+from .openapi.swagger import get_not_found_html, get_redoc_html, get_swagger_html
from .openapi.urls import build_docs_urls
from .routing import Route, Router
from .security import Security
@@ -1887,8 +1887,14 @@ async def handle_request(
get_swagger_html(
openapi_url=self.docs_urls["openapi_url"],
request_url=self.docs_urls["request_url"],
+ redoc_url=self.docs_urls["redoc_url"],
),
)
+ elif path == self.docs_urls["redoc_url"]:
+ await self._send_html(
+ send,
+ get_redoc_html(openapi_url=self.docs_urls["openapi_url"]),
+ )
elif path == self.docs_urls["openapi_url"]:
if self._openapi_cache is None:
schema = generate_openapi_schema(
@@ -1942,6 +1948,7 @@ async def _send_404(self, send: Callable[..., Any], _path: str = "/") -> None:
html = get_not_found_html(
docs_url=self.docs_urls["docs_url"],
openapi_url=self.docs_urls["openapi_url"],
+ redoc_url=self.docs_urls["redoc_url"],
)
await send(
{
@@ -2015,15 +2022,20 @@ async def _handle_proxy( # noqa: C901
else:
kwargs["content"] = str(req_body)
+ started_at = time.perf_counter()
if self._shared_client is not None:
response = await self._shared_client.request(**kwargs)
else:
async with httpx.AsyncClient(proxy=self.fasthttp.proxy) as tmp:
response = await tmp.request(**kwargs)
+ duration_ms = round((time.perf_counter() - started_at) * 1000, 2)
result: dict[str, Any] = {
"status": response.status_code,
- "headers": dict(response.headers),
+ "headers": {
+ **dict(response.headers),
+ "x-fasthttp-duration-ms": str(duration_ms),
+ },
"body": response.text,
}
diff --git a/fasthttp/openapi/generator.py b/fasthttp/openapi/generator.py
index 16f089a..6a2ab74 100644
--- a/fasthttp/openapi/generator.py
+++ b/fasthttp/openapi/generator.py
@@ -2,9 +2,12 @@
import inspect
from typing import TYPE_CHECKING, Annotated, Any, get_args, get_origin
+from urllib.parse import urlparse
from annotated_doc import Doc
-from pydantic import BaseModel
+from pydantic import BaseModel, TypeAdapter
+from pydantic.fields import FieldInfo
+from pydantic.json_schema import models_json_schema
from fasthttp.auth import BasicAuth, BearerAuth, DigestAuth, OAuth2ClientCredentials
@@ -12,43 +15,8 @@
from fasthttp.app import FastHTTP
from fasthttp.routing import Route
-
-def _get_type_string(annotation: Any) -> dict[str, Any] | None: # noqa: ANN401
- """Convert Python type annotation to OpenAPI type string."""
- if annotation is None:
- return {"type": "string", "nullable": True}
-
- origin = get_origin(annotation)
- args = get_args(annotation)
-
- if origin is Annotated:
- return _get_type_string(args[0])
-
- if origin is list:
- item_type = _get_type_string(args[0]) if args else None
- if item_type:
- return {"type": "array", "items": item_type}
- return {"type": "array"}
-
- if origin is dict:
- return {"type": "object"}
-
- type_map = {
- str: "string",
- int: "integer",
- float: "number",
- bool: "boolean",
- list: "array",
- dict: "object",
- }
-
- if annotation in type_map:
- return {"type": type_map[annotation]}
-
- if isinstance(annotation, type) and issubclass(annotation, BaseModel):
- return {"$ref": f"#/components/schemas/{annotation.__name__}"}
-
- return None
+REF_TEMPLATE = "#/components/schemas/{model}"
+_EXAMPLE_TYPES = (str, int, float, bool)
def _extract_docstring(func: Any) -> str: # noqa: ANN401
@@ -61,180 +29,178 @@ def _extract_docstring(func: Any) -> str: # noqa: ANN401
return ""
-def _generate_schema_from_model(model: type[BaseModel]) -> dict[str, Any]:
- """Generate OpenAPI schema from Pydantic model."""
- schema: dict[str, Any] = {
- "type": "object",
- "properties": {},
- "required": [],
- }
+def _iter_field_models(annotation: Any) -> list[type[BaseModel]]: # noqa: ANN401
+ """Recursively find Pydantic models reachable through a type annotation."""
+ origin = get_origin(annotation)
- for name, field in model.model_fields.items():
- field_info = field.annotation
-
- description = None
- if field_info and get_origin(field_info) is Annotated:
- annotations = get_args(field_info)
- for ann in annotations:
- if isinstance(ann, Doc):
- description = ann._doc # type: ignore # noqa: SLF001
-
- type_schema = _get_type_string(field.annotation)
- if type_schema:
- prop_schema = type_schema.copy()
- if description:
- prop_schema["description"] = description
- if field.is_required():
- schema["required"].append(name)
- schema["properties"][name] = prop_schema
- else:
- schema["properties"][name] = {"type": "string"}
+ if origin is Annotated:
+ return _iter_field_models(get_args(annotation)[0])
+
+ if origin is not None:
+ models: list[type[BaseModel]] = []
+ for arg in get_args(annotation):
+ models.extend(_iter_field_models(arg))
+ return models
- if not schema["required"]:
- del schema["required"]
+ if isinstance(annotation, type) and issubclass(annotation, BaseModel):
+ return [annotation]
- return schema
+ return []
-def _generate_parameter_schema(params: dict | None) -> dict[str, Any]:
- """Generate OpenAPI parameter schema from params dict."""
- if not params:
- return {}
+def _collect_model_closure(
+ model: type[BaseModel], seen: set[type[BaseModel]]
+) -> None:
+ """Walk a model's fields to find every nested Pydantic model it references."""
+ if model in seen:
+ return
+ seen.add(model)
+ for field in model.model_fields.values():
+ for nested in _iter_field_models(field.annotation):
+ _collect_model_closure(nested, seen)
- properties = {}
- required = []
- for name, value in params.items():
- properties[name] = _get_type_string(type(value)) or {"type": "string"}
- required.append(name)
+def _doc_description(field: FieldInfo) -> str | None:
+ """Read a `Doc(...)` annotation from field metadata (annotated-doc convention)."""
+ for meta in field.metadata:
+ if isinstance(meta, Doc):
+ return meta.documentation
+ return None
- return {
- "type": "object",
- "properties": properties,
- "required": required,
- }
+class _SchemaCollector:
+ """
+ Builds `components/schemas` using Pydantic's own JSON Schema
+ generation instead of a hand-written type mapper — this gives
+ correct handling of enums, unions, constraints, defaults, and
+ nested models for free.
+ """
-def _generate_response_schema(
- response_model: type[BaseModel] | None,
-) -> dict[str, Any]:
- """Generate OpenAPI response schema from response model."""
- if not response_model:
- return {"description": "Successful response"}
+ def __init__(self, top_level_models: set[type[BaseModel]]) -> None:
+ self.schemas: dict[str, Any] = {}
+ self._refs: dict[type[BaseModel], dict[str, Any]] = {}
- origin = get_origin(response_model)
- args = get_args(response_model)
+ closure: set[type[BaseModel]] = set()
+ for model in top_level_models:
+ _collect_model_closure(model, closure)
- if origin is list and args:
- item_model = args[0]
- if isinstance(item_model, type) and issubclass(item_model, BaseModel):
+ if closure:
+ key_map, top = models_json_schema(
+ [(model, "serialization") for model in closure],
+ ref_template=REF_TEMPLATE,
+ )
+ self.schemas.update(top.get("$defs", {}))
+ for (model, _mode), ref in key_map.items():
+ self._refs[model] = ref
+ self._backfill_doc_descriptions(closure)
+
+ def _backfill_doc_descriptions(self, models: set[type[BaseModel]]) -> None:
+ """Fill in descriptions from `Doc(...)` metadata where Pydantic found none."""
+ for model in models:
+ schema = self.schemas.get(model.__name__)
+ if not schema:
+ continue
+ properties = schema.get("properties", {})
+ for name, field in model.model_fields.items():
+ prop = properties.get(name)
+ if prop is None or "description" in prop:
+ continue
+ doc = _doc_description(field)
+ if doc:
+ prop["description"] = doc
+
+ def ref_for_model(self, model: type[BaseModel]) -> dict[str, Any]:
+ """Return (and lazily register) a `$ref` for a model outside the original closure."""
+ if model not in self._refs:
+ closure: set[type[BaseModel]] = set()
+ _collect_model_closure(model, closure)
+ key_map, top = models_json_schema(
+ [(m, "serialization") for m in closure],
+ ref_template=REF_TEMPLATE,
+ )
+ self.schemas.update(top.get("$defs", {}))
+ for (m, _mode), ref in key_map.items():
+ self._refs[m] = ref
+ self._backfill_doc_descriptions(closure)
+ return self._refs[model]
+
+ def schema_for_type(self, annotation: Any) -> dict[str, Any]: # noqa: ANN401
+ """Return an OpenAPI schema for an arbitrary Python type via `TypeAdapter`."""
+ try:
+ adapter = TypeAdapter(annotation)
+ schema = adapter.json_schema(ref_template=REF_TEMPLATE)
+ except Exception: # noqa: BLE001
+ return {"type": "object"}
+ defs = schema.pop("$defs", None) or {}
+ for name, def_schema in defs.items():
+ self.schemas.setdefault(name, def_schema)
+ return schema
+
+ def schema_for_value(self, value: Any) -> dict[str, Any]: # noqa: ANN401
+ """Infer a best-effort schema from a concrete runtime value (query params, raw bodies)."""
+ if isinstance(value, BaseModel):
+ return self.ref_for_model(type(value))
+ if isinstance(value, dict):
+ return {
+ "type": "object",
+ "properties": {k: self.schema_for_value(v) for k, v in value.items()},
+ }
+ if isinstance(value, (list, tuple)):
+ if value:
+ return {"type": "array", "items": self.schema_for_value(value[0])}
+ return {"type": "array"}
+ return self.schema_for_type(type(value))
+
+ def response_schema(self, model: type | None) -> dict[str, Any]:
+ """Build a `responses.200`-style schema for a route's `response_model`."""
+ if model is None:
+ return {"description": "Successful response"}
+
+ origin = get_origin(model)
+ if origin is list:
+ args = get_args(model)
+ item = args[0] if args else None
+ if isinstance(item, type) and issubclass(item, BaseModel):
+ item_schema = self.ref_for_model(item)
+ elif item is not None:
+ item_schema = self.schema_for_type(item)
+ else:
+ item_schema = {}
return {
"description": "Successful response",
"content": {
"application/json": {
- "schema": {
- "type": "array",
- "items": {
- "$ref": f"#/components/schemas/{item_model.__name__}"
- },
- }
+ "schema": {"type": "array", "items": item_schema}
}
},
}
- return {
- "description": "Successful response",
- "content": {"application/json": {"schema": {"type": "array"}}},
- }
- try:
- if isinstance(response_model, type) and issubclass(response_model, BaseModel):
+ if isinstance(model, type) and issubclass(model, BaseModel):
return {
"description": "Successful response",
"content": {
- "application/json": {
- "schema": {
- "$ref": f"#/components/schemas/{response_model.__name__}"
- }
- }
+ "application/json": {"schema": self.ref_for_model(model)}
},
}
- except TypeError:
- pass
-
- return {"description": "Successful response"}
+ return {
+ "description": "Successful response",
+ "content": {"application/json": {"schema": self.schema_for_type(model)}},
+ }
-def _generate_error_response_schema(
- status_code: int,
- model: type[BaseModel] | None,
-) -> dict[str, Any]:
- """Generate OpenAPI error response schema."""
- if not model:
+ def error_response_schema(
+ self, status_code: int, model: type[BaseModel] | None
+ ) -> dict[str, Any]:
+ """Build an error response schema (e.g. for `responses={404: {"model": ...}}`)."""
+ if not model:
+ return {"description": f"Error response (HTTP {status_code})"}
return {
"description": f"Error response (HTTP {status_code})",
+ "content": {
+ "application/json": {"schema": self.ref_for_model(model)}
+ },
}
- return {
- "description": f"Error response (HTTP {status_code})",
- "content": {
- "application/json": {
- "schema": {"$ref": f"#/components/schemas/{model.__name__}"}
- }
- },
- }
-
-
-def _collect_schemas(routes: list[Route]) -> dict[str, Any]: # noqa: C901
- """Collect all Pydantic schemas from routes."""
- schemas: dict[str, Any] = {}
-
- for route in routes:
- if route.response_model:
- model = route.response_model
- origin = get_origin(model)
- if origin is list:
- args = get_args(model)
- if args:
- model = args[0]
-
- try:
- if (
- isinstance(model, type)
- and issubclass(model, BaseModel)
- and model.__name__ not in schemas
- ):
- schemas[model.__name__] = _generate_schema_from_model(model)
- except TypeError:
- pass
-
- if route.request_model:
- model = route.request_model
- try:
- if (
- isinstance(model, type)
- and issubclass(model, BaseModel)
- and model.__name__ not in schemas
- ):
- schemas[model.__name__] = _generate_schema_from_model(model)
- except TypeError:
- pass
-
- if route.responses:
- for _, response_config in route.responses.items():
- model = response_config.get("model")
- try:
- if (
- model
- and isinstance(model, type)
- and issubclass(model, BaseModel)
- and model.__name__ not in schemas
- ):
- schemas[model.__name__] = _generate_schema_from_model(model)
- except TypeError:
- pass
-
- return schemas
-
def _normalize_path(url: str) -> str:
"""
@@ -243,8 +209,6 @@ def _normalize_path(url: str) -> str:
Converts full URLs like https://example.com/api/users
to /example.com/api/users format.
"""
- from urllib.parse import urlparse
-
parsed = urlparse(url)
host = parsed.netloc.replace(":", "_")
path = parsed.path
@@ -275,9 +239,7 @@ def _get_security_scheme_name(
return "auth"
-def _collect_security_schemes(
- routes: list[Route],
-) -> dict[str, Any]:
+def _collect_security_schemes(routes: list[Route]) -> dict[str, Any]:
"""Build components/securitySchemes from auth objects used in routes."""
schemes: dict[str, Any] = {}
@@ -285,18 +247,27 @@ def _collect_security_schemes(
if route.auth is None:
continue
if isinstance(route.auth, BearerAuth) and "bearerAuth" not in schemes:
- schemes["bearerAuth"] = {"type": "http", "scheme": "bearer", "bearerFormat": "JWT"}
+ schemes["bearerAuth"] = {
+ "type": "http",
+ "scheme": "bearer",
+ "bearerFormat": "JWT",
+ }
elif isinstance(route.auth, BasicAuth) and "basicAuth" not in schemes:
schemes["basicAuth"] = {"type": "http", "scheme": "basic"}
elif isinstance(route.auth, DigestAuth) and "digestAuth" not in schemes:
schemes["digestAuth"] = {"type": "http", "scheme": "digest"}
- elif isinstance(route.auth, OAuth2ClientCredentials) and "oauth2ClientCredentials" not in schemes:
+ elif (
+ isinstance(route.auth, OAuth2ClientCredentials)
+ and "oauth2ClientCredentials" not in schemes
+ ):
schemes["oauth2ClientCredentials"] = {
"type": "oauth2",
"flows": {
"clientCredentials": {
"tokenUrl": route.auth.token_url,
- "scopes": {s: "" for s in route.auth.scopes} if route.auth.scopes else {},
+ "scopes": {s: "" for s in route.auth.scopes}
+ if route.auth.scopes
+ else {},
}
},
}
@@ -304,6 +275,29 @@ def _collect_security_schemes(
return schemes
+def _route_models(route: Route) -> list[type[BaseModel]]:
+ """Every Pydantic model directly referenced by a route (unwrapped from list[...])."""
+ models: list[type[BaseModel]] = []
+
+ if route.response_model:
+ model = route.response_model
+ if get_origin(model) is list:
+ args = get_args(model)
+ model = args[0] if args else None
+ if isinstance(model, type) and issubclass(model, BaseModel):
+ models.append(model)
+
+ if route.request_model:
+ models.append(route.request_model)
+
+ for response_config in (route.responses or {}).values():
+ model = response_config.get("model")
+ if isinstance(model, type) and issubclass(model, BaseModel):
+ models.append(model)
+
+ return models
+
+
def generate_openapi_schema( # noqa: C901
app: Annotated[
FastHTTP,
@@ -347,7 +341,11 @@ def generate_openapi_schema( # noqa: C901
"""
routes = app.routes
- schemas = _collect_schemas(routes)
+ top_level_models: set[type[BaseModel]] = set()
+ for route in routes:
+ top_level_models.update(_route_models(route))
+
+ collector = _SchemaCollector(top_level_models)
security_schemes = _collect_security_schemes(routes)
paths: dict[str, Any] = {}
@@ -364,7 +362,7 @@ def generate_openapi_schema( # noqa: C901
operation: dict[str, Any] = {
"summary": summary,
"operationId": f"{route.method.lower()}_{route.url.replace('https://', '').replace('http://', '').replace('/', '_').replace(':', '').replace('.', '_')}",
- "tags": route.tags or ["Default"],
+ "tags": route.tags or [urlparse(route.url).netloc or "default"],
"responses": {},
"x-original-url": route.url,
}
@@ -377,56 +375,52 @@ def generate_openapi_schema( # noqa: C901
operation["security"] = [{scheme_name: []}]
if route.params:
- operation["parameters"] = [
- {
+ parameters = []
+ for name, value in route.params.items():
+ param: dict[str, Any] = {
"name": name,
"in": "query",
- "schema": _get_type_string(type(value)),
"required": True,
+ "schema": collector.schema_for_value(value),
}
- for name, value in route.params.items()
- ]
+ if isinstance(value, _EXAMPLE_TYPES):
+ param["example"] = value
+ parameters.append(param)
+ operation["parameters"] = parameters
if route.json or route.data or route.request_model:
- request_body: dict[str, Any] = {
- "required": True,
- "content": {},
- }
+ request_body: dict[str, Any] = {"required": True, "content": {}}
if route.request_model:
request_body["content"] = {
"application/json": {
- "schema": {
- "$ref": f"#/components/schemas/{route.request_model.__name__}"
- }
+ "schema": collector.ref_for_model(route.request_model)
}
}
elif route.json:
request_body["content"] = {
"application/json": {
- "schema": _get_type_string(type(route.json))
- or {"type": "object"}
+ "schema": collector.schema_for_value(route.json),
+ "example": route.json,
}
}
else:
- request_body["content"] = {"text/plain": {"schema": {"type": "string"}}}
+ content: dict[str, Any] = {"schema": {"type": "string"}}
+ if isinstance(route.data, str):
+ content["example"] = route.data
+ request_body["content"] = {"text/plain": content}
operation["requestBody"] = request_body
- if route.response_model:
- operation["responses"]["200"] = _generate_response_schema(
- route.response_model # type: ignore
- )
- else:
- operation["responses"]["200"] = {
- "description": "Successful response",
- }
+ operation["responses"]["200"] = collector.response_schema(
+ route.response_model
+ )
if route.responses:
for status_code, response_config in route.responses.items():
model = response_config.get("model")
operation["responses"][str(status_code)] = (
- _generate_error_response_schema(status_code, model)
+ collector.error_response_schema(status_code, model)
)
else:
operation["responses"]["400"] = {"description": "Bad request"}
@@ -445,7 +439,7 @@ def generate_openapi_schema( # noqa: C901
if description:
info["description"] = description
- components: dict[str, Any] = {"schemas": schemas}
+ components: dict[str, Any] = {"schemas": collector.schemas}
if security_schemes:
components["securitySchemes"] = security_schemes
diff --git a/fasthttp/openapi/routes.py b/fasthttp/openapi/routes.py
index 630c541..8bdd3cc 100644
--- a/fasthttp/openapi/routes.py
+++ b/fasthttp/openapi/routes.py
@@ -92,6 +92,7 @@ async def handle_not_found(
html = get_not_found_html(
docs_url=urls["docs_url"],
openapi_url=urls["openapi_url"],
+ redoc_url=urls["redoc_url"],
)
return Response(
status=status.HTTP_404_NOT_FOUND,
diff --git a/fasthttp/openapi/swagger.py b/fasthttp/openapi/swagger.py
index dca540c..df9e5db 100644
--- a/fasthttp/openapi/swagger.py
+++ b/fasthttp/openapi/swagger.py
@@ -361,6 +361,7 @@
fasthttp