Skip to content

Bug: OpenAPI schema generation raises StopIteration when a custom response model is a generic alias such as list[Model] #8450

Description

@aalvrz

Expected Behaviour

Declaring a list body in a custom response, e.g.

responses={200: {"content": {"application/json": {"model": list[Item]}}}}

should produce an array schema for that response, as it did through 3.24.0.

Current Behaviour

Since 3.25.0, get_openapi_schema() (and therefore get_openapi_json_schema(), enable_swagger(), and OpenAPI validation setup) raises an unhandled StopIteration for any route whose custom response model is a generic alias. Routes whose model is a plain Pydantic class are unaffected.

  File "aws_lambda_powertools/event_handler/openapi/schema_generator.py", line 251, in _build_responses
    operation_responses[resp_code] = _build_custom_response(
        response=copy.deepcopy(responses[resp_code]),
        ...
  File "aws_lambda_powertools/event_handler/openapi/schema_generator.py", line 517, in _build_custom_response
    response["content"][content_type] = _resolve_response_payload(
        ...
  File "aws_lambda_powertools/event_handler/openapi/schema_generator.py", line 539, in _resolve_response_payload
    return_field = next(
        filter(
            lambda model: model.type_ is model_payload_typed["model"],
            dependant.response_extra_models,
        ),
    )
StopIteration

Root cause

#7952 (shipped in 3.25.0, fixing #7711) started deep-copying each custom response dict before resolving its model:

response=copy.deepcopy(responses[resp_code]),

_resolve_response_payload then locates the matching ModelField with an identity comparison:

lambda model: model.type_ is model_payload_typed["model"]

copy.deepcopy returns the same object for a class, so model: Item still matches. It returns a new types.GenericAlias for list[Item], so the identity check never matches, next() exhausts the filter, and StopIteration escapes. The AssertionError("Model declared in custom responses was not found") guard on the next line is never reached.

>>> import copy
>>> copy.deepcopy(list[int]) is list[int]
False
>>> copy.deepcopy(list[int]) == list[int]
True

The regression tests added in #7952 use list return annotations on the handler but a plain class as the custom response model, so this path was not covered.

Code snippet

from pydantic import BaseModel

from aws_lambda_powertools.event_handler import APIGatewayRestResolver

app = APIGatewayRestResolver(enable_validation=True)


class Item(BaseModel):
    id: int


@app.get(
    "/items",
    responses={200: {"content": {"application/json": {"model": list[Item]}}}},
)
def list_items() -> list[Item]:
    return []


app.get_openapi_schema()  # StopIteration on >= 3.25.0, works on <= 3.24.0

Possible Solution

Compare by equality instead of identity in _resolve_response_payload; GenericAlias implements __eq__ structurally and classes compare by identity anyway:

lambda model: model.type_ == model_payload_typed["model"]

With that one-line change the snippet above and a 100+ route application that hit this both generate correctly.

Alternatives: resolve the model against response_extra_models before deep-copying, or exclude the model value from the copy (it is only read, never mutated).

Steps to Reproduce

  1. pip install "aws-lambda-powertools==3.34.0" pydantic
  2. Run the snippet above.
  3. Observe StopIteration.
  4. pip install "aws-lambda-powertools==3.24.0" and run it again: the schema is generated.

Verified on 3.24.0 (works), 3.25.0 (fails), 3.34.0 (fails) with Python 3.13.7 and Pydantic 2.12.5. The identity check is still present on develop at the time of writing.

Powertools for AWS Lambda (Python) version

3.34.0

AWS Lambda function runtime

3.13

Packaging format used

PyPi

Debugging logs

Activity

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

Metadata

Metadata

Assignees

No one assigned

    Labels

    bugSomething isn't workingtriagePending triage from maintainers

    Type

    No type

    Projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions