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
pip install "aws-lambda-powertools==3.34.0" pydantic
- Run the snippet above.
- Observe
StopIteration.
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
Expected Behaviour
Declaring a list body in a custom response, e.g.
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 thereforeget_openapi_json_schema(),enable_swagger(), and OpenAPI validation setup) raises an unhandledStopIterationfor any route whose custom responsemodelis a generic alias. Routes whosemodelis a plain Pydantic class are unaffected.Root cause
#7952 (shipped in 3.25.0, fixing #7711) started deep-copying each custom response dict before resolving its
model:_resolve_response_payloadthen locates the matchingModelFieldwith an identity comparison:copy.deepcopyreturns the same object for a class, somodel: Itemstill matches. It returns a newtypes.GenericAliasforlist[Item], so the identity check never matches,next()exhausts the filter, andStopIterationescapes. TheAssertionError("Model declared in custom responses was not found")guard on the next line is never reached.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
Possible Solution
Compare by equality instead of identity in
_resolve_response_payload;GenericAliasimplements__eq__structurally and classes compare by identity anyway:With that one-line change the snippet above and a 100+ route application that hit this both generate correctly.
Alternatives: resolve the
modelagainstresponse_extra_modelsbefore deep-copying, or exclude themodelvalue from the copy (it is only read, never mutated).Steps to Reproduce
pip install "aws-lambda-powertools==3.34.0" pydanticStopIteration.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
developat 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