Skip to content

🐛 Support Annotated in stream return annotations for JSONL and SSE - #16348

Open
Anurag-M1 wants to merge 2 commits into
fastapi:masterfrom
Anurag-M1:fix/annotated-stream-item-type
Open

Anurag-M1 wants to merge 2 commits into
fastapi:masterfrom
Anurag-M1:fix/annotated-stream-item-type

Conversation

@Anurag-M1

@Anurag-M1 Anurag-M1 commented Sep 12, 2026 •

Copy link
Copy Markdown

Pull Request

Discussion: #16337

Description

Fixes #16334

Problem

Wrapping an async stream return annotation (e.g. AsyncIterator[T], AsyncIterable[T], or AsyncGenerator[T, None]) in typing.Annotated causes route registration to raise FastAPIError: Invalid args for response field! at application startup:

@app.get("/stream")
async def stream() -> Annotated[AsyncIterator[Item], "metadata"]:
    yield Item(name="foo")

This occurs because get_stream_item_type() in fastapi/dependencies/utils.py checks whether get_origin(annotation) is in _STREAM_ORIGINS. Because the outermost origin of Annotated[...] is Annotated, get_stream_item_type returns None. FastAPI then erroneously falls back to treating the entire streaming iterator annotation as a single HTTP response model, which Pydantic cannot serialize as a field.

Solution

Unwrap Annotated layers in get_stream_item_type before evaluating against _STREAM_ORIGINS:

def get_stream_item_type(annotation: Any) -> Any | None:
    while get_origin(annotation) is Annotated:
        annotation = get_args(annotation)[0]
    origin = get_origin(annotation)
    if origin is not None and origin in _STREAM_ORIGINS:
        type_args = get_args(annotation)
        if type_args:
            return type_args[0]
        return Any
    return None

This restores complete parity between unwrapped AsyncIterator[T] and Annotated[AsyncIterator[T], ...]:

  • Routes register and run successfully.
  • Yielded stream items are validated against the extracted item type T.
  • OpenAPI schema generation correctly emits itemSchema for both application/jsonl and text/event-stream (SSE).

Tests

Added comprehensive test coverage in tests/test_stream_annotated.py:

  • test_get_stream_item_type_unwraps_annotated: Unit tests verifying get_stream_item_type unwrap behavior with single and nested Annotated types, sync/async iterators, and non-stream types.
  • test_stream_annotated_jsonl: End-to-end JSON Lines streaming with Annotated[AsyncIterator[Item], "metadata"].
  • test_stream_nested_annotated_jsonl: Nested Annotated wrappers.
  • test_stream_annotated_async_generator: AsyncGenerator.
  • test_stream_annotated_sync: Synchronous Iterator.
  • test_stream_annotated_sse: Server-Sent Events with EventSourceResponse.
  • test_router_stream_annotated: Streaming route included through APIRouter.
  • test_stream_annotated_openapi_schema: Verifying generated OpenAPI itemSchema for JSONL and SSE endpoints.

All 8 new tests fail on master with FastAPIError and pass cleanly with this fix. The existing stream and SSE test suites (67 tests) pass without regression, and all prek pre-commit hooks pass.

AI Disclaimer

Prompt: Diagnose issue #16334 / discussion #16337 in FastAPI regarding Annotated stream return annotations, implement the fix in get_stream_item_type, add comprehensive tests, and ensure all tests and linters pass.
Model: Gemini 3.8 Flash

Checklist

  • This PR links to a GitHub Discussion for the proposed code change.
  • I added tests for the change.
  • The new or updated tests fail on the main branch and pass on this PR.
  • Coverage stays at 100%.
  • The documentation explains the change if needed.

@codspeed

codspeed Bot commented Sep 12, 2026 •

Copy link
Copy Markdown

Merging this PR will not alter performance

✅ 24 untouched benchmarks


Comparing Anurag-M1:fix/annotated-stream-item-type (08454a0) with master (50113da)

Open in CodSpeed

@VimalN2005

Copy link
Copy Markdown

Hi @Anurag-M1, great PR! Supporting Annotated in streaming return types is super helpful for LLM and SSE endpoints.

One quick observation in fastapi/dependencies/utils.py:

while get_origin(annotation) is Annotated:
    annotation = get_args(annotation)[0]

@Anurag-M1

Copy link
Copy Markdown
Author

Hi @VimalN2005, thanks for reviewing!

It looks like your comment may have been submitted before you finished typing. In case you were wondering about the direct get_args(annotation)[0] indexing (or noticed the discrepancy with the earlier PR description snippet):

  1. Safety: When get_origin(annotation) is Annotated, annotation is guaranteed to be a subscripted Annotated[...] (for bare Annotated, get_origin(Annotated) evaluates to None), which always has at least two arguments in Python's typing system (Annotated[T, metadata]).
  2. 100% Coverage: An earlier draft included an explicit if not type_args: break guard, but we removed it in commit 08454a0 because that branch was completely unreachable and failed FastAPI's strict 100% test coverage requirement in CI.

I have also updated the PR description snippet to match the implementation in fastapi/dependencies/utils.py. Please feel free to let me know if you had any other observations or thoughts!

Shriprasad-P

This comment was marked as spam.

@Anurag-M1

Copy link
Copy Markdown
Author

Hi @Shriprasad-P, thanks for the review!

Yes, the new path is fully covered across unit, end-to-end runtime, and OpenAPI schema generation in tests/test_stream_annotated.py:

  1. Unit Tests (test_get_stream_item_type_unwraps_annotated):

    • Single and nested Annotated unwrapping (Annotated[Annotated[AsyncIterator[T], ...], ...]).
    • Various streaming types (AsyncIterator, AsyncIterable, AsyncGenerator, and sync Iterator / Iterable).
    • Edge cases: bare/unsubscripted iterators, non-stream types wrapped in Annotated, and Any.
  2. Runtime Integration Tests:

    • JSONL: test_stream_annotated_jsonl (streaming Pydantic models) and test_stream_nested_annotated_jsonl.
    • SSE: test_stream_annotated_sse with EventSourceResponse emitting formatted SSE events (data: ...\n\n).
    • Generator & Sync parity: test_stream_annotated_async_generator and test_stream_annotated_sync.
    • Router mounting: test_router_stream_annotated via APIRouter.
  3. OpenAPI Schema Verification:

    • test_stream_annotated_openapi_schema verifies that itemSchema is accurately emitted for both application/jsonl and text/event-stream endpoints.

All 8 tests fail on master with FastAPIError: Invalid args for response field! and pass on this branch, with FastAPI's coverage-combine CI check passing at 100% coverage.

Please let me know if there are any specific scenarios or edge cases you'd like to see added!

This branch has not been deployed

No deployments
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Annotated[AsyncIterator[T], ...] return annotations fail route registration

3 participants