Skip to content

BUG: Dependency Injection with Pydantic Validation #8333

Closed

Description

@leandrodamascena

Discussed in #8330

Originally posted by bdellegrazie July 11, 2026
Hi,

I'm trying to use the Dependency injection feature with Pydantic validation on routes and it fails.
Does anyone have a simple example where the injected dependency is not validated by Pydantic and isn't part of the Open API signature?

Non-working Example:

import botocore
import boto3

from typing import Annotated, Any

from aws_lambda_powertools import Logger, Tracer
from aws_lambda_powertools.event_handler import APIGatewayHttpResolver
from aws_lambda_powertools.event_handler.depends import Depends
from aws_lambda_powertools.event_handler.exceptions import (
    NotFoundError,
)
from aws_lambda_powertools.logging import correlation_paths
from aws_lambda_powertools.utilities.typing import LambdaContext
from boto3.dynamodb.types import TypeDeserializer
from pydantic import BaseModel, Field
from types_boto3_dynamodb.client import DynamoDBClient
from types_boto3_dynamodb.type_defs import AttributeValueTypeDef


logger = Logger()
tracer = Tracer()
app: APIGatewayHttpResolver = APIGatewayHttpResolver(
    enable_validation=True,
)


# A simple model
class Example(BaseModel):
    id: Annotated[str, Field(..., description="ID for this object.")]
    info: Annotated[str, Field(..., description="Additional Data")]


def dynamo_to_python(dynamo_object: dict[str, AttributeValueTypeDef]) -> dict[str, Any]:
    deserializer = TypeDeserializer()
    return {k: deserializer.deserialize(v) for k, v in dynamo_object.items()}


def get_botocore_session() -> botocore.session.Session:
    return botocore.session.get_session()


def get_boto3_session(
    botocore_session: Annotated[
        botocore.session.Session,
        Depends(get_botocore_session),
    ],
) -> boto3.Session:
    return boto3.Session(botocore_session=botocore_session)


def get_dynamodb_client(
    boto3_session: Annotated[
        boto3.Session,
        Depends(get_boto3_session),
    ],
) -> DynamoDBClient:
    return boto3_session.client("dynamodb")


@app.get("/example/<id>")
@tracer.capture_method(capture_response=False)
def example(
    id: str,
    dynamodb_client: Annotated[
        DynamoDBClient,
        Depends(get_dynamodb_client),
    ],
) -> Example:
    result = dynamodb_client.get_item(
        TableName="example-table",
        Key={
            "id": {"S": id},
        },
    )
    if "Item" not in result:
        raise NotFoundError
    return Example.model_validate(dynamo_to_python(result["Item"]))


@logger.inject_lambda_context(
    correlation_id_path=correlation_paths.API_GATEWAY_HTTP, clear_state=True
)
@tracer.capture_lambda_handler
def lambda_handler(
    event: dict,
    context: LambdaContext,
) -> dict:
    return app.resolve(event, context)

Simple pytest test class:

@pytest.fixture
def example_table(dynamodb: DynamoDBClient) -> str:
    # Create the example table
    dynamodb.create_table(
        TableName="example-table",
        KeySchema=[{"AttributeName": "id", "KeyType": "HASH"}],
        AttributeDefinitions=[
            {"AttributeName": "id", "AttributeType": "S"},
        ],
        BillingMode="PAY_PER_REQUEST",
    )
    return "example-table"

Fixtures:

import os
import json
import time
import uuid

import botocore
import boto3
import pytest

from typing import Final
from aws_lambda_powertools import Logger
from moto import mock_aws
from types_boto3_dynamodb import DynamoDBClient


MOTO_ACCOUNT_ID: Final[str] = "123456789012"
logger = Logger(child=True)


@pytest.fixture(scope="function")
def aws_credentials() -> None:
    os.environ["AWS_ACCESS_KEY_ID"] = "testing"
    os.environ["AWS_SECRET_ACCESS_KEY"] = "testing"
    os.environ["AWS_SECURITY_TOKEN"] = "testing"
    os.environ["AWS_SESSION_TOKEN"] = "testing"

    # as an additional layer of protection against accidentally touching our real AWS environment
    os.environ["MOTO_ACCOUNT_ID"] = MOTO_ACCOUNT_ID
    os.environ["AWS_DEFAULT_REGION"] = "us-east-1"

    # Framework specific
    os.environ["POWERTOOLS_DEV"] = "true"


@pytest.fixture(scope="function")
def mocked_aws(aws_credentials: None):
    with mock_aws():
        yield


@pytest.fixture(scope="function")
def botocore_session(aws_credentials: None) -> botocore.session.Session:  # type: ignore
    with mock_aws():
        yield botocore.session.get_session()


