Skip to content

Bug: Using discriminator field with TypeAdapter instances #5476

Closed
@xkortex

Description

@xkortex

Use case

I happened upon using the discriminator to allow my lambdas to handle multiple different event sources, e.g. aws.events and aws.s3. Using Pydantic's TypeAdapter and Field classes, we can create an adapter which looks at a specific field (in this case source) to determine how to parse the data, without having to do the expensive try-validate-fail-try_next_model pattern. This allows us to decorate a lambda in such a way that it can automatically cast different one of several event types to stricter subtypes

EventBridgeSource = Annotated[
    Union[S3EventNotificationEventBridgeModel, ScheduledNotificationEventBridgeModel],
    Field(discriminator="source"),
]

EventBridgeModelAdapter = TypeAdapter(Union[EventBridgeSource, EventBridgeModel])

Solution/User Experience

This fails with the stock event_parser so I had to bodge event_parser to get it to work. I think the issue is trying to cast the input TypeAdapter into a TypeAdapter again, throwing PydanticSchemaGenerationError. It should be straightforward to allow _retrieve_or_set_model_from_cache to check if the input is already a TypeAdapter and return that from the cache rather than trying to wrap it.

Setup

from typing import Any, Callable, Literal, Optional, TypeVar, Union

from typing_extensions import Annotated

from pydantic import BaseModel, Field, ValidationError, TypeAdapter

# from aws_lambda_powertools.utilities.parser import event_parser
from aws_lambda_powertools.utilities.parser.envelopes.base import Envelope

from aws_lambda_powertools.middleware_factory import lambda_handler_decorator
from aws_lambda_powertools.utilities.parser.models.event_bridge import EventBridgeModel
from aws_lambda_powertools.utilities.parser.models.s3 import (
    S3EventNotificationEventBridgeDetailModel,
)


T = TypeVar("T")


class S3EventNotificationEventBridgeModel(EventBridgeModel):
    detail: S3EventNotificationEventBridgeDetailModel
    source: Literal["aws.s3"]


class ScheduledNotificationEventBridgeModel(EventBridgeModel):
    source: Literal["aws.events"]


EventBridgeSource = Annotated[
    Union[S3EventNotificationEventBridgeModel, ScheduledNotificationEventBridgeModel],
    Field(discriminator="source"),
]

EventBridgeModelAdapter = TypeAdapter(Union[EventBridgeSource, EventBridgeModel])


class LambdaContext:
    function_name: str = "test-function"
    memory_limit_in_mb: int = 128
    invoked_function_arn: str = f"arn:aws:lambda:us-east-1:123456789012:test-function"
    aws_request_id: str = 'e7a150fa-ed3d-4fbb-8158-e470b60621da'

@lambda_handler_decorator
def event_parser(
    handler: Callable[..., 'EventParserReturnType'],
    event: dict[str, Any],
    context: LambdaContext = None,
    model: Optional[type[T]] = None,
    envelope: Optional[type[Envelope]] = None,
    **kwargs: Any,
) -> 'EventParserReturnType':
    parsed_event = EventBridgeModelAdapter.validate_python(event)
    return handler(parsed_event, context, **kwargs)


@event_parser(model=EventBridgeModelAdapter)
def handler(event: EventBridgeModelAdapter, context = None):
    print(f'Got event, parsed into {type(event)=}')

Testing

object_event = {
  "version": "0",
  "id": "55ec5937-9835-fb01-3122-a054f478882b",
  "detail-type": "Object Created",
  "source": "aws.s3",
  "account": "539101081866",
  "time": "2024-10-16T22:58:07Z",
  "region": "us-east-1",
  "resources": [
    "arn:aws:s3:::bpa-sandbox-mcdermott"
  ],
  "detail": {
    "version": "0",
    "bucket": {
      "name": "bpa-sandbox-mcdermott"
    },
    "object": {
      "key": "testbed/hello3.txt",
      "size": 12,
    },
    "requester": "539101081866",
  }
}

scheduled_event = {
  "version": "0",
  "id": "ac8bd595-adcc-cd98-cc06-5e8fad5d9565",
  "detail-type": "Scheduled Event",
  "source": "aws.events",
  "account": "084286224071",
  "time": "2024-10-23T09:00:00Z",
  "region": "us-east-1",
  "resources": [
    "arn:aws:events:us-east-1:084286224071:rule/rl-live-StatesIntegration-RuleDailyDownloadWorkflo-1KZH76DGASA21"
  ],
  "detail": {}
}

fallback_event = {
  "version": "0",
  "id": "ac8bd595-adcc-cd98-cc06-5e8fad5d9565",
  "detail-type": "Scheduled Event",
  "source": "aws.other",
  "account": "084286224071",
  "time": "2024-10-23T09:00:00Z",
  "region": "us-east-1",
  "resources": [
    "arn:aws:events:us-east-1:084286224071:rule/rl-live-StatesIntegration-RuleDailyDownloadWorkflo-1KZH76DGASA21"
  ],
  "detail": {}
}

handler(object_event, LambdaContext())
handler(scheduled_event, LambdaContext())
handler(fallback_event, LambdaContext())

results

Got event, parsed into type(event)=<class '__main__.S3EventNotificationEventBridgeModel'>
Got event, parsed into type(event)=<class '__main__.ScheduledNotificationEventBridgeModel'>
Got event, parsed into type(event)=<class 'aws_lambda_powertools.utilities.parser.models.event_bridge.EventBridgeModel'>

Alternative solutions

Not sure how else to implement this.

Acknowledgment

Metadata

Metadata

Labels

bugSomething isn't workingparserParser (Pydantic) utility

Type

No type

Projects

Status

Shipped

Milestone

No milestone

Relationships

None yet

Development

No branches or pull requests

Issue actions