Skip to content
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 SerializationError
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 SerializationError(
f"Unable to serialize the object {obj} as it is not a supported type. Error details: {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 @@ -23,6 +23,12 @@ def __init__(self, errors: Sequence[Any], *, body: Any = None) -> None:
self.body = body


class SerializationError(Exception):
"""
Base exception for all encoding errors
"""


class SchemaValidationError(ValidationException):
"""
Raised when the OpenAPI schema validation fails
Expand Down
19 changes: 19 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,25 @@ 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.

#### Supported types for response serialization

With data validation enabled, we natively support serializing the following data types to JSON:

| Data type | Serialized type |
| -------------------------------------------------------------------- | -------------------------------------------------------------------------------- |
| **Pydantic models** | `dict` |
| **Python Dataclasses** | `dict` |
| **Enum** | Enum values |
| **Datetime** | Datetime ISO format string |
| **Decimal** | `int` if no exponent, or `float` |
| **Path** | `str` |
| **UUID** | `str` |
| **Set** | `list` |
| **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"} |

???+ info "See [custom serializer section](#custom-serializer) for bringing your own."
Otherwise, we will raise `SerializationError` for any unsupported types _e.g., SQLAlchemy models_.

### 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 SerializationError


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(SerializationError, match="Unable to serialize the object*"):
jsonable_encoder(MyClass())