powertools-lambda-python icon indicating copy to clipboard operation
powertools-lambda-python copied to clipboard

Bug: Broken behaviour for setting alias in Query Annotated Field after pydantic 2.12.0 update

Open migrund opened this issue 2 months ago • 5 comments

Expected Behaviour

When defining a Annotated Query Field it should behave in the same way as pydantic does (set validation_alias the same as alias if not set otherwise).

This issue was reported in pydantic (#12369) before. I was told that this is an issue how powertools-lambda-python implements the FieldInfo. It is related to this topic in pydantic (#12374)

Current Behaviour

The validation_alias attribute is not set if not explicitly defined.

Code snippet

from __future__ import annotations

from http import HTTPStatus
from typing import Annotated
from typing import Any
from unittest.mock import MagicMock

from annotated_types import Ge
from annotated_types import Le
from aws_lambda_powertools.event_handler.api_gateway import APIGatewayRestResolver
from aws_lambda_powertools.event_handler.api_gateway import Response
from aws_lambda_powertools.event_handler.openapi.params import Query
from aws_lambda_powertools.utilities.typing import LambdaContext
from pydantic import StringConstraints
from pydantic import validate_call

app = APIGatewayRestResolver(
    strip_prefixes=["/my-service"],
    enable_validation=True,
)

type IntQuery = Annotated[int, Ge(1), Le(100)]
type StrQuery = Annotated[str, StringConstraints(min_length=4, max_length=128)]


def handler(event: dict[str, Any], context: LambdaContext) -> dict[str, Any]:
    return app.resolve(event, context)


@app.get("/foo")
def get_foo(
    # With pydantic v2.11.9: PydanticValidation and PowertoolsValidation tests successful
    # With pydantic v2.12.0: PydanticValidation test fails with ValidationError, PowertoolsValidation test successful
    str_query: Annotated[StrQuery, Query(alias="strQuery")],
    int_query: Annotated[IntQuery, Query(alias="intQuery")],
    #
    # With pydantic v2.11.9: PydanticValidation and PowertoolsValidation tests fail with ValidationError
    # With pydantic v2.12.0: PydanticValidation test successful, PowertoolsValidation test fails with ValidationError
    # str_query: Annotated[StrQuery, Query(validation_alias="strQuery")],
    # int_query: Annotated[IntQuery, Query(validation_alias="intQuery")],
    #
    # With pydantic v2.11.9: PydanticValidation and PowertoolsValidation tests successful
    # With pydantic v2.12.0: PydanticValidation and PowertoolsValidation tests successful
    # str_query: Annotated[StrQuery, Query(alias="strQuery", validation_alias="strQuery")],
    # int_query: Annotated[IntQuery, Query(alias="intQuery", validation_alias="intQuery")],
) -> Response[str]:
    return Response(HTTPStatus.OK, content_type="plain/text", body=f"{int_query}, {str_query}")


class TestPydanticValidation:
    def test__valid_parameters_given__success_expected(self):
        # GIVEN
        valid_parameters = dict(intQuery=20, strQuery="fooBarFizzBuzz")

        # WHEN
        result = validate_call(get_foo)(**valid_parameters)

        # THEN
        assert result.body == "20, fooBarFizzBuzz"


class TestPowertoolsValidation:
    def test__valid_parameters_given__success_expected(self):
        # GIVEN
        api_gateway_proxy_event = {
            "httpMethod": "GET",
            "path": "/my-service/foo",
            "multiValueQueryStringParameters": {
                "intQuery": ["20"],
                "strQuery": ["fooBarFizzBuzz"],
            },
        }

        # WHEN
        response = handler(api_gateway_proxy_event, MagicMock())

        # THEN
        assert response["body"] == "20, fooBarFizzBuzz"
        assert response["statusCode"] == HTTPStatus.OK

Possible Solution

No response

Steps to Reproduce

In the above code snippet (un-)comment str_query and int_query depending on the pydantic version and the use-case.

Powertools for AWS Lambda (Python) version

latest

AWS Lambda function runtime

3.13

Packaging format used

PyPi

Debugging logs


migrund avatar Oct 17 '25 13:10 migrund

Thanks for opening your first issue here! We'll come back to you as soon as we can. In the meantime, check out the #python channel on our Powertools for AWS Lambda Discord: Invite link

boring-cyborg[bot] avatar Oct 17 '25 13:10 boring-cyborg[bot]

Hey @migrund thanks for opening this issue!

I ran into the same issue and did some digging. From what I understand, this is happening because Pydantic 2.12.0 changed how it handles aliases in subclasses of FieldInfo. Basically, when you write Query(alias="strQuery"), Pydantic used to automatically set validation_alias to the same value as alias. But in 2.12+, it doesn't do that anymore for custom FieldInfo subclasses like our Query class.

So when validate_call() tries to validate your function parameters, it's looking for validation_alias but finds None, which breaks the alias mapping.

I think the solution might be to automatically set validation_alias = alias when someone provides an alias but no validation_alias, but I'd need to test this properly first.

Here's what I think needs to change in the params.py file:

In the Param class __init__ method (around line 220), add this:

# Fix for Pydantic 2.12+ - auto-set validation_alias when alias is provided
if alias is not None and validation_alias is None:
    validation_alias = alias

And in the Header class, we might need to handle the lowercase conversion properly:

# Handle alias and validation_alias for headers (they get lowercased)
if alias is not None:
    self._alias = alias.lower()
    if validation_alias is None:
        validation_alias = alias.lower()
else:
    self._alias = alias

I did some quick local testing and this seems to fix the issue, but we'd need proper tests to make sure it doesn't break anything else.

Speaking of tests - I noticed we don't have any tests that specifically cover the interaction between Query(alias="...") and validate_call(). That's probably why this regression slipped through when Pydantic 2.12 was released. We should definitely add some tests for this scenario to catch similar issues in the future.

Please let me know your thoughts.

leandrodamascena avatar Oct 17 '25 14:10 leandrodamascena

Thank you for the quick response.

I'm glad you already found a potential way to get this issue fixed in the future.

I assume the same would happen with the ´serialization_alias´, although I haven't tried that out. So this should also be considered.

migrund avatar Oct 17 '25 14:10 migrund

Hi @leandrodamascena, to be clear, nothing changed in Pydantic 2.12 regarding aliases. The following code behaves the same in 2.11/2.12:

from pydantic import Field

from aws_lambda_powertools.event_handler.openapi.params import Query

q = Query(alias='test')
q.alias
#> 'test'
q.validation_alias
#> None

f = Field(alias='test')
f.alias
#> 'test'
f.validation_alias
#> 'test'

Populating the validation/serialization_alias from alias is done in our Field() function, before constructing the FieldInfo instance.

Because Query is defined as a FieldInfo subclass, this logic doesn't happen (note that because we don't explicitly support FieldInfo subclasses from third party libraries, other issues like this one could occur).

Viicos avatar Oct 17 '25 15:10 Viicos

Hi @leandrodamascena, to be clear, nothing changed in Pydantic 2.12 regarding aliases. The following code behaves the same in 2.11/2.12:

from pydantic import Field

from aws_lambda_powertools.event_handler.openapi.params import Query

q = Query(alias='test') q.alias #> 'test' q.validation_alias #> None

f = Field(alias='test') f.alias #> 'test' f.validation_alias #> 'test' Populating the validation/serialization_alias from alias is done in our Field() function, before constructing the FieldInfo instance.

Thanks for clarifying the behavior @Viicos! I now understand the change.

Because Query is defined as a FieldInfo subclass, this logic doesn't happen (note that because we don't explicitly support FieldInfo subclasses from third party libraries, other issues like this one could occur).

We only subclass FieldInfo in our Params classes, and I think fixing that will solve the problem.

I'll work on a solution and try to get the PR to fix the issue.

Thanks both for the explanation.

leandrodamascena avatar Oct 17 '25 15:10 leandrodamascena

Hi @leandrodamascena if you have not already worked on this, let me know if you can take it up.

oyiz-michael avatar Nov 14 '25 22:11 oyiz-michael