Skip to content

Commit f1437fd

Browse files
fix(event_handler): raise more specific SerializationError exception for unsupported types in data validation (#4415)
1 parent 420795a commit f1437fd

File tree

4 files changed

+110
-69
lines changed

4 files changed

+110
-69
lines changed

aws_lambda_powertools/event_handler/openapi/encoders.py

+76-69
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@
1414
from pydantic.types import SecretBytes, SecretStr
1515

1616
from aws_lambda_powertools.event_handler.openapi.compat import _model_dump
17+
from aws_lambda_powertools.event_handler.openapi.exceptions import SerializationError
1718
from aws_lambda_powertools.event_handler.openapi.types import IncEx
1819

1920
"""
@@ -69,88 +70,94 @@ def jsonable_encoder( # noqa: PLR0911
6970
if exclude is not None and not isinstance(exclude, (set, dict)):
7071
exclude = set(exclude)
7172

72-
# Pydantic models
73-
if isinstance(obj, BaseModel):
74-
return _dump_base_model(
75-
obj=obj,
76-
include=include,
77-
exclude=exclude,
78-
by_alias=by_alias,
79-
exclude_unset=exclude_unset,
80-
exclude_none=exclude_none,
81-
exclude_defaults=exclude_defaults,
82-
)
73+
try:
74+
# Pydantic models
75+
if isinstance(obj, BaseModel):
76+
return _dump_base_model(
77+
obj=obj,
78+
include=include,
79+
exclude=exclude,
80+
by_alias=by_alias,
81+
exclude_unset=exclude_unset,
82+
exclude_none=exclude_none,
83+
exclude_defaults=exclude_defaults,
84+
)
8385

84-
# Dataclasses
85-
if dataclasses.is_dataclass(obj):
86-
obj_dict = dataclasses.asdict(obj)
87-
return jsonable_encoder(
88-
obj_dict,
89-
include=include,
90-
exclude=exclude,
91-
by_alias=by_alias,
92-
exclude_unset=exclude_unset,
93-
exclude_defaults=exclude_defaults,
94-
exclude_none=exclude_none,
95-
)
86+
# Dataclasses
87+
if dataclasses.is_dataclass(obj):
88+
obj_dict = dataclasses.asdict(obj)
89+
return jsonable_encoder(
90+
obj_dict,
91+
include=include,
92+
exclude=exclude,
93+
by_alias=by_alias,
94+
exclude_unset=exclude_unset,
95+
exclude_defaults=exclude_defaults,
96+
exclude_none=exclude_none,
97+
)
9698

97-
# Enums
98-
if isinstance(obj, Enum):
99-
return obj.value
99+
# Enums
100+
if isinstance(obj, Enum):
101+
return obj.value
100102

101-
# Paths
102-
if isinstance(obj, PurePath):
103-
return str(obj)
103+
# Paths
104+
if isinstance(obj, PurePath):
105+
return str(obj)
104106

105-
# Scalars
106-
if isinstance(obj, (str, int, float, type(None))):
107-
return obj
107+
# Scalars
108+
if isinstance(obj, (str, int, float, type(None))):
109+
return obj
108110

109-
# Dictionaries
110-
if isinstance(obj, dict):
111-
return _dump_dict(
112-
obj=obj,
113-
include=include,
114-
exclude=exclude,
115-
by_alias=by_alias,
116-
exclude_none=exclude_none,
117-
exclude_unset=exclude_unset,
118-
)
111+
# Dictionaries
112+
if isinstance(obj, dict):
113+
return _dump_dict(
114+
obj=obj,
115+
include=include,
116+
exclude=exclude,
117+
by_alias=by_alias,
118+
exclude_none=exclude_none,
119+
exclude_unset=exclude_unset,
120+
)
121+
122+
# Sequences
123+
if isinstance(obj, (list, set, frozenset, GeneratorType, tuple, deque)):
124+
return _dump_sequence(
125+
obj=obj,
126+
include=include,
127+
exclude=exclude,
128+
by_alias=by_alias,
129+
exclude_none=exclude_none,
130+
exclude_defaults=exclude_defaults,
131+
exclude_unset=exclude_unset,
132+
)
119133

120-
# Sequences
121-
if isinstance(obj, (list, set, frozenset, GeneratorType, tuple, deque)):
122-
return _dump_sequence(
134+
# Other types
135+
if type(obj) in ENCODERS_BY_TYPE:
136+
return ENCODERS_BY_TYPE[type(obj)](obj)
137+
138+
for encoder, classes_tuple in encoders_by_class_tuples.items():
139+
if isinstance(obj, classes_tuple):
140+
return encoder(obj)
141+
142+
# Use custom serializer if present
143+
if custom_serializer:
144+
return custom_serializer(obj)
145+
146+
# Default
147+
return _dump_other(
123148
obj=obj,
124149
include=include,
125150
exclude=exclude,
126151
by_alias=by_alias,
127152
exclude_none=exclude_none,
128-
exclude_defaults=exclude_defaults,
129153
exclude_unset=exclude_unset,
154+
exclude_defaults=exclude_defaults,
130155
)
131-
132-
# Other types
133-
if type(obj) in ENCODERS_BY_TYPE:
134-
return ENCODERS_BY_TYPE[type(obj)](obj)
135-
136-
for encoder, classes_tuple in encoders_by_class_tuples.items():
137-
if isinstance(obj, classes_tuple):
138-
return encoder(obj)
139-
140-
# Use custom serializer if present
141-
if custom_serializer:
142-
return custom_serializer(obj)
143-
144-
# Default
145-
return _dump_other(
146-
obj=obj,
147-
include=include,
148-
exclude=exclude,
149-
by_alias=by_alias,
150-
exclude_none=exclude_none,
151-
exclude_unset=exclude_unset,
152-
exclude_defaults=exclude_defaults,
153-
)
156+
except ValueError as exc:
157+
raise SerializationError(
158+
f"Unable to serialize the object {obj} as it is not a supported type. Error details: {exc}",
159+
"See: https://docs.powertools.aws.dev/lambda/python/latest/core/event_handler/api_gateway/#serializing-objects",
160+
) from exc
154161

155162

156163
def _dump_base_model(

aws_lambda_powertools/event_handler/openapi/exceptions.py

+6
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,12 @@ def __init__(self, errors: Sequence[Any], *, body: Any = None) -> None:
2323
self.body = body
2424

2525

26+
class SerializationError(Exception):
27+
"""
28+
Base exception for all encoding errors
29+
"""
30+
31+
2632
class SchemaValidationError(ValidationException):
2733
"""
2834
Raised when the OpenAPI schema validation fails

docs/core/event_handler/api_gateway.md

+19
Original file line numberDiff line numberDiff line change
@@ -458,6 +458,25 @@ In the following example, we use a new `Header` OpenAPI type to add [one out of
458458

459459
1. `cloudfront_viewer_country` is a list that must contain values from the `CountriesAllowed` enumeration.
460460

461+
#### Supported types for response serialization
462+
463+
With data validation enabled, we natively support serializing the following data types to JSON:
464+
465+
| Data type | Serialized type |
466+
| -------------------------------------------------------------------- | -------------------------------------------------------------------------------- |
467+
| **Pydantic models** | `dict` |
468+
| **Python Dataclasses** | `dict` |
469+
| **Enum** | Enum values |
470+
| **Datetime** | Datetime ISO format string |
471+
| **Decimal** | `int` if no exponent, or `float` |
472+
| **Path** | `str` |
473+
| **UUID** | `str` |
474+
| **Set** | `list` |
475+
| **Python primitives** _(dict, string, sequences, numbers, booleans)_ | [Python's default JSON serializable types](https://docs.python.org/3/library/json.html#encoders-and-decoders){target="_blank" rel="nofollow"} |
476+
477+
???+ info "See [custom serializer section](#custom-serializer) for bringing your own."
478+
Otherwise, we will raise `SerializationError` for any unsupported types _e.g., SQLAlchemy models_.
479+
461480
### Accessing request details
462481

463482
Event Handler integrates with [Event Source Data Classes utilities](../../utilities/data_classes.md){target="_blank"}, and it exposes their respective resolver request details and convenient methods under `app.current_event`.

tests/functional/event_handler/test_openapi_encoders.py

+9
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@
77
from pydantic.color import Color
88

99
from aws_lambda_powertools.event_handler.openapi.encoders import jsonable_encoder
10+
from aws_lambda_powertools.event_handler.openapi.exceptions import SerializationError
1011

1112

1213
def test_openapi_encode_include():
@@ -184,3 +185,11 @@ def __init__(self, name: str):
184185

185186
result = jsonable_encoder(User(name="John"))
186187
assert result == {"name": "John"}
188+
189+
190+
def test_openapi_encode_with_error():
191+
class MyClass:
192+
__slots__ = []
193+
194+
with pytest.raises(SerializationError, match="Unable to serialize the object*"):
195+
jsonable_encoder(MyClass())

0 commit comments

Comments
 (0)