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))

image

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>')

chrome_LkeuELpI0T

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_class still determines how non-response return values are rendered.
  • No implicit selection between multiple Response classes 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 Response subclasses declared in return annotations.
  • Default status codes defined by those Response classes (for example, 307 for RedirectResponse).
  • Multiple Response subclasses when unions are declared.
  • Mixed unions combining Response subclasses 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 Response subclass annotations.
  • Unions of multiple Response subclasses.
  • Mixed unions (Response subclasses + 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.