Document Response subclasses declared in return annotations in OpenAPI by AlberLC · Pull Request #15040 · fastapi/fastapi
Background and Motivation
This PR builds on the discussion #14172 I opened some months ago regarding the possibility of reflecting Response subclasses declared in return type annotations in OpenAPI documentation.
Example 1:
@router.get('/{file_name}') async def embed_page(file_name: str, request: Request) -> HTMLResponse | RedirectResponse: file_url = request.url_for('get_file', file_name=file_name) if 'bot' not in request.headers.get('user-agent', '').lower(): return RedirectResponse(file_url) return HTMLResponse(embed_service.generate_html(file_name, file_url, request))
Example 2:
class Item(BaseModel): id: int class User(BaseModel): username: str password: str @app.get('/') def read_root() -> HTMLResponse | RedirectResponse | Item | PlainTextResponse | User: return User(username='root', password='<PASSWORD>')
After revisiting the topic and spending more time reading through the routing and OpenAPI internals, I refined the original idea to better align with FastAPI’s design principles.
FastAPI already leverages return type annotations for response models. However, Response subclasses declared in return annotations are currently not reflected in OpenAPI, and unions of Response subclasses have historically led to model inference errors unless response_model=None was explicitly set (#13053).
Several past discussions and issues have highlighted limitations around unions and response model inference (e.g. #101, #2296, #5215, #875, #1436). While simple single-response cases were improved in #5855, unions of Response subclasses and mixed unions (Response + models) still expose inconsistencies in documentation and model inference.
This PR aims to address those inconsistencies while preserving FastAPI’s runtime design principles.
Scope and Design Decisions
After getting used to how cleanly response models can be declared in return type annotations (instead of always relying on the response_model parameter in the decorator), it felt natural to explore whether Response subclasses could follow a similar pattern.
However, extending that idea to runtime behavior quickly revealed deeper implications. Unlike response models, response_class is not only descriptive metadata, it is the fallback rendering mechanism used when the returned value is not already a Response instance. Allowing unions of Response subclasses to implicitly determine response_class would require additional resolution logic (for example, selecting one class over another, prioritizing a specific implementation, or relying on declaration order), introducing ambiguity and hidden behavior.
This becomes even more problematic when considering streaming responses, including JSONL and SSE support, where response handling is tightly coupled with rendering semantics.
For these reasons, the decision was made to keep response_class strictly runtime-oriented and unaffected by return annotations. Response subclasses declared in return types are therefore used exclusively for OpenAPI documentation.
During exploration, I also considered whether status codes could be inferred dynamically from the function body. However, that would require inspecting the implementation rather than relying purely on type information, introducing fragile heuristics and going beyond FastAPI’s current boundaries. Similar concerns were raised previously when discussing automatic documentation of HTTPException status codes, where inspecting internal logic was considered unreliable (see #1573 (comment)). For that reason, this change strictly operates on return type annotations and does not attempt to inspect return expressions or infer dynamic status codes.
Therefore:
- The decorator parameter
response_classstill determines how non-response return values are rendered. - No implicit selection between multiple
Responseclasses is introduced. - No dynamic status code inference from function bodies.
- No changes to response serialization semantics.
Overall, the scope of this PR is limited to improving OpenAPI documentation and removing inconsistencies in return annotation handling, without altering how responses are rendered at runtime.
What This PR Does
Response subclasses found in return annotations are extracted before response model inference. They are excluded from Pydantic model field creation and instead used exclusively for OpenAPI documentation.
The generated OpenAPI schema now reflects:
- Media types of
Responsesubclasses declared in return annotations. - Default status codes defined by those
Responseclasses (for example,307forRedirectResponse). - Multiple
Responsesubclasses when unions are declared. - Mixed unions combining
Responsesubclasses with Pydantic models or primitive types.
Unions of Response subclasses no longer raise model inference errors, and explicit response_model=None is no longer required in those scenarios.
The implementation integrates with the new JSONL and SSE streaming logic. The route.is_json_stream behavior remains unchanged.
Tests
All existing tests pass.
A small number of tests were adjusted where previous OpenAPI snapshots did not accurately reflect declared return annotations (for example, unions of Response subclasses or media type expectations).
Additionally, 36 new tests were added, and these tests provide complete coverage for the new logic and ensure compatibility with existing behavior, covering:
- Single
Responsesubclass annotations. - Unions of multiple
Responsesubclasses. - Mixed unions (
Responsesubclasses + models). - Mixed unions including primitive types.
- Interaction between return annotations and decorator
response_class. - Edge cases involving
None.
Documentation Impact
The following documentation sections may require updates to reflect this behavior:
This PR does not modify documentation content, as wording and presentation decisions are likely better handled separately.
Feedback is very welcome.

