Skip to content

Commit d89f1f9

Browse files
feat(event_handler): enable support for custom deserializer to parse the request body (#6601)
* allowed a new deserializer for parser the body * Change the name + add examples * Change the name + add examples * Change the name + add examples --------- Co-authored-by: Leandro Damascena <[email protected]>
1 parent 818ad49 commit d89f1f9

File tree

8 files changed

+86
-9
lines changed

8 files changed

+86
-9
lines changed

.github/workflows/quality_check.yml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -44,7 +44,7 @@ jobs:
4444
quality_check:
4545
runs-on: ubuntu-latest
4646
strategy:
47-
max-parallel: 4
47+
max-parallel: 5
4848
matrix:
4949
python-version: ["3.9","3.10","3.11","3.12","3.13"]
5050
env:

aws_lambda_powertools/event_handler/api_gateway.py

Lines changed: 18 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1579,6 +1579,7 @@ def __init__(
15791579
strip_prefixes: list[str | Pattern] | None = None,
15801580
enable_validation: bool = False,
15811581
response_validation_error_http_code: HTTPStatus | int | None = None,
1582+
json_body_deserializer: Callable[[str], dict] | None = None,
15821583
):
15831584
"""
15841585
Parameters
@@ -1600,6 +1601,9 @@ def __init__(
16001601
Enables validation of the request body against the route schema, by default False.
16011602
response_validation_error_http_code
16021603
Sets the returned status code if response is not validated. enable_validation must be True.
1604+
json_body_deserializer: Callable[[str], dict], optional
1605+
function to deserialize `str`, `bytes`, `bytearray` containing a JSON document to a Python `dict`,
1606+
by default json.loads when integrating with EventSource data class
16031607
"""
16041608
self._proxy_type = proxy_type
16051609
self._dynamic_routes: list[Route] = []
@@ -1625,6 +1629,7 @@ def __init__(
16251629

16261630
# Allow for a custom serializer or a concise json serialization
16271631
self._serializer = serializer or partial(json.dumps, separators=(",", ":"), cls=Encoder)
1632+
self._json_body_deserializer = json_body_deserializer
16281633

16291634
if self._enable_validation:
16301635
from aws_lambda_powertools.event_handler.middlewares.openapi_validation import OpenAPIValidationMiddleware
@@ -2436,24 +2441,24 @@ def _to_proxy_event(self, event: dict) -> BaseProxyEvent: # noqa: PLR0911 # ig
24362441
"""Convert the event dict to the corresponding data class"""
24372442
if self._proxy_type == ProxyEventType.APIGatewayProxyEvent:
24382443
logger.debug("Converting event to API Gateway REST API contract")
2439-
return APIGatewayProxyEvent(event)
2444+
return APIGatewayProxyEvent(event, self._json_body_deserializer)
24402445
if self._proxy_type == ProxyEventType.APIGatewayProxyEventV2:
24412446
logger.debug("Converting event to API Gateway HTTP API contract")
2442-
return APIGatewayProxyEventV2(event)
2447+
return APIGatewayProxyEventV2(event, self._json_body_deserializer)
24432448
if self._proxy_type == ProxyEventType.BedrockAgentEvent:
24442449
logger.debug("Converting event to Bedrock Agent contract")
2445-
return BedrockAgentEvent(event)
2450+
return BedrockAgentEvent(event, self._json_body_deserializer)
24462451
if self._proxy_type == ProxyEventType.LambdaFunctionUrlEvent:
24472452
logger.debug("Converting event to Lambda Function URL contract")
2448-
return LambdaFunctionUrlEvent(event)
2453+
return LambdaFunctionUrlEvent(event, self._json_body_deserializer)
24492454
if self._proxy_type == ProxyEventType.VPCLatticeEvent:
24502455
logger.debug("Converting event to VPC Lattice contract")
2451-
return VPCLatticeEvent(event)
2456+
return VPCLatticeEvent(event, self._json_body_deserializer)
24522457
if self._proxy_type == ProxyEventType.VPCLatticeEventV2:
24532458
logger.debug("Converting event to VPC LatticeV2 contract")
2454-
return VPCLatticeEventV2(event)
2459+
return VPCLatticeEventV2(event, self._json_body_deserializer)
24552460
logger.debug("Converting event to ALB contract")
2456-
return ALBEvent(event)
2461+
return ALBEvent(event, self._json_body_deserializer)
24572462

24582463
def _resolve(self) -> ResponseBuilder:
24592464
"""Resolves the response or return the not found response"""
@@ -2870,6 +2875,7 @@ def __init__(
28702875
strip_prefixes: list[str | Pattern] | None = None,
28712876
enable_validation: bool = False,
28722877
response_validation_error_http_code: HTTPStatus | int | None = None,
2878+
json_body_deserializer: Callable[[str], dict] | None = None,
28732879
):
28742880
"""Amazon API Gateway REST and HTTP API v1 payload resolver"""
28752881
super().__init__(
@@ -2880,6 +2886,7 @@ def __init__(
28802886
strip_prefixes,
28812887
enable_validation,
28822888
response_validation_error_http_code,
2889+
json_body_deserializer=json_body_deserializer,
28832890
)
28842891

28852892
def _get_base_path(self) -> str:
@@ -2956,6 +2963,7 @@ def __init__(
29562963
strip_prefixes: list[str | Pattern] | None = None,
29572964
enable_validation: bool = False,
29582965
response_validation_error_http_code: HTTPStatus | int | None = None,
2966+
json_body_deserializer: Callable[[str], dict] | None = None,
29592967
):
29602968
"""Amazon API Gateway HTTP API v2 payload resolver"""
29612969
super().__init__(
@@ -2966,6 +2974,7 @@ def __init__(
29662974
strip_prefixes,
29672975
enable_validation,
29682976
response_validation_error_http_code,
2977+
json_body_deserializer=json_body_deserializer,
29692978
)
29702979

29712980
def _get_base_path(self) -> str:
@@ -2995,6 +3004,7 @@ def __init__(
29953004
strip_prefixes: list[str | Pattern] | None = None,
29963005
enable_validation: bool = False,
29973006
response_validation_error_http_code: HTTPStatus | int | None = None,
3007+
json_body_deserializer: Callable[[str], dict] | None = None,
29983008
):
29993009
"""Amazon Application Load Balancer (ALB) resolver"""
30003010
super().__init__(
@@ -3005,6 +3015,7 @@ def __init__(
30053015
strip_prefixes,
30063016
enable_validation,
30073017
response_validation_error_http_code,
3018+
json_body_deserializer=json_body_deserializer,
30083019
)
30093020

30103021
def _get_base_path(self) -> str:

aws_lambda_powertools/event_handler/bedrock_agent.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -103,6 +103,7 @@ def __init__(self, debug: bool = False, enable_validation: bool = True):
103103
serializer=None,
104104
strip_prefixes=None,
105105
enable_validation=enable_validation,
106+
json_body_deserializer=None,
106107
)
107108
self._response_builder_class = BedrockResponseBuilder
108109

aws_lambda_powertools/event_handler/lambda_function_url.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -61,6 +61,7 @@ def __init__(
6161
strip_prefixes: list[str | Pattern] | None = None,
6262
enable_validation: bool = False,
6363
response_validation_error_http_code: HTTPStatus | int | None = None,
64+
json_body_deserializer: Callable[[str], dict] | None = None,
6465
):
6566
super().__init__(
6667
ProxyEventType.LambdaFunctionUrlEvent,
@@ -70,6 +71,7 @@ def __init__(
7071
strip_prefixes,
7172
enable_validation,
7273
response_validation_error_http_code,
74+
json_body_deserializer=json_body_deserializer,
7375
)
7476

7577
def _get_base_path(self) -> str:

aws_lambda_powertools/event_handler/vpc_lattice.py

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -57,6 +57,7 @@ def __init__(
5757
strip_prefixes: list[str | Pattern] | None = None,
5858
enable_validation: bool = False,
5959
response_validation_error_http_code: HTTPStatus | int | None = None,
60+
json_body_deserializer: Callable[[str], dict] | None = None,
6061
):
6162
"""Amazon VPC Lattice resolver"""
6263
super().__init__(
@@ -67,6 +68,7 @@ def __init__(
6768
strip_prefixes,
6869
enable_validation,
6970
response_validation_error_http_code,
71+
json_body_deserializer=json_body_deserializer,
7072
)
7173

7274
def _get_base_path(self) -> str:
@@ -115,6 +117,7 @@ def __init__(
115117
strip_prefixes: list[str | Pattern] | None = None,
116118
enable_validation: bool = False,
117119
response_validation_error_http_code: HTTPStatus | int | None = None,
120+
json_body_deserializer: Callable[[str], dict] | None = None,
118121
):
119122
"""Amazon VPC Lattice resolver"""
120123
super().__init__(
@@ -125,6 +128,7 @@ def __init__(
125128
strip_prefixes,
126129
enable_validation,
127130
response_validation_error_http_code,
131+
json_body_deserializer=json_body_deserializer,
128132
)
129133

130134
def _get_base_path(self) -> str:

docs/core/event_handler/api_gateway.md

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1182,6 +1182,14 @@ You can instruct event handler to use a custom serializer to best suit your need
11821182
--8<-- "examples/event_handler_rest/src/custom_serializer.py"
11831183
```
11841184

1185+
### Custom body deserializer
1186+
1187+
You can customize how the integrated [Event Source Data Classes](https://docs.powertools.aws.dev/lambda/python/latest/utilities/data_classes/#api-gateway-proxy) parse the JSON request body by providing your own deserializer function. By default it is `json.loads`
1188+
1189+
```python hl_lines="15" title="Using a custom JSON deserializer for body"
1190+
--8<-- "examples/event_handler_rest/src/custom_json_deserializer.py"
1191+
```
1192+
11851193
### Split routes with Router
11861194

11871195
As you grow the number of routes a given Lambda function should handle, it is natural to either break into smaller Lambda functions, or split routes into separate files to ease maintenance - that's where the `Router` feature is useful.
Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
import json
2+
from decimal import Decimal
3+
from functools import partial
4+
5+
from aws_lambda_powertools import Logger, Tracer
6+
from aws_lambda_powertools.event_handler import APIGatewayRestResolver
7+
from aws_lambda_powertools.logging import correlation_paths
8+
from aws_lambda_powertools.utilities.typing import LambdaContext
9+
10+
tracer = Tracer()
11+
logger = Logger()
12+
app = APIGatewayRestResolver()
13+
14+
15+
app = APIGatewayRestResolver(json_body_deserializer=partial(json.loads, parse_float=Decimal))
16+
17+
18+
@app.get("/body")
19+
def get_body():
20+
return app.current_event.json_body
21+
22+
23+
# You can continue to use other utilities just as before
24+
@logger.inject_lambda_context(correlation_id_path=correlation_paths.API_GATEWAY_REST)
25+
@tracer.capture_lambda_handler
26+
def lambda_handler(event: dict, context: LambdaContext) -> dict:
27+
return app.resolve(event, context)

tests/functional/event_handler/required_dependencies/test_api_gateway.py

Lines changed: 25 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,12 +8,15 @@
88
from copy import deepcopy
99
from decimal import Decimal
1010
from enum import Enum
11+
from functools import partial
1112
from json import JSONEncoder
1213
from pathlib import Path
1314

1415
import pytest
1516

16-
from aws_lambda_powertools.event_handler import content_types
17+
from aws_lambda_powertools.event_handler import (
18+
content_types,
19+
)
1720
from aws_lambda_powertools.event_handler.api_gateway import (
1821
ALBResolver,
1922
APIGatewayHttpResolver,
@@ -1968,3 +1971,24 @@ def opa():
19681971
# THEN body should be converted to an empty string
19691972
assert result["statusCode"] == 200
19701973
assert result["body"] == ""
1974+
1975+
1976+
def test_api_gateway_resolver_with_custom_deserializer():
1977+
# GIVEN a basic API Gateway resolver
1978+
app = ApiGatewayResolver(json_body_deserializer=partial(json.loads, parse_float=Decimal))
1979+
1980+
@app.post("/my/path")
1981+
def test_handler():
1982+
return app.current_event.json_body
1983+
1984+
# WHEN calling the event handler
1985+
event = {}
1986+
event.update(LOAD_GW_EVENT)
1987+
event["body"] = '{"amount": 2.2999999999999998}'
1988+
event["httpMethod"] = "POST"
1989+
1990+
result = app(event, {})
1991+
# THEN process event correctly
1992+
assert result["statusCode"] == 200
1993+
assert result["multiValueHeaders"]["Content-Type"] == [content_types.APPLICATION_JSON]
1994+
assert result["body"] == '{"amount":"2.2999999999999998"}'

0 commit comments

Comments
 (0)