Skip to content

Commit 36a91b7

Browse files
feat(event-handler): add per-route validation support (#7965)
* feat(event-handler): add enable_validation parameter to Route class and decorators - Add enable_validation parameter to Route class constructor - Update all route decorators to accept enable_validation parameter - Modify _build_middleware_stack to check route-level validation setting - Route-level setting overrides resolver-level when explicitly set - Maintains backwards compatibility (None inherits from resolver) Addresses #6983 * feat(event-handler): add enable_validation support to BedrockAgentResolver - Update all HTTP method decorators to accept enable_validation parameter - Pass enable_validation to parent class methods - Ensures type safety and consistency across all resolvers Addresses #6983 * feat(event-handler): add per-route validation support to async middleware chain - Update _run_middleware_chain_async to check route-level validation - Conditionally add validation middlewares based on effective setting - Supports async/await pattern with per-route validation control Addresses #6983 * test(event-handler): add comprehensive tests for per-route validation - Test explicit route-level enable_validation=True - Test disabling validation on specific routes when globally enabled - Test request body and response validation with per-route settings - Test inheritance behavior and mixed validation scenarios - Test Pydantic v2 compatibility - All tests passing with full coverage Addresses #6983 * docs(event-handler): add per-route validation example - Demonstrate incremental validation adoption for monolithic Lambda - Show validated routes inheriting global setting - Show legacy routes with enable_validation=False - Practical example for migration scenarios Addresses #6983 * fix(test): correct type ignore placement for SonarCloud - Move type: ignore comment to function definition line - Properly suppress return type mismatch warning in test * test: revert to standard type ignore pattern for consistency Use same pattern as other validation tests in codebase * fix(test): use cast to satisfy SonarCloud type checking Use typing.cast instead of type: ignore comment to properly handle intentional type mismatch in validation error test. This satisfies both mypy and SonarCloud while maintaining test functionality. --------- Co-authored-by: Andrea Amorosi <dreamorosi@gmail.com>
1 parent ea5c094 commit 36a91b7

File tree

5 files changed

+460
-4
lines changed

5 files changed

+460
-4
lines changed

aws_lambda_powertools/event_handler/api_gateway.py

Lines changed: 31 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -379,6 +379,7 @@ def __init__(
379379
security: list[dict[str, list[str]]] | None = None,
380380
openapi_extensions: dict[str, Any] | None = None,
381381
deprecated: bool = False,
382+
enable_validation: bool | None = None,
382383
custom_response_validation_http_code: HTTPStatus | None = None,
383384
middlewares: list[Callable[..., Response]] | None = None,
384385
):
@@ -421,6 +422,8 @@ def __init__(
421422
Additional OpenAPI extensions as a dictionary.
422423
deprecated: bool
423424
Whether or not to mark this route as deprecated in the OpenAPI schema
425+
enable_validation: bool | None, optional
426+
Enable or disable validation for this specific route. If None, inherits from resolver setting.
424427
custom_response_validation_http_code: int | HTTPStatus | None, optional
425428
Whether to have custom http status code for this route if response validation fails
426429
middlewares: list[Callable[..., Response]] | None
@@ -450,6 +453,7 @@ def __init__(
450453
self.middlewares = middlewares or []
451454
self.operation_id = operation_id or self._generate_operation_id()
452455
self.deprecated = deprecated
456+
self.enable_validation = enable_validation
453457

454458
# _middleware_stack_built is used to ensure the middleware stack is only built once.
455459
self._middleware_stack_built = False
@@ -536,15 +540,21 @@ def _build_middleware_stack(self, router_middlewares: list[Callable[..., Any]],
536540

537541
all_middlewares = []
538542

543+
# Determine if validation should be enabled for this route
544+
# If route has explicit enable_validation setting, use it; otherwise, use resolver's global setting
545+
route_validation_enabled = (
546+
self.enable_validation if self.enable_validation is not None else app._enable_validation
547+
)
548+
539549
# Add request validation middleware first if validation is enabled
540-
if hasattr(app, "_request_validation_middleware"):
550+
if route_validation_enabled and hasattr(app, "_request_validation_middleware"):
541551
all_middlewares.append(app._request_validation_middleware)
542552

543553
# Add user middlewares in the middle
544554
all_middlewares.extend(router_middlewares + self.middlewares)
545555

546556
# Add response validation middleware before the route handler if validation is enabled
547-
if hasattr(app, "_response_validation_middleware"):
557+
if route_validation_enabled and hasattr(app, "_response_validation_middleware"):
548558
all_middlewares.append(app._response_validation_middleware)
549559

550560
logger.debug(f"Building middleware stack: {all_middlewares}")
@@ -1133,6 +1143,7 @@ def route(
11331143
security: list[dict[str, list[str]]] | None = None,
11341144
openapi_extensions: dict[str, Any] | None = None,
11351145
deprecated: bool = False,
1146+
enable_validation: bool | None = None,
11361147
custom_response_validation_http_code: int | HTTPStatus | None = None,
11371148
middlewares: list[Callable[..., Any]] | None = None,
11381149
) -> Callable[[AnyCallableT], AnyCallableT]:
@@ -1195,6 +1206,7 @@ def get(
11951206
security: list[dict[str, list[str]]] | None = None,
11961207
openapi_extensions: dict[str, Any] | None = None,
11971208
deprecated: bool = False,
1209+
enable_validation: bool | None = None,
11981210
custom_response_validation_http_code: int | HTTPStatus | None = None,
11991211
middlewares: list[Callable[..., Any]] | None = None,
12001212
) -> Callable[[AnyCallableT], AnyCallableT]:
@@ -1236,6 +1248,7 @@ def lambda_handler(event, context):
12361248
security,
12371249
openapi_extensions,
12381250
deprecated,
1251+
enable_validation,
12391252
custom_response_validation_http_code,
12401253
middlewares,
12411254
)
@@ -1256,6 +1269,7 @@ def post(
12561269
security: list[dict[str, list[str]]] | None = None,
12571270
openapi_extensions: dict[str, Any] | None = None,
12581271
deprecated: bool = False,
1272+
enable_validation: bool | None = None,
12591273
custom_response_validation_http_code: int | HTTPStatus | None = None,
12601274
middlewares: list[Callable[..., Any]] | None = None,
12611275
) -> Callable[[AnyCallableT], AnyCallableT]:
@@ -1298,6 +1312,7 @@ def lambda_handler(event, context):
12981312
security,
12991313
openapi_extensions,
13001314
deprecated,
1315+
enable_validation,
13011316
custom_response_validation_http_code,
13021317
middlewares,
13031318
)
@@ -1318,6 +1333,7 @@ def put(
13181333
security: list[dict[str, list[str]]] | None = None,
13191334
openapi_extensions: dict[str, Any] | None = None,
13201335
deprecated: bool = False,
1336+
enable_validation: bool | None = None,
13211337
custom_response_validation_http_code: int | HTTPStatus | None = None,
13221338
middlewares: list[Callable[..., Any]] | None = None,
13231339
) -> Callable[[AnyCallableT], AnyCallableT]:
@@ -1360,6 +1376,7 @@ def lambda_handler(event, context):
13601376
security,
13611377
openapi_extensions,
13621378
deprecated,
1379+
enable_validation,
13631380
custom_response_validation_http_code,
13641381
middlewares,
13651382
)
@@ -1380,6 +1397,7 @@ def delete(
13801397
security: list[dict[str, list[str]]] | None = None,
13811398
openapi_extensions: dict[str, Any] | None = None,
13821399
deprecated: bool = False,
1400+
enable_validation: bool | None = None,
13831401
custom_response_validation_http_code: int | HTTPStatus | None = None,
13841402
middlewares: list[Callable[..., Any]] | None = None,
13851403
) -> Callable[[AnyCallableT], AnyCallableT]:
@@ -1421,6 +1439,7 @@ def lambda_handler(event, context):
14211439
security,
14221440
openapi_extensions,
14231441
deprecated,
1442+
enable_validation,
14241443
custom_response_validation_http_code,
14251444
middlewares,
14261445
)
@@ -1441,6 +1460,7 @@ def patch(
14411460
security: list[dict[str, list[str]]] | None = None,
14421461
openapi_extensions: dict[str, Any] | None = None,
14431462
deprecated: bool = False,
1463+
enable_validation: bool | None = None,
14441464
custom_response_validation_http_code: int | HTTPStatus | None = None,
14451465
middlewares: list[Callable] | None = None,
14461466
) -> Callable[[AnyCallableT], AnyCallableT]:
@@ -1485,6 +1505,7 @@ def lambda_handler(event, context):
14851505
security,
14861506
openapi_extensions,
14871507
deprecated,
1508+
enable_validation,
14881509
custom_response_validation_http_code,
14891510
middlewares,
14901511
)
@@ -1505,6 +1526,7 @@ def head(
15051526
security: list[dict[str, list[str]]] | None = None,
15061527
openapi_extensions: dict[str, Any] | None = None,
15071528
deprecated: bool = False,
1529+
enable_validation: bool | None = None,
15081530
custom_response_validation_http_code: int | HTTPStatus | None = None,
15091531
middlewares: list[Callable] | None = None,
15101532
) -> Callable[[AnyCallableT], AnyCallableT]:
@@ -1548,6 +1570,7 @@ def lambda_handler(event, context):
15481570
security,
15491571
openapi_extensions,
15501572
deprecated,
1573+
enable_validation,
15511574
custom_response_validation_http_code,
15521575
middlewares,
15531576
)
@@ -2569,6 +2592,7 @@ def route(
25692592
security: list[dict[str, list[str]]] | None = None,
25702593
openapi_extensions: dict[str, Any] | None = None,
25712594
deprecated: bool = False,
2595+
enable_validation: bool | None = None,
25722596
custom_response_validation_http_code: int | HTTPStatus | None = None,
25732597
middlewares: list[Callable[..., Any]] | None = None,
25742598
) -> Callable[[AnyCallableT], AnyCallableT]:
@@ -2603,6 +2627,7 @@ def register_resolver(func: AnyCallableT) -> AnyCallableT:
26032627
security,
26042628
openapi_extensions,
26052629
deprecated,
2630+
enable_validation,
26062631
custom_response_validation_http_code,
26072632
middlewares,
26082633
)
@@ -3130,6 +3155,7 @@ def route(
31303155
security: list[dict[str, list[str]]] | None = None,
31313156
openapi_extensions: dict[str, Any] | None = None,
31323157
deprecated: bool = False,
3158+
enable_validation: bool | None = None,
31333159
custom_response_validation_http_code: int | HTTPStatus | None = None,
31343160
middlewares: list[Callable[..., Any]] | None = None,
31353161
) -> Callable[[AnyCallableT], AnyCallableT]:
@@ -3157,6 +3183,7 @@ def register_route(func: AnyCallableT) -> AnyCallableT:
31573183
frozen_security,
31583184
frozen_openapi_extensions,
31593185
deprecated,
3186+
enable_validation,
31603187
custom_response_validation_http_code,
31613188
)
31623189

