Skip to content

Commit 1554019

Browse files
fix(event_handler): custom serializer recursive values when using data validation (#4664)
* fix(event_handler): custom serializer recursive values * fix: use custom serializers for custom sequence values * fix: use custom serializers for custom base model values * fix: use custom serializers for custom data classes values * fix: propagate custom serializer for any unmatched type for safety * fix(test): target pydantic v2 only in arbitrary type * Customer must handle serialization on Pydantic models --------- Co-authored-by: Leandro Damascena <[email protected]>
1 parent c7f9bb0 commit 1554019

File tree

3 files changed

+76
-3
lines changed

3 files changed

+76
-3
lines changed

Diff for: aws_lambda_powertools/event_handler/openapi/encoders.py

+18-2
Original file line numberDiff line numberDiff line change
@@ -94,6 +94,7 @@ def jsonable_encoder( # noqa: PLR0911
9494
exclude_unset=exclude_unset,
9595
exclude_defaults=exclude_defaults,
9696
exclude_none=exclude_none,
97+
custom_serializer=custom_serializer,
9798
)
9899

99100
# Enums
@@ -115,8 +116,9 @@ def jsonable_encoder( # noqa: PLR0911
115116
include=include,
116117
exclude=exclude,
117118
by_alias=by_alias,
118-
exclude_none=exclude_none,
119119
exclude_unset=exclude_unset,
120+
exclude_none=exclude_none,
121+
custom_serializer=custom_serializer,
120122
)
121123

122124
# Sequences
@@ -129,6 +131,7 @@ def jsonable_encoder( # noqa: PLR0911
129131
exclude_none=exclude_none,
130132
exclude_defaults=exclude_defaults,
131133
exclude_unset=exclude_unset,
134+
custom_serializer=custom_serializer,
132135
)
133136

134137
# Other types
@@ -152,6 +155,7 @@ def jsonable_encoder( # noqa: PLR0911
152155
exclude_none=exclude_none,
153156
exclude_unset=exclude_unset,
154157
exclude_defaults=exclude_defaults,
158+
custom_serializer=custom_serializer,
155159
)
156160
except ValueError as exc:
157161
raise SerializationError(
@@ -201,9 +205,15 @@ def _dump_dict(
201205
by_alias: bool = True,
202206
exclude_unset: bool = False,
203207
exclude_none: bool = False,
208+
custom_serializer: Optional[Callable[[Any], str]] = None,
204209
) -> Dict[str, Any]:
205210
"""
206211
Dump a dict to a dict, using the same parameters as jsonable_encoder
212+
213+
Parameters
214+
----------
215+
custom_serializer : Callable, optional
216+
A custom serializer to use for encoding the object, when everything else fails.
207217
"""
208218
encoded_dict = {}
209219
allowed_keys = set(obj.keys())
@@ -222,12 +232,14 @@ def _dump_dict(
222232
by_alias=by_alias,
223233
exclude_unset=exclude_unset,
224234
exclude_none=exclude_none,
235+
custom_serializer=custom_serializer,
225236
)
226237
encoded_value = jsonable_encoder(
227238
value,
228239
by_alias=by_alias,
229240
exclude_unset=exclude_unset,
230241
exclude_none=exclude_none,
242+
custom_serializer=custom_serializer,
231243
)
232244
encoded_dict[encoded_key] = encoded_value
233245
return encoded_dict
@@ -242,9 +254,10 @@ def _dump_sequence(
242254
exclude_unset: bool = False,
243255
exclude_none: bool = False,
244256
exclude_defaults: bool = False,
257+
custom_serializer: Optional[Callable[[Any], str]] = None,
245258
) -> List[Any]:
246259
"""
247-
Dump a sequence to a list, using the same parameters as jsonable_encoder
260+
Dump a sequence to a list, using the same parameters as jsonable_encoder.
248261
"""
249262
encoded_list = []
250263
for item in obj:
@@ -257,6 +270,7 @@ def _dump_sequence(
257270
exclude_unset=exclude_unset,
258271
exclude_defaults=exclude_defaults,
259272
exclude_none=exclude_none,
273+
custom_serializer=custom_serializer,
260274
),
261275
)
262276
return encoded_list
@@ -271,6 +285,7 @@ def _dump_other(
271285
exclude_unset: bool = False,
272286
exclude_none: bool = False,
273287
exclude_defaults: bool = False,
288+
custom_serializer: Optional[Callable[[Any], str]] = None,
274289
) -> Any:
275290
"""
276291
Dump an object to a hashable object, using the same parameters as jsonable_encoder
@@ -292,6 +307,7 @@ def _dump_other(
292307
exclude_unset=exclude_unset,
293308
exclude_defaults=exclude_defaults,
294309
exclude_none=exclude_none,
310+
custom_serializer=custom_serializer,
295311
)
296312

297313

Diff for: noxfile.py

+1-1
Original file line numberDiff line numberDiff line change
@@ -148,7 +148,7 @@ def test_with_aws_encryption_sdk_as_required_package(session: nox.Session):
148148

149149

150150
@nox.session()
151-
@nox.parametrize("pydantic", ["1.10", "2.0"])
151+
@nox.parametrize("pydantic", ["1.10,<2.0", "2.0"])
152152
def test_with_pydantic_required_package(session: nox.Session, pydantic: str):
153153
"""Tests that only depends for Pydantic library v1 and v2"""
154154
# Event Handler OpenAPI

Diff for: tests/functional/event_handler/_pydantic/test_openapi_encoders.py

+57
Original file line numberDiff line numberDiff line change
@@ -193,3 +193,60 @@ class MyClass:
193193

194194
with pytest.raises(SerializationError, match="Unable to serialize the object*"):
195195
jsonable_encoder(MyClass())
196+
197+
198+
def test_openapi_encode_custom_serializer_nested_dict():
199+
# GIVEN a nested dictionary with a custom class
200+
class CustomClass: ...
201+
202+
nested_dict = {"a": {"b": CustomClass()}}
203+
204+
# AND a custom serializer
205+
def serializer(value):
206+
return "serialized"
207+
208+
# WHEN we call jsonable_encoder with the nested dictionary and unserializable value
209+
result = jsonable_encoder(nested_dict, custom_serializer=serializer)
210+
211+
# THEN we should get the custom serializer output
212+
assert result == {"a": {"b": "serialized"}}
213+
214+
215+
def test_openapi_encode_custom_serializer_sequences():
216+
# GIVEN a sequence with a custom class
217+
class CustomClass:
218+
__slots__ = []
219+
220+
seq = [CustomClass()]
221+
222+
# AND a custom serializer
223+
def serializer(value):
224+
return "serialized"
225+
226+
# WHEN we call jsonable_encoder with the nested dictionary and unserializable value
227+
result = jsonable_encoder(seq, custom_serializer=serializer)
228+
229+
# THEN we should get the custom serializer output
230+
assert result == ["serialized"]
231+
232+
233+
def test_openapi_encode_custom_serializer_dataclasses():
234+
# GIVEN a sequence with a custom class
235+
class CustomClass:
236+
__slots__ = []
237+
238+
@dataclass
239+
class Order:
240+
kind: CustomClass
241+
242+
order = Order(kind=CustomClass())
243+
244+
# AND a custom serializer
245+
def serializer(value):
246+
return "serialized"
247+
248+
# WHEN we call jsonable_encoder with the nested dictionary and unserializable value
249+
result = jsonable_encoder(order, custom_serializer=serializer)
250+
251+
# THEN we should get the custom serializer output
252+
assert result == {"kind": "serialized"}

0 commit comments

Comments
 (0)