From d548350e4a4bba0b3b7779b1aefd6ce1d92e23b3 Mon Sep 17 00:00:00 2001 From: Leandro Damascena Date: Mon, 5 Jan 2026 14:31:02 +0000 Subject: [PATCH 1/4] fix: add support for Pydantic 2.12+ --- .../event_handler/openapi/params.py | 22 ++- .../_pydantic/test_openapi_params.py | 168 ++++++++++++++++++ 2 files changed, 184 insertions(+), 6 deletions(-) diff --git a/aws_lambda_powertools/event_handler/openapi/params.py b/aws_lambda_powertools/event_handler/openapi/params.py index 0439b1c2fc1..9aa719408a9 100644 --- a/aws_lambda_powertools/event_handler/openapi/params.py +++ b/aws_lambda_powertools/event_handler/openapi/params.py @@ -103,7 +103,7 @@ def __init__( alias_priority: int | None = _Unset, # MAINTENANCE: update when deprecating Pydantic v1, import these types # MAINTENANCE: validation_alias: str | AliasPath | AliasChoices | None - validation_alias: str | None = None, + validation_alias: str | None = _Unset, serialization_alias: str | None = None, title: str | None = None, description: str | None = None, @@ -217,6 +217,11 @@ def __init__( self.openapi_examples = openapi_examples + # Pydantic 2.12+ no longer copies alias to validation_alias automatically + # Set validation_alias to alias when not explicitly provided + if validation_alias is _Unset and alias is not None: + validation_alias = alias + kwargs.update( { "annotation": annotation, @@ -254,7 +259,7 @@ def __init__( alias_priority: int | None = _Unset, # MAINTENANCE: update when deprecating Pydantic v1, import these types # MAINTENANCE: validation_alias: str | AliasPath | AliasChoices | None - validation_alias: str | None = None, + validation_alias: str | None = _Unset, serialization_alias: str | None = None, title: str | None = None, description: str | None = None, @@ -386,7 +391,7 @@ def __init__( annotation: Any | None = None, alias: str | None = None, alias_priority: int | None = _Unset, - validation_alias: str | None = None, + validation_alias: str | None = _Unset, serialization_alias: str | None = None, title: str | None = None, description: str | None = None, @@ -517,7 +522,7 @@ def __init__( alias_priority: int | None = _Unset, # MAINTENANCE: update when deprecating Pydantic v1, import these types # str | AliasPath | AliasChoices | None - validation_alias: str | None = None, + validation_alias: str | None = _Unset, serialization_alias: str | None = None, convert_underscores: bool = True, title: str | None = None, @@ -667,7 +672,7 @@ def __init__( alias_priority: int | None = _Unset, # MAINTENANCE: update when deprecating Pydantic v1, import these types # str | AliasPath | AliasChoices | None - validation_alias: str | None = None, + validation_alias: str | None = _Unset, serialization_alias: str | None = None, title: str | None = None, description: str | None = None, @@ -718,6 +723,11 @@ def __init__( kwargs["examples"] = examples current_json_schema_extra = json_schema_extra or extra + # Pydantic 2.12+ no longer copies alias to validation_alias automatically + # Set validation_alias to alias when not explicitly provided + if validation_alias is _Unset and alias is not None: + validation_alias = alias + kwargs.update( { "annotation": annotation, @@ -754,7 +764,7 @@ def __init__( alias_priority: int | None = _Unset, # MAINTENANCE: update when deprecating Pydantic v1, import these types # str | AliasPath | AliasChoices | None - validation_alias: str | None = None, + validation_alias: str | None = _Unset, serialization_alias: str | None = None, title: str | None = None, description: str | None = None, diff --git a/tests/functional/event_handler/_pydantic/test_openapi_params.py b/tests/functional/event_handler/_pydantic/test_openapi_params.py index 4c9087fff13..108a38b8377 100644 --- a/tests/functional/event_handler/_pydantic/test_openapi_params.py +++ b/tests/functional/event_handler/_pydantic/test_openapi_params.py @@ -1228,3 +1228,171 @@ def list_items(limit: Annotated[constrained_int, Query()] = 10): assert limit_param.schema_.type == "integer" assert limit_param.schema_.default == 10 assert limit_param.required is False + + +def test_query_alias_sets_validation_alias_automatically(): + """ + Test for issue #7552: When alias is set but validation_alias is not, + validation_alias should be automatically set to alias value. + This ensures compatibility with Pydantic 2.12+. + """ + from annotated_types import Ge, Le + from pydantic import StringConstraints + + # GIVEN an APIGatewayRestResolver with validation enabled + app = APIGatewayRestResolver(enable_validation=True) + + # AND constrained types using annotated_types + IntQuery = Annotated[int, Ge(1), Le(100)] + StrQuery = Annotated[str, StringConstraints(min_length=4, max_length=128)] + + @app.get("/foo") + def get_foo( + str_query: Annotated[StrQuery, Query(alias="strQuery")], + int_query: Annotated[IntQuery, Query(alias="intQuery")], + ): + return {"int_query": int_query, "str_query": str_query} + + # WHEN sending a request with aliased query parameters + event = { + "httpMethod": "GET", + "path": "/foo", + "queryStringParameters": { + "intQuery": "20", + "strQuery": "fooBarFizzBuzz", + }, + } + + # THEN the request should succeed with correct values + result = app(event, {}) + assert result["statusCode"] == 200 + body = json.loads(result["body"]) + assert body["int_query"] == 20 + assert body["str_query"] == "fooBarFizzBuzz" + + +def test_query_alias_with_multivalue_query_string_parameters(): + """ + Test for issue #7552: Ensure alias works with multiValueQueryStringParameters. + """ + from annotated_types import Ge, Le + from pydantic import StringConstraints + + # GIVEN an APIGatewayRestResolver with validation enabled + app = APIGatewayRestResolver(enable_validation=True) + + IntQuery = Annotated[int, Ge(1), Le(100)] + StrQuery = Annotated[str, StringConstraints(min_length=4, max_length=128)] + + @app.get("/foo") + def get_foo( + str_query: Annotated[StrQuery, Query(alias="strQuery")], + int_query: Annotated[IntQuery, Query(alias="intQuery")], + ): + return {"int_query": int_query, "str_query": str_query} + + # WHEN sending a request with multiValueQueryStringParameters + event = { + "httpMethod": "GET", + "path": "/foo", + "multiValueQueryStringParameters": { + "intQuery": ["20"], + "strQuery": ["fooBarFizzBuzz"], + }, + } + + # THEN the request should succeed + result = app(event, {}) + assert result["statusCode"] == 200 + body = json.loads(result["body"]) + assert body["int_query"] == 20 + assert body["str_query"] == "fooBarFizzBuzz" + + +def test_query_explicit_validation_alias_takes_precedence(): + """ + Test that explicitly set validation_alias is preserved and not overwritten by alias. + The alias is used by Powertools to extract the value from the request, + while validation_alias is used by Pydantic for internal validation. + """ + # GIVEN an APIGatewayRestResolver with validation enabled + app = APIGatewayRestResolver(enable_validation=True) + + @app.get("/foo") + def get_foo( + my_param: Annotated[str, Query(alias="aliasName", validation_alias="validationAliasName")], + ): + return {"my_param": my_param} + + # WHEN sending a request with the alias name (used by Powertools to extract value) + event = { + "httpMethod": "GET", + "path": "/foo", + "queryStringParameters": { + "aliasName": "test_value", + }, + } + + # THEN the request should succeed using alias for extraction + result = app(event, {}) + assert result["statusCode"] == 200 + body = json.loads(result["body"]) + assert body["my_param"] == "test_value" + + +def test_header_alias_sets_validation_alias_automatically(): + """ + Test for issue #7552: Header alias should also set validation_alias automatically. + """ + # GIVEN an APIGatewayRestResolver with validation enabled + app = APIGatewayRestResolver(enable_validation=True) + + @app.get("/foo") + def get_foo( + custom_header: Annotated[str, Header(alias="X-Custom-Header")], + ): + return {"custom_header": custom_header} + + # WHEN sending a request with the aliased header + event = { + "httpMethod": "GET", + "path": "/foo", + "headers": { + "X-Custom-Header": "header_value", + }, + } + + # THEN the request should succeed + result = app(event, {}) + assert result["statusCode"] == 200 + body = json.loads(result["body"]) + assert body["custom_header"] == "header_value" + + +def test_query_without_alias_works_normally(): + """ + Test that Query without alias continues to work normally. + """ + # GIVEN an APIGatewayRestResolver with validation enabled + app = APIGatewayRestResolver(enable_validation=True) + + @app.get("/foo") + def get_foo( + my_param: Annotated[str, Query()], + ): + return {"my_param": my_param} + + # WHEN sending a request with the parameter name + event = { + "httpMethod": "GET", + "path": "/foo", + "queryStringParameters": { + "my_param": "test_value", + }, + } + + # THEN the request should succeed + result = app(event, {}) + assert result["statusCode"] == 200 + body = json.loads(result["body"]) + assert body["my_param"] == "test_value" From 84954f89f183d3877046da2f13763fce4e3ed260 Mon Sep 17 00:00:00 2001 From: Leandro Damascena Date: Mon, 5 Jan 2026 14:52:55 +0000 Subject: [PATCH 2/4] fix: add support for Pydantic 2.12+ --- .../event_handler/openapi/params.py | 10 ++++- .../_pydantic/test_openapi_params.py | 40 +++++++++++++++++++ 2 files changed, 48 insertions(+), 2 deletions(-) diff --git a/aws_lambda_powertools/event_handler/openapi/params.py b/aws_lambda_powertools/event_handler/openapi/params.py index 9aa719408a9..04a971f3427 100644 --- a/aws_lambda_powertools/event_handler/openapi/params.py +++ b/aws_lambda_powertools/event_handler/openapi/params.py @@ -218,9 +218,12 @@ def __init__( self.openapi_examples = openapi_examples # Pydantic 2.12+ no longer copies alias to validation_alias automatically - # Set validation_alias to alias when not explicitly provided + # Ensure alias and validation_alias are in sync when only one is provided if validation_alias is _Unset and alias is not None: validation_alias = alias + elif alias is None and validation_alias is not _Unset and validation_alias is not None: + alias = validation_alias + kwargs["alias"] = alias kwargs.update( { @@ -724,9 +727,12 @@ def __init__( current_json_schema_extra = json_schema_extra or extra # Pydantic 2.12+ no longer copies alias to validation_alias automatically - # Set validation_alias to alias when not explicitly provided + # Ensure alias and validation_alias are in sync when only one is provided if validation_alias is _Unset and alias is not None: validation_alias = alias + elif alias is None and validation_alias is not _Unset and validation_alias is not None: + alias = validation_alias + kwargs["alias"] = alias kwargs.update( { diff --git a/tests/functional/event_handler/_pydantic/test_openapi_params.py b/tests/functional/event_handler/_pydantic/test_openapi_params.py index 108a38b8377..ca36504d0ed 100644 --- a/tests/functional/event_handler/_pydantic/test_openapi_params.py +++ b/tests/functional/event_handler/_pydantic/test_openapi_params.py @@ -1396,3 +1396,43 @@ def get_foo( assert result["statusCode"] == 200 body = json.loads(result["body"]) assert body["my_param"] == "test_value" + + +def test_query_validation_alias_only_sets_alias_automatically(): + """ + Test for issue #7552: When only validation_alias is set (without alias), + alias should be automatically set to validation_alias value. + This ensures the middleware can find the parameter in the request. + """ + from annotated_types import Ge, Le + from pydantic import StringConstraints + + # GIVEN an APIGatewayRestResolver with validation enabled + app = APIGatewayRestResolver(enable_validation=True) + + IntQuery = Annotated[int, Ge(1), Le(100)] + StrQuery = Annotated[str, StringConstraints(min_length=4, max_length=128)] + + @app.get("/foo") + def get_foo( + str_query: Annotated[StrQuery, Query(validation_alias="strQuery")], + int_query: Annotated[IntQuery, Query(validation_alias="intQuery")], + ): + return {"int_query": int_query, "str_query": str_query} + + # WHEN sending a request with validation_alias names + event = { + "httpMethod": "GET", + "path": "/foo", + "queryStringParameters": { + "intQuery": "20", + "strQuery": "fooBarFizzBuzz", + }, + } + + # THEN the request should succeed + result = app(event, {}) + assert result["statusCode"] == 200 + body = json.loads(result["body"]) + assert body["int_query"] == 20 + assert body["str_query"] == "fooBarFizzBuzz" From 62bea76a95812da90e99200bdadc231f67cd4de3 Mon Sep 17 00:00:00 2001 From: Leandro Damascena Date: Mon, 5 Jan 2026 14:57:52 +0000 Subject: [PATCH 3/4] fix: add support for Pydantic 2.12+ --- .../event_handler/_pydantic/test_openapi_params.py | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/tests/functional/event_handler/_pydantic/test_openapi_params.py b/tests/functional/event_handler/_pydantic/test_openapi_params.py index ca36504d0ed..749ce6af630 100644 --- a/tests/functional/event_handler/_pydantic/test_openapi_params.py +++ b/tests/functional/event_handler/_pydantic/test_openapi_params.py @@ -1232,7 +1232,7 @@ def list_items(limit: Annotated[constrained_int, Query()] = 10): def test_query_alias_sets_validation_alias_automatically(): """ - Test for issue #7552: When alias is set but validation_alias is not, + When alias is set but validation_alias is not, validation_alias should be automatically set to alias value. This ensures compatibility with Pydantic 2.12+. """ @@ -1273,7 +1273,7 @@ def get_foo( def test_query_alias_with_multivalue_query_string_parameters(): """ - Test for issue #7552: Ensure alias works with multiValueQueryStringParameters. + Ensure alias works with multiValueQueryStringParameters. """ from annotated_types import Ge, Le from pydantic import StringConstraints @@ -1311,7 +1311,7 @@ def get_foo( def test_query_explicit_validation_alias_takes_precedence(): """ - Test that explicitly set validation_alias is preserved and not overwritten by alias. + Explicitly set validation_alias is preserved and not overwritten by alias. The alias is used by Powertools to extract the value from the request, while validation_alias is used by Pydantic for internal validation. """ @@ -1342,7 +1342,7 @@ def get_foo( def test_header_alias_sets_validation_alias_automatically(): """ - Test for issue #7552: Header alias should also set validation_alias automatically. + Header alias should also set validation_alias automatically. """ # GIVEN an APIGatewayRestResolver with validation enabled app = APIGatewayRestResolver(enable_validation=True) @@ -1371,7 +1371,7 @@ def get_foo( def test_query_without_alias_works_normally(): """ - Test that Query without alias continues to work normally. + Query without alias continues to work normally. """ # GIVEN an APIGatewayRestResolver with validation enabled app = APIGatewayRestResolver(enable_validation=True) @@ -1400,7 +1400,7 @@ def get_foo( def test_query_validation_alias_only_sets_alias_automatically(): """ - Test for issue #7552: When only validation_alias is set (without alias), + When only validation_alias is set (without alias), alias should be automatically set to validation_alias value. This ensures the middleware can find the parameter in the request. """ From fc9875bbad37a544a8d4cb0a53c9f44c6b6e65bc Mon Sep 17 00:00:00 2001 From: Leandro Damascena Date: Mon, 5 Jan 2026 15:17:23 +0000 Subject: [PATCH 4/4] fix: add support for Pydantic 2.12+ --- .../_pydantic/test_openapi_params.py | 56 +++++++++++++++++++ 1 file changed, 56 insertions(+) diff --git a/tests/functional/event_handler/_pydantic/test_openapi_params.py b/tests/functional/event_handler/_pydantic/test_openapi_params.py index 749ce6af630..c50853a2b67 100644 --- a/tests/functional/event_handler/_pydantic/test_openapi_params.py +++ b/tests/functional/event_handler/_pydantic/test_openapi_params.py @@ -1436,3 +1436,59 @@ def get_foo( body = json.loads(result["body"]) assert body["int_query"] == 20 assert body["str_query"] == "fooBarFizzBuzz" + + +def test_body_alias_sets_validation_alias_automatically(): + """ + When alias is set but validation_alias is not in Body, + validation_alias should be automatically set to alias value. + """ + # GIVEN an APIGatewayRestResolver with validation enabled + app = APIGatewayRestResolver(enable_validation=True) + + @app.post("/foo") + def post_foo( + my_body: Annotated[str, Body(alias="myBody")], + ): + return {"my_body": my_body} + + # WHEN sending a request with body + event = { + "httpMethod": "POST", + "path": "/foo", + "body": '"test_value"', + } + + # THEN the request should succeed + result = app(event, {}) + assert result["statusCode"] == 200 + body = json.loads(result["body"]) + assert body["my_body"] == "test_value" + + +def test_body_validation_alias_only_sets_alias_automatically(): + """ + When only validation_alias is set (without alias) in Body, + alias should be automatically set to validation_alias value. + """ + # GIVEN an APIGatewayRestResolver with validation enabled + app = APIGatewayRestResolver(enable_validation=True) + + @app.post("/foo") + def post_foo( + my_body: Annotated[str, Body(validation_alias="myBody")], + ): + return {"my_body": my_body} + + # WHEN sending a request with body + event = { + "httpMethod": "POST", + "path": "/foo", + "body": '"test_value"', + } + + # THEN the request should succeed + result = app(event, {}) + assert result["statusCode"] == 200 + body = json.loads(result["body"]) + assert body["my_body"] == "test_value"