@@ -3246,6 +3273,7 @@ def route(
32463273
security: list[dict[str, list[str]]] | None = None,
32473274
openapi_extensions: dict[str, Any] | None = None,
32483275
deprecated: bool = False,
3276+
enable_validation: bool | None = None,
32493277
custom_response_validation_http_code: int | HTTPStatus | None = None,
32503278
middlewares: list[Callable[..., Any]] | None = None,
32513279
) -> Callable[[AnyCallableT], AnyCallableT]:
@@ -3266,6 +3294,7 @@ def route(
32663294
security,
32673295
openapi_extensions,
32683296
deprecated,
3297+
enable_validation,
32693298
custom_response_validation_http_code,
32703299
middlewares,
32713300
)

aws_lambda_powertools/event_handler/bedrock_agent.py

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -124,6 +124,7 @@ def get( # type: ignore[override]
124124
include_in_schema: bool = True,
125125
openapi_extensions: dict[str, Any] | None = None,
126126
deprecated: bool = False,
127+
enable_validation: bool | None = None,
127128
custom_response_validation_http_code: int | HTTPStatus | None = None,
128129
middlewares: list[Callable[..., Any]] | None = None,
129130
) -> Callable[[Callable[..., Any]], Callable[..., Any]]:
@@ -144,6 +145,7 @@ def get( # type: ignore[override]
144145
security,
145146
openapi_extensions,
146147
deprecated,
148+
enable_validation,
147149
custom_response_validation_http_code,
148150
middlewares,
149151
)
@@ -165,6 +167,7 @@ def post( # type: ignore[override]
165167
include_in_schema: bool = True,
166168
openapi_extensions: dict[str, Any] | None = None,
167169
deprecated: bool = False,
170+
enable_validation: bool | None = None,
168171
custom_response_validation_http_code: int | HTTPStatus | None = None,
169172
middlewares: list[Callable[..., Any]] | None = None,
170173
):
@@ -185,6 +188,7 @@ def post( # type: ignore[override]
185188
security,
186189
openapi_extensions,
187190
deprecated,
191+
enable_validation,
188192
custom_response_validation_http_code,
189193
middlewares,
190194
)
@@ -206,6 +210,7 @@ def put( # type: ignore[override]
206210
include_in_schema: bool = True,
207211
openapi_extensions: dict[str, Any] | None = None,
208212
deprecated: bool = False,
213+
enable_validation: bool | None = None,
209214
custom_response_validation_http_code: int | HTTPStatus | None = None,
210215
middlewares: list[Callable[..., Any]] | None = None,
211216
):
@@ -226,6 +231,7 @@ def put( # type: ignore[override]
226231
security,
227232
openapi_extensions,
228233
deprecated,
234+
enable_validation,
229235
custom_response_validation_http_code,
230236
middlewares,
231237
)
@@ -247,6 +253,7 @@ def patch( # type: ignore[override]
247253
include_in_schema: bool = True,
248254
openapi_extensions: dict[str, Any] | None = None,
249255
deprecated: bool = False,
256+
enable_validation: bool | None = None,
250257
custom_response_validation_http_code: int | HTTPStatus | None = None,
251258
middlewares: list[Callable] | None = None,
252259
):
@@ -267,6 +274,7 @@ def patch( # type: ignore[override]
267274
security,
268275
openapi_extensions,
269276
deprecated,
277+
enable_validation,
270278
custom_response_validation_http_code,
271279
middlewares,
272280
)
@@ -288,6 +296,7 @@ def delete( # type: ignore[override]
288296
include_in_schema: bool = True,
289297
openapi_extensions: dict[str, Any] | None = None,
290298
deprecated: bool = False,
299+
enable_validation: bool | None = None,
291300
custom_response_validation_http_code: int | HTTPStatus | None = None,
292301
middlewares: list[Callable[..., Any]] | None = None,
293302
):
@@ -308,6 +317,7 @@ def delete( # type: ignore[override]
308317
security,
309318
openapi_extensions,
310319
deprecated,
320+
enable_validation,
311321
custom_response_validation_http_code,
312322
middlewares,
313323
)

aws_lambda_powertools/event_handler/http_resolver.py

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -290,12 +290,18 @@ async def _run_middleware_chain_async(self, route: Route) -> Response:
290290
# Build middleware list
291291
all_middlewares: list[Callable[..., Any]] = []
292292

293-
if hasattr(self, "_request_validation_middleware"):
293+
# Determine if validation should be enabled for this route
294+
# If route has explicit enable_validation setting, use it; otherwise, use resolver's global setting
295+
route_validation_enabled = (
296+
route.enable_validation if route.enable_validation is not None else self._enable_validation
297+
)
298+
299+
if route_validation_enabled and hasattr(self, "_request_validation_middleware"):
294300
all_middlewares.append(self._request_validation_middleware)
295301

296302
all_middlewares.extend(self._router_middlewares + route.middlewares)
297303

298-
if hasattr(self, "_response_validation_middleware"):
304+
if route_validation_enabled and hasattr(self, "_response_validation_middleware"):
299305
all_middlewares.append(self._response_validation_middleware)
300306

301307
# Create the final handler that calls the route function

0 commit comments

Comments
 (0)