@pytest.fixture(scope="function")
def boto3_session(botocore_session: botocore.session.Session) -> boto3.Session:  # type: ignore
    with mock_aws():
        yield boto3.Session(botocore_session=botocore_session)


@pytest.fixture(scope="function")
def dynamodb(boto3_session: boto3.Session) -> DynamoDBClient:  # type: ignore
    with mock_aws():
        yield boto3_session.client("dynamodb")


@pytest.fixture
def example_table(dynamodb: DynamoDBClient) -> str:
    # Create the example table
    dynamodb.create_table(
        TableName="example-table",
        KeySchema=[{"AttributeName": "id", "KeyType": "HASH"}],
        AttributeDefinitions=[
            {"AttributeName": "id", "AttributeType": "S"},
        ],
        BillingMode="PAY_PER_REQUEST",
    )
    return "example-table"


@pytest.fixture
def example_item(dynamodb: DynamoDBClient, example_table: str) -> str:
    # Create the example table
    dynamodb.put_item(
        TableName="example-table",
        Item={
            "id": {"S": "example-id"},
            "info": {"S": "example-info"},
        },
    )
    return "example-id"

Results in:

pydantic.errors.PydanticSchemaGenerationError: Unable to generate pydantic-core schema for <class 'botocore.session.Session'>. Set `arbitrary_types_allowed=True` in the model_config to ignore this error or implement `__get_pydantic_core_schema__` on your type to fully support it.

I have tried to:

  • add arbitrary_types_allowed=True to the route but it doesn't work.
  • use SkipValidation on the dynamodb_client but that doesn't work either.

Any ideas?

Thanks,

Brett

Activity

  1. bdellegrazie commented on Jul 11, 2026

    @bdellegrazie

    Thank you @leandrodamascena for the insanely fast response in triage and work-around.

    Related side question: the intent was to ensure that low-level AWS clients are created as efficiently as possible during Lambda startup before processing begins while still remaining testable.
    This is especially important in the context of asynchronous I/O, which this example does not use.

    If there are better approaches I'd love to hear them / be pointed in the general direction.
    Obviously this may obscure the issue / PR with unrelated material, so I'm happy to open a discussion if that's the best course of action.

    Thanks in advance!

  2. leandrodamascena commented on Jul 12, 2026

    @leandrodamascena
    ContributorAuthor

    Hey @bdellegrazie, thanks for the kind words and I'm glad the workaround unblocked you!

    So, the "create the clients efficiently but keep it testable" thing is actually one of my favorite parts of Depends, because you get both for free once you set it up right.

    What I do is build the client once at import time (so the cold start pays for it, not every request) and let the dependency just return it:

    import boto3
    
    _dynamodb = boto3.resource("dynamodb")  # created once, reused on warm invokes
    
    def get_table():
        return _dynamodb.Table("example-table")
    
    @app.get("/example/<id>")
    def example(id: str, table: Annotated[Any, Depends(get_table)]):
        ...

    Then for the tests you never touch the real client and you just override the dependency:

    app.dependency_overrides[get_table] = lambda: fake_table

    That's literally what dependency_overrides is there for. Real client in prod, fake in tests, and the handler has no idea which one it got. This is the part I always tell people about because it's easy to miss.

    Now the async bit is where it gets annoying, and I'll be honest with you: boto3 is sync, so the second you want real concurrent I/O you're either throwing calls into a thread pool or jumping to aioboto3 and babysitting the aiobotocore session yourself (create it at startup, never per-invoke, since it's holding connections).

    If the async I/O is really the thing you care about with DynamoDB, take a look at pydynox. It's async-first with a Rust core, so the network calls are genuinely non-blocking instead of going through sync boto3, and it plays nice with Pydantic if you're already using models. Full disclosure so nobody thinks I'm being sneaky: it's not a Powertools thing, I just think it fits exactly the problem you're describing with async performance.

    Btw, if you want to dig into the whole "how should async AWS clients be structured in Lambda" topic, let's take it to a discussion so it doesn't get lost under this bug. Happy to go back and forth there.

    Thanks

  3. powertools-for-aws-oss-automation commented on Jul 13, 2026

    @powertools-for-aws-oss-automation

    Warning

    This issue is now closed. Please be mindful that future comments are hard for our team to see.
    If you need more assistance, please either reopen the issue, or open a new issue referencing this one.
    If you wish to keep having a conversation with other community members under this issue feel free to do so.

  4. github-actions commented on Jul 13, 2026

    @github-actions
    Contributor

    This is now released under 3.31.1 version!

  5. bdellegrazie commented on Jul 13, 2026

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

Metadata

Metadata

Labels

Type

No type

Projects

Milestone

No milestone

Relationships

None yet

Development

No branches or pull requests

Issue actions