Skip to content

fix(event_handler): raise more specific SerializationError exception for unsupported types in data validation #4415

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
145 changes: 76 additions & 69 deletions aws_lambda_powertools/event_handler/openapi/encoders.py
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@
from pydantic.types import SecretBytes, SecretStr

from aws_lambda_powertools.event_handler.openapi.compat import _model_dump
from aws_lambda_powertools.event_handler.openapi.exceptions import EncoderError
from aws_lambda_powertools.event_handler.openapi.types import IncEx

"""
Expand Down Expand Up @@ -69,88 +70,94 @@ def jsonable_encoder( # noqa: PLR0911
if exclude is not None and not isinstance(exclude, (set, dict)):
exclude = set(exclude)

# Pydantic models
if isinstance(obj, BaseModel):
return _dump_base_model(
obj=obj,
include=include,
exclude=exclude,
by_alias=by_alias,
exclude_unset=exclude_unset,
exclude_none=exclude_none,
exclude_defaults=exclude_defaults,
)
try:
# Pydantic models
if isinstance(obj, BaseModel):
return _dump_base_model(
obj=obj,
include=include,
exclude=exclude,
by_alias=by_alias,
exclude_unset=exclude_unset,
exclude_none=exclude_none,
exclude_defaults=exclude_defaults,
)

# Dataclasses
if dataclasses.is_dataclass(obj):
obj_dict = dataclasses.asdict(obj)
return jsonable_encoder(
obj_dict,
include=include,
exclude=exclude,
by_alias=by_alias,
exclude_unset=exclude_unset,
exclude_defaults=exclude_defaults,
exclude_none=exclude_none,
)
# Dataclasses
if dataclasses.is_dataclass(obj):
obj_dict = dataclasses.asdict(obj)
return jsonable_encoder(
obj_dict,
include=include,
exclude=exclude,
by_alias=by_alias,
exclude_unset=exclude_unset,
exclude_defaults=exclude_defaults,
exclude_none=exclude_none,
)

# Enums
if isinstance(obj, Enum):
return obj.value
# Enums
if isinstance(obj, Enum):
return obj.value

# Paths
if isinstance(obj, PurePath):
return str(obj)
# Paths
if isinstance(obj, PurePath):
return str(obj)

# Scalars
if isinstance(obj, (str, int, float, type(None))):
return obj
# Scalars
if isinstance(obj, (str, int, float, type(None))):
return obj

# Dictionaries
if isinstance(obj, dict):
return _dump_dict(
obj=obj,
include=include,
exclude=exclude,
by_alias=by_alias,
exclude_none=exclude_none,
exclude_unset=exclude_unset,
)
# Dictionaries
if isinstance(obj, dict):
return _dump_dict(
obj=obj,
include=include,
exclude=exclude,
by_alias=by_alias,
exclude_none=exclude_none,
exclude_unset=exclude_unset,
)

# Sequences
if isinstance(obj, (list, set, frozenset, GeneratorType, tuple, deque)):
return _dump_sequence(
obj=obj,
include=include,
exclude=exclude,
by_alias=by_alias,
exclude_none=exclude_none,
exclude_defaults=exclude_defaults,
exclude_unset=exclude_unset,
)

# Sequences
if isinstance(obj, (list, set, frozenset, GeneratorType, tuple, deque)):
return _dump_sequence(
# Other types
if type(obj) in ENCODERS_BY_TYPE:
return ENCODERS_BY_TYPE[type(obj)](obj)

for encoder, classes_tuple in encoders_by_class_tuples.items():
if isinstance(obj, classes_tuple):
return encoder(obj)

# Use custom serializer if present
if custom_serializer:
return custom_serializer(obj)

# Default
return _dump_other(
obj=obj,
include=include,
exclude=exclude,
by_alias=by_alias,
exclude_none=exclude_none,
exclude_defaults=exclude_defaults,
exclude_unset=exclude_unset,
exclude_defaults=exclude_defaults,
)

# Other types
if type(obj) in ENCODERS_BY_TYPE:
return ENCODERS_BY_TYPE[type(obj)](obj)

for encoder, classes_tuple in encoders_by_class_tuples.items():
if isinstance(obj, classes_tuple):
return encoder(obj)

# Use custom serializer if present
if custom_serializer:
return custom_serializer(obj)

# Default
return _dump_other(
obj=obj,
include=include,
exclude=exclude,
by_alias=by_alias,
exclude_none=exclude_none,
exclude_unset=exclude_unset,
exclude_defaults=exclude_defaults,
)
except ValueError as exc:
raise EncoderError(
f"Unable to serializer the object {obj} as it is not a supported type. Error details: {str(exc)}",
"See: https://docs.powertools.aws.dev/lambda/python/latest/core/event_handler/api_gateway/#serializing-objects",
) from exc


def _dump_base_model(
Expand Down
6 changes: 6 additions & 0 deletions aws_lambda_powertools/event_handler/openapi/exceptions.py
Original file line number Diff line number Diff line change
Expand Up @@ -21,3 +21,9 @@ class RequestValidationError(ValidationException):
def __init__(self, errors: Sequence[Any], *, body: Any = None) -> None:
super().__init__(errors)
self.body = body


class EncoderError(Exception):
"""
Base exception for all encoding errors
"""
6 changes: 6 additions & 0 deletions docs/core/event_handler/api_gateway.md
Original file line number Diff line number Diff line change
Expand Up @@ -458,6 +458,12 @@ In the following example, we use a new `Header` OpenAPI type to add [one out of

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

#### Serializing objects

We support the serialization of various Python objects, including Pydantic models, dataclasses, enumerations, file paths, scalar types (strings, integers, floats, and None), dictionaries, various sequence types (lists, sets, frozen sets, generators, tuples, and deques), and others defined [here](https://github.com/aws-powertools/powertools-lambda-python/blob/develop/aws_lambda_powertools/event_handler/openapi/encoders.py#L24).

For objects we do not support, such as SQLAlchemy models, we suggest providing your own [custom serializer](#custom-serializer).

### Accessing request details

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`.
Expand Down
9 changes: 9 additions & 0 deletions tests/functional/event_handler/test_openapi_encoders.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@
from pydantic.color import Color

from aws_lambda_powertools.event_handler.openapi.encoders import jsonable_encoder
from aws_lambda_powertools.event_handler.openapi.exceptions import EncoderError


def test_openapi_encode_include():
Expand Down Expand Up @@ -184,3 +185,11 @@ def __init__(self, name: str):

result = jsonable_encoder(User(name="John"))
assert result == {"name": "John"}


def test_openapi_encode_with_error():
class MyClass:
__slots__ = []

with pytest.raises(EncoderError, match="Unable to serializer the object*"):
jsonable_encoder(MyClass())