Bug: Broken behaviour for setting alias in Query Annotated Field after pydantic 2.12.0 update
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
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
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.
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.
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).
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_aliasfromaliasis done in ourField()function, before constructing theFieldInfoinstance.
Thanks for clarifying the behavior @Viicos! I now understand the change.
Because
Queryis defined as aFieldInfosubclass, this logic doesn't happen (note that because we don't explicitly supportFieldInfosubclasses 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.
Hi @leandrodamascena if you have not already worked on this, let me know if you can take it up.