-
Notifications
You must be signed in to change notification settings - Fork 470
Description
Expected Behaviour
When using Union[Model, List[Model]] or RootModel[List[Model]] as a Body parameter annotation, and sending a JSON array like [{...}, {...}, {...}], the handler should receive the entire list of items for validation and processing.
Both patterns should work:
Union[Model, List[Model]]- to accept either a single item or a listRootModel[List[Model]]- to wrap a list in a Pydantic model
Current Behaviour
Since v3.21.0, when sending a JSON array to endpoints with these type annotations, only the first element is passed to the handler instead of the entire list. The _normalize_field_value() function incorrectly treats these complex types as non-sequence fields and normalizes the list to value[0].
Code snippet
### Case 1: Union[Model, List[Model]]
from typing import Annotated, Union
from pydantic import BaseModel
from aws_lambda_powertools.event_handler import APIGatewayRestResolver
from aws_lambda_powertools.event_handler.openapi.params import Body
app = APIGatewayRestResolver(enable_validation=True)
class Item(BaseModel):
name: str
value: int
@app.post("/items")
def post_items(
items: Annotated[Union[Item, list[Item]], Body()],
):
# When sending [{"name": "a", "value": 1}, {"name": "b", "value": 2}]
# Expected: items = [Item(name="a", value=1), Item(name="b", value=2)]
# Actual in v3.21.0+: items = Item(name="a", value=1) # Only first element!
if isinstance(items, list):
return {"count": len(items)} # Should return {"count": 2}
else:
return {"count": 1}
def lambda_handler(event, context):
return app(event, context)
### Case 2: RootModel[List[Model]]
from typing import Annotated
from pydantic import BaseModel, RootModel
from aws_lambda_powertools.event_handler import APIGatewayRestResolver
from aws_lambda_powertools.event_handler.openapi.params import Body
app = APIGatewayRestResolver(enable_validation=True)
class Item(BaseModel):
name: str
value: int
class ItemCollection(RootModel[list[Item]]):
root: list[Item]
@app.post("/items")
def post_items(
collection: Annotated[ItemCollection, Body()],
):
# When sending [{"name": "a", "value": 1}, {"name": "b", "value": 2}]
# Expected: collection.root = [Item(name="a", value=1), Item(name="b", value=2)]
# Actual in v3.21.0+: Validation error or only first element
return {"count": len(collection.root)}
def lambda_handler(event, context):
return app(event, context)
---Possible Solution
The root cause is in the _normalize_field_value() function in aws_lambda_powertools/event_handler/middlewares/openapi_validation.py (lines 434-441).
The function only checks if the outer type annotation is a sequence, but doesn't inspect:
- Whether a Union contains any sequence types
- Whether a RootModel wraps a sequence type
Proposed fix:
Add a helper function _is_or_contains_sequence() that recursively checks:
- If a
Unioncontains any sequence member types - If a
RootModelwraps a sequence in itsrootfield
def _is_or_contains_sequence(annotation: Any) -> bool:
"""Check if annotation is a sequence or Union/RootModel containing a sequence."""
# Direct sequence check
if field_annotation_is_sequence(annotation):
return True
# Check Union members for any sequence types
origin = get_origin(annotation)
if origin is Union or origin is UnionType:
for arg in get_args(annotation):
if field_annotation_is_sequence(arg):
return True
# Check if it's a RootModel wrapping a sequence
if lenient_issubclass(annotation, BaseModel):
if getattr(annotation, "__pydantic_root_model__", False):
if hasattr(annotation, "model_fields") and "root" in annotation.model_fields:
root_field = annotation.model_fields["root"]
return field_annotation_is_sequence(root_field.annotation)
return False
def _normalize_field_value(value: Any, field_info: FieldInfo) -> Any:
"""Normalize field value, converting lists to single values for non-sequence fields."""
if _is_or_contains_sequence(field_info.annotation):
return value
elif isinstance(value, list) and value:
return value[0]
return valueSteps to Reproduce
- Create a Lambda function with Powertools v3.21.0 or later
- Define an endpoint with
Union[Model, List[Model]]orRootModel[List[Model]]as a Body parameter - Send a POST request with a JSON array body:
[{"name": "a", "value": 1}, {"name": "b", "value": 2}] - Observe that the handler only receives the first element or validation fails
Working in v3.20.0 and earlier
Broken in v3.21.0, v3.25.0, v3.26.0
Powertools for AWS Lambda (Python) version
latest
AWS Lambda function runtime
3.13
Packaging format used
Lambda Layers
Debugging logs
Not applicable - this is a validation/type checking issue that occurs before the handler executes.Metadata
Metadata
Assignees
Labels
Type
Projects
Status