Skip to content

Bug: Union[Model, List[Model]] and RootModel[List[Model]] with Body() only receive first element since v3.21.0 #8057

@danjhd

Description

@danjhd

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:

  1. Union[Model, List[Model]] - to accept either a single item or a list
  2. RootModel[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:

  1. Whether a Union contains any sequence types
  2. Whether a RootModel wraps a sequence type

Proposed fix:

Add a helper function _is_or_contains_sequence() that recursively checks:

  • If a Union contains any sequence member types
  • If a RootModel wraps a sequence in its root field
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 value

Steps to Reproduce

  1. Create a Lambda function with Powertools v3.21.0 or later
  2. Define an endpoint with Union[Model, List[Model]] or RootModel[List[Model]] as a Body parameter
  3. Send a POST request with a JSON array body: [{"name": "a", "value": 1}, {"name": "b", "value": 2}]
  4. 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

No one assigned

    Labels

    bugSomething isn't workingtriagePending triage from maintainers

    Type

    No type

    Projects

    Status

    Triage

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions