Skip to content

Commit d548350

Browse files
fix: add support for Pydantic 2.12+
1 parent edf150a commit d548350

File tree

2 files changed

+184
-6
lines changed

2 files changed

+184
-6
lines changed

aws_lambda_powertools/event_handler/openapi/params.py

Lines changed: 16 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -103,7 +103,7 @@ def __init__(
103103
alias_priority: int | None = _Unset,
104104
# MAINTENANCE: update when deprecating Pydantic v1, import these types
105105
# MAINTENANCE: validation_alias: str | AliasPath | AliasChoices | None
106-
validation_alias: str | None = None,
106+
validation_alias: str | None = _Unset,
107107
serialization_alias: str | None = None,
108108
title: str | None = None,
109109
description: str | None = None,
@@ -217,6 +217,11 @@ def __init__(
217217

218218
self.openapi_examples = openapi_examples
219219

220+
# Pydantic 2.12+ no longer copies alias to validation_alias automatically
221+
# Set validation_alias to alias when not explicitly provided
222+
if validation_alias is _Unset and alias is not None:
223+
validation_alias = alias
224+
220225
kwargs.update(
221226
{
222227
"annotation": annotation,
@@ -254,7 +259,7 @@ def __init__(
254259
alias_priority: int | None = _Unset,
255260
# MAINTENANCE: update when deprecating Pydantic v1, import these types
256261
# MAINTENANCE: validation_alias: str | AliasPath | AliasChoices | None
257-
validation_alias: str | None = None,
262+
validation_alias: str | None = _Unset,
258263
serialization_alias: str | None = None,
259264
title: str | None = None,
260265
description: str | None = None,
@@ -386,7 +391,7 @@ def __init__(
386391
annotation: Any | None = None,
387392
alias: str | None = None,
388393
alias_priority: int | None = _Unset,
389-
validation_alias: str | None = None,
394+
validation_alias: str | None = _Unset,
390395
serialization_alias: str | None = None,
391396
title: str | None = None,
392397
description: str | None = None,
@@ -517,7 +522,7 @@ def __init__(
517522
alias_priority: int | None = _Unset,
518523
# MAINTENANCE: update when deprecating Pydantic v1, import these types
519524
# str | AliasPath | AliasChoices | None
520-
validation_alias: str | None = None,
525+
validation_alias: str | None = _Unset,
521526
serialization_alias: str | None = None,
522527
convert_underscores: bool = True,
523528
title: str | None = None,
@@ -667,7 +672,7 @@ def __init__(
667672
alias_priority: int | None = _Unset,
668673
# MAINTENANCE: update when deprecating Pydantic v1, import these types
669674
# str | AliasPath | AliasChoices | None
670-
validation_alias: str | None = None,
675+
validation_alias: str | None = _Unset,
671676
serialization_alias: str | None = None,
672677
title: str | None = None,
673678
description: str | None = None,
@@ -718,6 +723,11 @@ def __init__(
718723
kwargs["examples"] = examples
719724
current_json_schema_extra = json_schema_extra or extra
720725

726+
# Pydantic 2.12+ no longer copies alias to validation_alias automatically
727+
# Set validation_alias to alias when not explicitly provided
728+
if validation_alias is _Unset and alias is not None:
729+
validation_alias = alias
730+
721731
kwargs.update(
722732
{
723733
"annotation": annotation,
@@ -754,7 +764,7 @@ def __init__(
754764
alias_priority: int | None = _Unset,
755765
# MAINTENANCE: update when deprecating Pydantic v1, import these types
756766
# str | AliasPath | AliasChoices | None
757-
validation_alias: str | None = None,
767+
validation_alias: str | None = _Unset,
758768
serialization_alias: str | None = None,
759769
title: str | None = None,
760770
description: str | None = None,

tests/functional/event_handler/_pydantic/test_openapi_params.py

Lines changed: 168 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1228,3 +1228,171 @@ def list_items(limit: Annotated[constrained_int, Query()] = 10):
12281228
assert limit_param.schema_.type == "integer"
12291229
assert limit_param.schema_.default == 10
12301230
assert limit_param.required is False
1231+
1232+
1233+
def test_query_alias_sets_validation_alias_automatically():
1234+
"""
1235+
Test for issue #7552: When alias is set but validation_alias is not,
1236+
validation_alias should be automatically set to alias value.
1237+
This ensures compatibility with Pydantic 2.12+.
1238+
"""
1239+
from annotated_types import Ge, Le
1240+
from pydantic import StringConstraints
1241+
1242+
# GIVEN an APIGatewayRestResolver with validation enabled
1243+
app = APIGatewayRestResolver(enable_validation=True)
1244+
1245+
# AND constrained types using annotated_types
1246+
IntQuery = Annotated[int, Ge(1), Le(100)]
1247+
StrQuery = Annotated[str, StringConstraints(min_length=4, max_length=128)]
1248+
1249+
@app.get("/foo")
1250+
def get_foo(
1251+
str_query: Annotated[StrQuery, Query(alias="strQuery")],
1252+
int_query: Annotated[IntQuery, Query(alias="intQuery")],
1253+
):
1254+
return {"int_query": int_query, "str_query": str_query}
1255+
1256+
# WHEN sending a request with aliased query parameters
1257+
event = {
1258+
"httpMethod": "GET",
1259+
"path": "/foo",
1260+
"queryStringParameters": {
1261+
"intQuery": "20",
1262+
"strQuery": "fooBarFizzBuzz",
1263+
},
1264+
}
1265+
1266+
# THEN the request should succeed with correct values
1267+
result = app(event, {})
1268+
assert result["statusCode"] == 200
1269+
body = json.loads(result["body"])
1270+
assert body["int_query"] == 20
1271+
assert body["str_query"] == "fooBarFizzBuzz"
1272+
1273+
1274+
def test_query_alias_with_multivalue_query_string_parameters():
1275+
"""
1276+
Test for issue #7552: Ensure alias works with multiValueQueryStringParameters.
1277+
"""
1278+
from annotated_types import Ge, Le
1279+
from pydantic import StringConstraints
1280+
1281+
# GIVEN an APIGatewayRestResolver with validation enabled
1282+
app = APIGatewayRestResolver(enable_validation=True)
1283+
1284+
IntQuery = Annotated[int, Ge(1), Le(100)]
1285+
StrQuery = Annotated[str, StringConstraints(min_length=4, max_length=128)]
1286+
1287+
@app.get("/foo")
1288+
def get_foo(
1289+
str_query: Annotated[StrQuery, Query(alias="strQuery")],
1290+
int_query: Annotated[IntQuery, Query(alias="intQuery")],
1291+
):
1292+
return {"int_query": int_query, "str_query": str_query}
1293+
1294+
# WHEN sending a request with multiValueQueryStringParameters
1295+
event = {
1296+
"httpMethod": "GET",
1297+
"path": "/foo",
1298+
"multiValueQueryStringParameters": {
1299+
"intQuery": ["20"],
1300+
"strQuery": ["fooBarFizzBuzz"],
1301+
},
1302+
}
1303+
1304+
# THEN the request should succeed
1305+
result = app(event, {})
1306+
assert result["statusCode"] == 200
1307+
body = json.loads(result["body"])
1308+
assert body["int_query"] == 20
1309+
assert body["str_query"] == "fooBarFizzBuzz"
1310+
1311+
1312+
def test_query_explicit_validation_alias_takes_precedence():
1313+
"""
1314+
Test that explicitly set validation_alias is preserved and not overwritten by alias.
1315+
The alias is used by Powertools to extract the value from the request,
1316+
while validation_alias is used by Pydantic for internal validation.
1317+
"""
1318+
# GIVEN an APIGatewayRestResolver with validation enabled
1319+
app = APIGatewayRestResolver(enable_validation=True)
1320+
1321+
@app.get("/foo")
1322+
def get_foo(
1323+
my_param: Annotated[str, Query(alias="aliasName", validation_alias="validationAliasName")],
1324+
):
1325+
return {"my_param": my_param}
1326+
1327+
# WHEN sending a request with the alias name (used by Powertools to extract value)
1328+
event = {
1329+
"httpMethod": "GET",
1330+
"path": "/foo",
1331+
"queryStringParameters": {
1332+
"aliasName": "test_value",
1333+
},
1334+
}
1335+
1336+
# THEN the request should succeed using alias for extraction
1337+
result = app(event, {})
1338+
assert result["statusCode"] == 200
1339+
body = json.loads(result["body"])
1340+
assert body["my_param"] == "test_value"
1341+
1342+
1343+
def test_header_alias_sets_validation_alias_automatically():
1344+
"""
1345+
Test for issue #7552: Header alias should also set validation_alias automatically.
1346+
"""
1347+
# GIVEN an APIGatewayRestResolver with validation enabled
1348+
app = APIGatewayRestResolver(enable_validation=True)
1349+
1350+
@app.get("/foo")
1351+
def get_foo(
1352+
custom_header: Annotated[str, Header(alias="X-Custom-Header")],
1353+
):
1354+
return {"custom_header": custom_header}
1355+
1356+
# WHEN sending a request with the aliased header
1357+
event = {
1358+
"httpMethod": "GET",
1359+
"path": "/foo",
1360+
"headers": {
1361+
"X-Custom-Header": "header_value",
1362+
},
1363+
}
1364+
1365+
# THEN the request should succeed
1366+
result = app(event, {})
1367+
assert result["statusCode"] == 200
1368+
body = json.loads(result["body"])
1369+
assert body["custom_header"] == "header_value"
1370+
1371+
1372+
def test_query_without_alias_works_normally():
1373+
"""
1374+
Test that Query without alias continues to work normally.
1375+
"""
1376+
# GIVEN an APIGatewayRestResolver with validation enabled
1377+
app = APIGatewayRestResolver(enable_validation=True)
1378+
1379+
@app.get("/foo")
1380+
def get_foo(
1381+
my_param: Annotated[str, Query()],
1382+
):
1383+
return {"my_param": my_param}
1384+
1385+
# WHEN sending a request with the parameter name
1386+
event = {
1387+
"httpMethod": "GET",
1388+
"path": "/foo",
1389+
"queryStringParameters": {
1390+
"my_param": "test_value",
1391+
},
1392+
}
1393+
1394+
# THEN the request should succeed
1395+
result = app(event, {})
1396+
assert result["statusCode"] == 200
1397+
body = json.loads(result["body"])
1398+
assert body["my_param"] == "test_value"

0 commit comments

Comments
 (0)