diff --git a/aws_lambda_powertools/event_handler/openapi/params.py b/aws_lambda_powertools/event_handler/openapi/params.py
index f267a4841f5..bd542ba7932 100644
--- a/aws_lambda_powertools/event_handler/openapi/params.py
+++ b/aws_lambda_powertools/event_handler/openapi/params.py
@@ -117,6 +117,64 @@ def __init__(
json_schema_extra: Union[Dict[str, Any], None] = None,
**extra: Any,
):
+ """
+ Constructs a new Param.
+
+ Parameters
+ ----------
+ default: Any
+ The default value of the parameter
+ default_factory: Callable[[], Any], optional
+ Callable that will be called when a default value is needed for this field
+ annotation: Any, optional
+ The type annotation of the parameter
+ alias: str, optional
+ The public name of the field
+ alias_priority: int, optional
+ Priority of the alias. This affects whether an alias generator is used
+ validation_alias: str | AliasPath | AliasChoices | None, optional
+ Alias to be used for validation only
+ serialization_alias: str | AliasPath | AliasChoices | None, optional
+ Alias to be used for serialization only
+ title: str, optional
+ The title of the parameter
+ description: str, optional
+ The description of the parameter
+ gt: float, optional
+ Only applies to numbers, required the field to be "greater than"
+ ge: float, optional
+ Only applies to numbers, required the field to be "greater than or equal"
+ lt: float, optional
+ Only applies to numbers, required the field to be "less than"
+ le: float, optional
+ Only applies to numbers, required the field to be "less than or equal"
+ min_length: int, optional
+ Only applies to strings, required the field to have a minimum length
+ max_length: int, optional
+ Only applies to strings, required the field to have a maximum length
+ pattern: str, optional
+ Only applies to strings, requires the field match against a regular expression pattern string
+ discriminator: str, optional
+ Parameter field name for discriminating the type in a tagged union
+ strict: bool, optional
+ Enables Pydantic's strict mode for the field
+ multiple_of: float, optional
+ Only applies to numbers, requires the field to be a multiple of the given value
+ allow_inf_nan: bool, optional
+ Only applies to numbers, requires the field to allow infinity and NaN values
+ max_digits: int, optional
+ Only applies to Decimals, requires the field to have a maxmium number of digits within the decimal.
+ decimal_places: int, optional
+ Only applies to Decimals, requires the field to have at most a number of decimal places
+ examples: List[Any], optional
+ A list of examples for the parameter
+ deprecated: bool, optional
+ If `True`, the parameter will be marked as deprecated
+ include_in_schema: bool, optional
+ If `False`, the parameter will be excluded from the generated OpenAPI schema
+ json_schema_extra: Dict[str, Any], optional
+ Extra values to include in the generated OpenAPI schema
+ """
self.deprecated = deprecated
self.include_in_schema = include_in_schema
@@ -207,6 +265,64 @@ def __init__(
json_schema_extra: Union[Dict[str, Any], None] = None,
**extra: Any,
):
+ """
+ Constructs a new Path param.
+
+ Parameters
+ ----------
+ default: Any
+ The default value of the parameter
+ default_factory: Callable[[], Any], optional
+ Callable that will be called when a default value is needed for this field
+ annotation: Any, optional
+ The type annotation of the parameter
+ alias: str, optional
+ The public name of the field
+ alias_priority: int, optional
+ Priority of the alias. This affects whether an alias generator is used
+ validation_alias: str | AliasPath | AliasChoices | None, optional
+ Alias to be used for validation only
+ serialization_alias: str | AliasPath | AliasChoices | None, optional
+ Alias to be used for serialization only
+ title: str, optional
+ The title of the parameter
+ description: str, optional
+ The description of the parameter
+ gt: float, optional
+ Only applies to numbers, required the field to be "greater than"
+ ge: float, optional
+ Only applies to numbers, required the field to be "greater than or equal"
+ lt: float, optional
+ Only applies to numbers, required the field to be "less than"
+ le: float, optional
+ Only applies to numbers, required the field to be "less than or equal"
+ min_length: int, optional
+ Only applies to strings, required the field to have a minimum length
+ max_length: int, optional
+ Only applies to strings, required the field to have a maximum length
+ pattern: str, optional
+ Only applies to strings, requires the field match against a regular expression pattern string
+ discriminator: str, optional
+ Parameter field name for discriminating the type in a tagged union
+ strict: bool, optional
+ Enables Pydantic's strict mode for the field
+ multiple_of: float, optional
+ Only applies to numbers, requires the field to be a multiple of the given value
+ allow_inf_nan: bool, optional
+ Only applies to numbers, requires the field to allow infinity and NaN values
+ max_digits: int, optional
+ Only applies to Decimals, requires the field to have a maxmium number of digits within the decimal.
+ decimal_places: int, optional
+ Only applies to Decimals, requires the field to have at most a number of decimal places
+ examples: List[Any], optional
+ A list of examples for the parameter
+ deprecated: bool, optional
+ If `True`, the parameter will be marked as deprecated
+ include_in_schema: bool, optional
+ If `False`, the parameter will be excluded from the generated OpenAPI schema
+ json_schema_extra: Dict[str, Any], optional
+ Extra values to include in the generated OpenAPI schema
+ """
if default is not ...:
raise AssertionError("Path parameters cannot have a default value")
@@ -279,6 +395,64 @@ def __init__(
json_schema_extra: Union[Dict[str, Any], None] = None,
**extra: Any,
):
+ """
+ Constructs a new Query param.
+
+ Parameters
+ ----------
+ default: Any
+ The default value of the parameter
+ default_factory: Callable[[], Any], optional
+ Callable that will be called when a default value is needed for this field
+ annotation: Any, optional
+ The type annotation of the parameter
+ alias: str, optional
+ The public name of the field
+ alias_priority: int, optional
+ Priority of the alias. This affects whether an alias generator is used
+ validation_alias: str | AliasPath | AliasChoices | None, optional
+ Alias to be used for validation only
+ serialization_alias: str | AliasPath | AliasChoices | None, optional
+ Alias to be used for serialization only
+ title: str, optional
+ The title of the parameter
+ description: str, optional
+ The description of the parameter
+ gt: float, optional
+ Only applies to numbers, required the field to be "greater than"
+ ge: float, optional
+ Only applies to numbers, required the field to be "greater than or equal"
+ lt: float, optional
+ Only applies to numbers, required the field to be "less than"
+ le: float, optional
+ Only applies to numbers, required the field to be "less than or equal"
+ min_length: int, optional
+ Only applies to strings, required the field to have a minimum length
+ max_length: int, optional
+ Only applies to strings, required the field to have a maximum length
+ pattern: str, optional
+ Only applies to strings, requires the field match against a regular expression pattern string
+ discriminator: str, optional
+ Parameter field name for discriminating the type in a tagged union
+ strict: bool, optional
+ Enables Pydantic's strict mode for the field
+ multiple_of: float, optional
+ Only applies to numbers, requires the field to be a multiple of the given value
+ allow_inf_nan: bool, optional
+ Only applies to numbers, requires the field to allow infinity and NaN values
+ max_digits: int, optional
+ Only applies to Decimals, requires the field to have a maxmium number of digits within the decimal.
+ decimal_places: int, optional
+ Only applies to Decimals, requires the field to have at most a number of decimal places
+ examples: List[Any], optional
+ A list of examples for the parameter
+ deprecated: bool, optional
+ If `True`, the parameter will be marked as deprecated
+ include_in_schema: bool, optional
+ If `False`, the parameter will be excluded from the generated OpenAPI schema
+ json_schema_extra: Dict[str, Any], optional
+ Extra values to include in the generated OpenAPI schema
+ """
super().__init__(
default=default,
default_factory=default_factory,
diff --git a/docs/core/event_handler/api_gateway.md b/docs/core/event_handler/api_gateway.md
index 6868ce25d46..005ac3a4b7b 100644
--- a/docs/core/event_handler/api_gateway.md
+++ b/docs/core/event_handler/api_gateway.md
@@ -11,12 +11,21 @@ Event handler for Amazon API Gateway REST and HTTP APIs, Application Loader Bala
* Support for CORS, binary and Gzip compression, Decimals JSON encoding and bring your own JSON serializer
* Built-in integration with [Event Source Data Classes utilities](../../utilities/data_classes.md){target="_blank"} for self-documented event schema
* Works with micro function (one or a few routes) and monolithic functions (all routes)
+* Support for OpenAPI and data validation for requests/responses
## Getting started
???+ tip
All examples shared in this documentation are available within the [project repository](https://github.com/aws-powertools/powertools-lambda-python/tree/develop/examples){target="_blank"}.
+### Install
+
+!!! info "This is not necessary if you're installing Powertools for AWS Lambda (Python) via [Lambda Layer/SAR](../../index.md#lambda-layer){target="_blank"}."
+
+**When using the data validation feature**, you need to add `pydantic` as a dependency in your preferred tool _e.g., requirements.txt, pyproject.toml_.
+
+As of now, both Pydantic V1 and V2 are supported. For a future major version, we will only support Pydantic V2.
+
### Required resources
@@ -221,6 +230,190 @@ If you need to accept multiple HTTP methods in a single function, you can use th
???+ note
It is generally better to have separate functions for each HTTP method, as the functionality tends to differ depending on which method is used.
+### Data validation
+
+!!! note "This changes the authoring experience by relying on Python's type annotations"
+ It's inspired by [FastAPI framework](https://fastapi.tiangolo.com/){target="_blank" rel="nofollow"} for ergonomics and to ease migrations in either direction. We support both Pydantic models and Python's dataclass.
+
+ For brevity, we'll focus on Pydantic only.
+
+All resolvers can optionally coerce and validate incoming requests by setting `enable_validation=True`.
+
+With this feature, we can now express how we expect our incoming data and response to look like. This moves data validation responsibilities to Event Handler resolvers, reducing a ton of boilerplate code.
+
+Let's rewrite the previous examples to signal our resolver what shape we expect our data to be.
+
+
+
+=== "data_validation.py"
+
+ ```python hl_lines="13 16 25 29"
+ --8<-- "examples/event_handler_rest/src/data_validation.py"
+ ```
+
+ 1. This enforces data validation at runtime. Any validation error will return `HTTP 422: Unprocessable Entity error`.
+ 2. We create a Pydantic model to define how our data looks like.
+ 3. Defining a route remains exactly as before.
+ 4. By default, URL Paths will be `str`. Here, we are telling our resolver it should be `int`, so it converts it for us.
Lastly, we're also saying the return should be our `Todo`. This will help us later when we touch OpenAPI auto-documentation.
+ 5. `todo.json()` returns a dictionary. However, Event Handler knows the response should be `Todo` so it converts and validates accordingly.
+
+=== "data_validation.json"
+
+ ```json hl_lines="4"
+ --8<-- "examples/event_handler_rest/src/data_validation.json"
+ ```
+
+=== "data_validation_output.json"
+
+ ```json hl_lines="2-3"
+ --8<-- "examples/event_handler_rest/src/data_validation_output.json"
+ ```
+
+
+
+#### Handling validation errors
+
+!!! info "By default, we hide extended error details for security reasons _(e.g., pydantic url, Pydantic code)_."
+
+Any incoming request that fails validation will lead to a `HTTP 422: Unprocessable Entity error` response that will look similar to this:
+
+```json hl_lines="2 3" title="data_validation_error_unsanitized_output.json"
+--8<-- "examples/event_handler_rest/src/data_validation_error_unsanitized_output.json"
+```
+
+You can customize the error message by catching the `RequestValidationError` exception. This is useful when you might have a security policy to return opaque validation errors, or have a company standard for API validation errors.
+
+Here's an example where we catch validation errors, log all details for further investigation, and return the same `HTTP 422` with an opaque error.
+
+=== "data_validation_sanitized_error.py"
+
+ Note that Pydantic versions [1](https://docs.pydantic.dev/1.10/usage/models/#error-handling){target="_blank" rel="nofollow"} and [2](https://docs.pydantic.dev/latest/errors/errors/){target="_blank" rel="nofollow"} report validation detailed errors differently.
+
+ ```python hl_lines="8 24-25 31"
+ --8<-- "examples/event_handler_rest/src/data_validation_sanitized_error.py"
+ ```
+
+ 1. We use [exception handler](#exception-handling) decorator to catch **any** request validation errors.
Then, we log the detailed reason as to why it failed while returning a custom `Response` object to hide that from them.
+
+=== "data_validation_sanitized_error_output.json"
+
+ ```json hl_lines="2 3"
+ --8<-- "examples/event_handler_rest/src/data_validation_sanitized_error_output.json"
+ ```
+
+#### Validating payloads
+
+!!! info "We will automatically validate, inject, and convert incoming request payloads based on models via type annotation."
+
+Let's improve our previous example by handling the creation of todo items via `HTTP POST`.
+
+What we want is for Event Handler to convert the incoming payload as an instance of our `Todo` model. We handle the creation of that `todo`, and then return the `ID` of the newly created `todo`.
+
+Even better, we can also let Event Handler validate and convert our response according to type annotations, further reducing boilerplate.
+
+=== "validating_payloads.py"
+
+ ```python hl_lines="13 16 24 33"
+ --8<-- "examples/event_handler_rest/src/validating_payloads.py"
+ ```
+
+ 1. This enforces data validation at runtime. Any validation error will return `HTTP 422: Unprocessable Entity error`.
+ 2. We create a Pydantic model to define how our data looks like.
+ 3. We define `Todo` as our type annotation. Event Handler then uses this model to validate and inject the incoming request as `Todo`.
+ 4. Lastly, we return the ID of our newly created `todo` item.
Because we specify the return type (`str`), Event Handler will take care of serializing this as a JSON string.
+ 5. Note that the return type is `List[Todo]`.
Event Handler will take the return (`todo.json`), and validate each list item against `Todo` model before returning the response accordingly.
+
+=== "validating_payloads.json"
+
+ ```json hl_lines="3 5-6"
+ --8<-- "examples/event_handler_rest/src/validating_payloads.json"
+ ```
+
+=== "validating_payloads_output.json"
+
+ ```json hl_lines="3"
+ --8<-- "examples/event_handler_rest/src/validating_payloads_output.json"
+ ```
+
+##### Validating payload subset
+
+With the addition of the [`Annotated` type starting in Python 3.9](https://docs.python.org/3/library/typing.html#typing.Annotated){target="_blank" rel="nofollow"}, types can contain additional metadata, allowing us to represent anything we want.
+
+We use the `Annotated` and OpenAPI `Body` type to instruct Event Handler that our payload is located in a particular JSON key.
+
+!!! note "Event Handler will match the parameter name with the JSON key to validate and inject what you want."
+
+=== "validating_payload_subset.py"
+
+ ```python hl_lines="7 8 22"
+ --8<-- "examples/event_handler_rest/src/validating_payload_subset.py"
+ ```
+
+ 1. `Body` is a special OpenAPI type that can add additional constraints to a request payload.
+ 2. `Body(embed=True)` instructs Event Handler to look up inside the payload for a key.
This means Event Handler will look up for a key named `todo`, validate the value against `Todo`, and inject it.
+
+=== "validating_payload_subset.json"
+
+ ```json hl_lines="3-4 6"
+ --8<-- "examples/event_handler_rest/src/validating_payload_subset.json"
+ ```
+
+=== "validating_payload_subset_output.json"
+
+ ```json hl_lines="3"
+ --8<-- "examples/event_handler_rest/src/validating_payload_subset_output.json"
+ ```
+
+#### Validating query strings
+
+!!! info "We will automatically validate and inject incoming query strings via type annotation."
+
+We use the `Annotated` type to tell Event Handler that a particular parameter is not only an optional string, but also a query string with constraints.
+
+In the following example, we use a new `Query` OpenAPI type to add [one out of many possible constraints](#customizing-openapi-parameters), which should read as:
+
+* `completed` is a query string with a `None` as its default value
+* `completed`, when set, should have at minimum 4 characters
+* Doesn't match? Event Handler will return a validation error response
+
+
+
+=== "validating_query_strings.py"
+
+ ```python hl_lines="8 10 27"
+ --8<-- "examples/event_handler_rest/src/validating_query_strings.py"
+ ```
+
+ 1. If you're not using Python 3.9 or higher, you can install and use [`typing_extensions`](https://pypi.org/project/typing-extensions/){target="_blank" rel="nofollow"} to the same effect
+ 2. `Query` is a special OpenAPI type that can add constraints to a query string as well as document them
+ 3. **First time seeing the `Annotated`?**
This special type uses the first argument as the actual type, and subsequent arguments are metadata.
At runtime, static checkers will also see the first argument, but anyone receiving them could inspect them to fetch their metadata.
+
+=== "skip_validating_query_strings.py"
+
+ If you don't want to validate query strings but simply let Event Handler inject them as parameters, you can omit `Query` type annotation.
+
+ This is merely for your convenience.
+
+ ```python hl_lines="25"
+ --8<-- "examples/event_handler_rest/src/skip_validating_query_strings.py"
+ ```
+
+ 1. `completed` is still the same query string as before, except we simply state it's an string. No `Query` or `Annotated` to validate it.
+
+
+
+#### Validating path parameters
+
+Just like we learned in [query string validation](#validating-query-strings), we can use a new `Path` OpenAPI type to [add constraints](#customizing-openapi-parameters).
+
+For example, we could validate that `` dynamic path should be no greater than three digits.
+
+```python hl_lines="8 10 27" title="validating_path.py"
+--8<-- "examples/event_handler_rest/src/validating_path.py"
+```
+
+1. `Path` is a special OpenAPI type that allows us to constrain todo_id to be less than 999.
+
### 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`.
@@ -279,6 +472,30 @@ We provide pre-defined errors for the most popular ones such as HTTP 400, 401, 4
--8<-- "examples/event_handler_rest/src/raising_http_errors.py"
```
+### Enabling SwaggerUI
+
+!!! note "This feature requires [data validation](#data-validation) feature to be enabled."
+
+Behind the scenes, the [data validation](#data-validation) feature auto-generates an OpenAPI specification from your routes and type annotations. You can use [Swagger UI](https://swagger.io/tools/swagger-ui/){target="_blank" rel="nofollow"} to visualize and interact with your newly auto-documented API.
+
+There are some important **caveats** that you should know before enabling it:
+
+| Caveat | Description |
+| ------------------------------------------------ | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
+| Swagger UI is **publicly accessible by default** | When using `enable_swagger` method, you can [protect sensitive API endpoints by implementing a custom middleware](#customizing-swagger-ui) using your preferred authorization mechanism. |
+| **No micro-functions support** yet | Swagger UI is enabled on a per resolver instance which will limit its accuracy here. |
+| You need to expose **new routes** | You'll need to expose the following paths to Lambda: `/swagger`, `/swagger.css`, `/swagger.js`; ignore if you're routing all paths already. |
+
+```python hl_lines="12-13" title="enabling_swagger.py"
+--8<-- "examples/event_handler_rest/src/enabling_swagger.py"
+```
+
+1. `enable_swagger` creates a route to serve Swagger UI and allows quick customizations.
You can also include middlewares to protect or enhance the overall experience.
+
+Here's an example of what it looks like by default:
+
+
+
### Custom Domain API Mappings
When using [Custom Domain API Mappings feature](https://docs.aws.amazon.com/apigateway/latest/developerguide/rest-api-mappings.html){target="_blank"}, you must use **`strip_prefixes`** param in the `APIGatewayRestResolver` constructor.
@@ -573,8 +790,8 @@ As a practical example, let's refactor our correlation ID middleware so it accep
These are native middlewares that may become native features depending on customer demand.
-| Middleware | Purpose |
-| ------------------------------------------------------------------------------------------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------- |
+| Middleware | Purpose |
+| ------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------ |
| [SchemaValidationMiddleware](/lambda/python/latest/api/event_handler/middlewares/schema_validation.html){target="_blank"} | Validates API request body and response against JSON Schema, using [Validation utility](../../utilities/validation.md){target="_blank"} |
#### Being a good citizen
@@ -696,6 +913,112 @@ This will enable full tracebacks errors in the response, print request and respo
--8<-- "examples/event_handler_rest/src/debug_mode.py"
```
+### OpenAPI
+
+When you enable [Data Validation](#data-validation), we use a combination of Pydantic Models and [OpenAPI](https://www.openapis.org/){target="_blank"} type annotations to add constraints to your API's parameters.
+
+In OpenAPI documentation tools like [SwaggerUI](#enabling-swaggerui), these annotations become readable descriptions, offering a self-explanatory API interface. This reduces boilerplate code while improving functionality and enabling auto-documentation.
+
+???+ note
+ We don't have support for files, form data, and header parameters at the moment. If you're interested in this, please [open an issue](https://github.com/aws-powertools/powertools-lambda-python/issues/new?assignees=&labels=feature-request%2Ctriage&projects=&template=feature_request.yml&title=Feature+request%3A+TITLE).
+
+#### Customizing OpenAPI parameters
+
+Whenever you use OpenAPI parameters to validate [query strings](#validating-query-strings) or [path parameters](#validating-path-parameters), you can enhance validation and OpenAPI documentation by using any of these parameters:
+
+| Field name | Type | Description |
+| --------------------- | ------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
+| `alias` | `str` | Alternative name for a field, used when serializing and deserializing data |
+| `validation_alias` | `str` | Alternative name for a field during validation (but not serialization) |
+| `serialization_alias` | `str` | Alternative name for a field during serialization (but not during validation) |
+| `description` | `str` | Human-readable description |
+| `gt` | `float` | Greater than. If set, value must be greater than this. Only applicable to numbers |
+| `ge` | `float` | Greater than or equal. If set, value must be greater than or equal to this. Only applicable to numbers |
+| `lt` | `float` | Less than. If set, value must be less than this. Only applicable to numbers |
+| `le` | `float` | Less than or equal. If set, value must be less than or equal to this. Only applicable to numbers |
+| `min_length` | `int` | Minimum length for strings |
+| `max_length` | `int` | Maximum length for strings |
+| `pattern` | `string` | A regular expression that the string must match. |
+| `strict` | `bool` | If `True`, strict validation is applied to the field. See [Strict Mode](https://docs.pydantic.dev/latest/concepts/strict_mode/){target"_blank" rel="nofollow"} for details |
+| `multiple_of` | `float` | Value must be a multiple of this. Only applicable to numbers |
+| `allow_inf_nan` | `bool` | Allow `inf`, `-inf`, `nan`. Only applicable to numbers |
+| `max_digits` | `int` | Maximum number of allow digits for strings |
+| `decimal_places` | `int` | Maximum number of decimal places allowed for numbers |
+| `examples` | `List\[Any\]` | List of examples of the field |
+| `deprecated` | `bool` | Marks the field as deprecated |
+| `include_in_schema` | `bool` | If `False` the field will not be part of the exported OpenAPI schema |
+| `json_schema_extra` | `JsonDict` | Any additional JSON schema data for the schema property |
+
+#### Customizing API operations
+
+Customize your API endpoints by adding metadata to endpoint definitions. This provides descriptive documentation for API consumers and gives extra instructions to the framework.
+
+Here's a breakdown of various customizable fields:
+
+| Field Name | Type | Description |
+| ---------------------- | --------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
+| `summary` | `str` | A concise overview of the main functionality of the endpoint. This brief introduction is usually displayed in autogenerated API documentation and helps consumers quickly understand what the endpoint does. |
+| `description` | `str` | A more detailed explanation of the endpoint, which can include information about the operation's behavior, including side effects, error states, and other operational guidelines. |
+| `responses` | `Dict[int, Dict[str, Any]]` | A dictionary that maps each HTTP status code to a Response Object as defined by the [OpenAPI Specification](https://swagger.io/specification/#response-object). This allows you to describe expected responses, including default or error messages, and their corresponding schemas for different status codes. |
+| `response_description` | `str` | Provides the default textual description of the response sent by the endpoint when the operation is successful. It is intended to give a human-readable understanding of the result. |
+| `tags` | `List[str]` | Tags are a way to categorize and group endpoints within the API documentation. They can help organize the operations by resources or other heuristic. |
+| `operation_id` | `str` | A unique identifier for the operation, which can be used for referencing this operation in documentation or code. This ID must be unique across all operations described in the API. |
+| `include_in_schema` | `bool` | A boolean value that determines whether or not this operation should be included in the OpenAPI schema. Setting it to `False` can hide the endpoint from generated documentation and schema exports, which might be useful for private or experimental endpoints. |
+
+To implement these customizations, include extra parameters when defining your routes:
+
+```python hl_lines="11-20" title="customizing_api_operations.py"
+--8<-- "examples/event_handler_rest/src/customizing_api_operations.py"
+```
+
+#### Customizing Swagger UI
+
+???+note "Customizing the Swagger metadata"
+ The `enable_swagger` method accepts the same metadata as described at [Customizing OpenAPI metadata](#customizing-openapi-metadata).
+
+The Swagger UI appears by default at the `/swagger` path, but you can customize this to serve the documentation from another path and specify the source for Swagger UI assets.
+
+Below is an example configuration for serving Swagger UI from a custom path or CDN, with assets like CSS and JavaScript loading from a chosen CDN base URL.
+
+=== "customizing_swagger.py"
+
+ ```python hl_lines="10"
+ --8<-- "examples/event_handler_rest/src/customizing_swagger.py"
+ ```
+
+=== "customizing_swagger_middlewares.py"
+
+ A Middleware can handle tasks such as adding security headers, user authentication, or other request processing for serving the Swagger UI.
+
+ ```python hl_lines="7 13-18 21"
+ --8<-- "examples/event_handler_rest/src/customizing_swagger_middlewares.py"
+ ```
+
+#### Customizing OpenAPI metadata
+
+Defining and customizing OpenAPI metadata gives detailed, top-level information about your API. Here's the method to set and tailor this metadata:
+
+| Field Name | Type | Description |
+| ------------------ | -------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
+| `title` | `str` | The title for your API. It should be a concise, specific name that can be used to identify the API in documentation or listings. |
+| `version` | `str` | The version of the API you are documenting. This could reflect the release iteration of the API and helps clients understand the evolution of the API. |
+| `openapi_version` | `str` | Specifies the version of the OpenAPI Specification on which your API is based. For most contemporary APIs, the default value would be `3.0.0` or higher. |
+| `summary` | `str` | A short and informative summary that can provide an overview of what the API does. This can be the same as or different from the title but should add context or information. |
+| `description` | `str` | A verbose description that can include Markdown formatting, providing a full explanation of the API's purpose, functionalities, and general usage instructions. |
+| `tags` | `List[str]` | A collection of tags that categorize endpoints for better organization and navigation within the documentation. This can group endpoints by their functionality or other criteria. |
+| `servers` | `List[Server]` | An array of Server objects, which specify the URL to the server and a description for its environment (production, staging, development, etc.), providing connectivity information. |
+| `terms_of_service` | `str` | A URL that points to the terms of service for your API. This could provide legal information and user responsibilities related to the usage of the API. |
+| `contact` | `Contact` | A Contact object containing contact details of the organization or individuals maintaining the API. This may include fields such as name, URL, and email. |
+| `license_info` | `License` | A License object providing the license details for the API, typically including the name of the license and the URL to the full license text. |
+
+Include extra parameters when exporting your OpenAPI specification to apply these customizations:
+
+=== "customizing_api_metadata.py"
+
+ ```python hl_lines="25-31"
+ --8<-- "examples/event_handler_rest/src/customizing_api_metadata.py"
+ ```
+
### Custom serializer
You can instruct event handler to use a custom serializer to best suit your needs, for example take into account Enums when serializing.
diff --git a/docs/media/swagger.png b/docs/media/swagger.png
new file mode 100644
index 00000000000..3db7786886b
Binary files /dev/null and b/docs/media/swagger.png differ
diff --git a/examples/event_handler_lambda_function_url/sam/template.yaml b/examples/event_handler_lambda_function_url/sam/template.yaml
index 457f68c3816..0c58c9e87c1 100644
--- a/examples/event_handler_lambda_function_url/sam/template.yaml
+++ b/examples/event_handler_lambda_function_url/sam/template.yaml
@@ -5,7 +5,7 @@ Description: Hello world event handler Lambda Function URL
Globals:
Function:
Timeout: 5
- Runtime: python3.9
+ Runtime: python3.11
Tracing: Active
Environment:
Variables:
@@ -28,4 +28,4 @@ Resources:
CodeUri: ../src
Description: API handler function
FunctionUrlConfig:
- AuthType: NONE # AWS_IAM for added security beyond sample documentation
+ AuthType: NONE # AWS_IAM for added security beyond sample documentation
diff --git a/examples/event_handler_rest/sam/template.yaml b/examples/event_handler_rest/sam/template.yaml
index 67000a39122..aa0ad4b0452 100644
--- a/examples/event_handler_rest/sam/template.yaml
+++ b/examples/event_handler_rest/sam/template.yaml
@@ -59,3 +59,21 @@ Resources:
# Properties:
# Path: /todos
# Method: POST
+
+ ## Swagger UI specific routes
+
+ # SwaggerUI:
+ # Type: Api
+ # Properties:
+ # Path: /swagger
+ # Method: GET
+ # SwaggerUICSS:
+ # Type: Api
+ # Properties:
+ # Path: /swagger.css
+ # Method: GET
+ # SwaggerUIJS:
+ # Type: Api
+ # Properties:
+ # Path: /swagger.js
+ # Method: GET
diff --git a/examples/event_handler_rest/src/customizing_api_metadata.py b/examples/event_handler_rest/src/customizing_api_metadata.py
new file mode 100644
index 00000000000..cd9ced455d2
--- /dev/null
+++ b/examples/event_handler_rest/src/customizing_api_metadata.py
@@ -0,0 +1,33 @@
+import requests
+
+from aws_lambda_powertools.event_handler import APIGatewayRestResolver
+from aws_lambda_powertools.event_handler.openapi.models import Contact, Server
+from aws_lambda_powertools.utilities.typing import LambdaContext
+
+app = APIGatewayRestResolver(enable_validation=True)
+
+
+@app.get("/todos/")
+def get_todo_title(todo_id: int) -> str:
+ todo = requests.get(f"https://jsonplaceholder.typicode.com/todos/{todo_id}")
+ todo.raise_for_status()
+
+ return todo.json()["title"]
+
+
+def lambda_handler(event: dict, context: LambdaContext) -> dict:
+ return app.resolve(event, context)
+
+
+if __name__ == "__main__":
+ print(
+ app.get_openapi_json_schema(
+ title="TODO's API",
+ version="1.21.3",
+ summary="API to manage TODOs",
+ description="This API implements all the CRUD operations for the TODO app",
+ tags=["todos"],
+ servers=[Server(url="https://stg.example.org/orders", description="Staging server")],
+ contact=Contact(name="John Smith", email="john@smith.com"),
+ ),
+ )
diff --git a/examples/event_handler_rest/src/customizing_api_operations.py b/examples/event_handler_rest/src/customizing_api_operations.py
new file mode 100644
index 00000000000..e455fc7dadd
--- /dev/null
+++ b/examples/event_handler_rest/src/customizing_api_operations.py
@@ -0,0 +1,30 @@
+import requests
+
+from aws_lambda_powertools.event_handler import APIGatewayRestResolver
+from aws_lambda_powertools.utilities.typing import LambdaContext
+
+app = APIGatewayRestResolver(enable_validation=True)
+
+
+@app.get(
+ "/todos/",
+ summary="Retrieves a todo item",
+ description="Loads a todo item identified by the `todo_id`",
+ response_description="The todo object",
+ responses={
+ 200: {"description": "Todo item found"},
+ 404: {
+ "description": "Item not found",
+ },
+ },
+ tags=["Todos"],
+)
+def get_todo_title(todo_id: int) -> str:
+ todo = requests.get(f"https://jsonplaceholder.typicode.com/todos/{todo_id}")
+ todo.raise_for_status()
+
+ return todo.json()["title"]
+
+
+def lambda_handler(event: dict, context: LambdaContext) -> dict:
+ return app.resolve(event, context)
diff --git a/examples/event_handler_rest/src/customizing_swagger.py b/examples/event_handler_rest/src/customizing_swagger.py
new file mode 100644
index 00000000000..4903ff25443
--- /dev/null
+++ b/examples/event_handler_rest/src/customizing_swagger.py
@@ -0,0 +1,29 @@
+from typing import List
+
+import requests
+from pydantic import BaseModel, EmailStr, Field
+
+from aws_lambda_powertools.event_handler import APIGatewayRestResolver
+from aws_lambda_powertools.utilities.typing import LambdaContext
+
+app = APIGatewayRestResolver(enable_validation=True)
+app.enable_swagger(path="/_swagger", swagger_base_url="https://cdn.example.com/path/to/assets/")
+
+
+class Todo(BaseModel):
+ userId: int
+ id_: int = Field(alias="id")
+ title: str
+ completed: bool
+
+
+@app.get("/todos")
+def get_todos_by_email(email: EmailStr) -> List[Todo]:
+ todos = requests.get(f"https://jsonplaceholder.typicode.com/todos?email={email}")
+ todos.raise_for_status()
+
+ return todos.json()
+
+
+def lambda_handler(event: dict, context: LambdaContext) -> dict:
+ return app.resolve(event, context)
diff --git a/examples/event_handler_rest/src/customizing_swagger_middlewares.py b/examples/event_handler_rest/src/customizing_swagger_middlewares.py
new file mode 100644
index 00000000000..49822fecefe
--- /dev/null
+++ b/examples/event_handler_rest/src/customizing_swagger_middlewares.py
@@ -0,0 +1,40 @@
+from typing import List
+
+import requests
+from pydantic import BaseModel, EmailStr, Field
+
+from aws_lambda_powertools.event_handler import APIGatewayRestResolver, Response
+from aws_lambda_powertools.event_handler.middlewares import NextMiddleware
+from aws_lambda_powertools.utilities.typing import LambdaContext
+
+app = APIGatewayRestResolver(enable_validation=True)
+
+
+def swagger_middleware(app: APIGatewayRestResolver, next_middleware: NextMiddleware) -> Response:
+ is_authenticated = ...
+ if not is_authenticated:
+ return Response(status_code=400, body="Unauthorized")
+
+ return next_middleware(app)
+
+
+app.enable_swagger(middlewares=[swagger_middleware])
+
+
+class Todo(BaseModel):
+ userId: int
+ id_: int = Field(alias="id")
+ title: str
+ completed: bool
+
+
+@app.get("/todos")
+def get_todos_by_email(email: EmailStr) -> List[Todo]:
+ todos = requests.get(f"https://jsonplaceholder.typicode.com/todos?email={email}")
+ todos.raise_for_status()
+
+ return todos.json()
+
+
+def lambda_handler(event: dict, context: LambdaContext) -> dict:
+ return app.resolve(event, context)
diff --git a/examples/event_handler_rest/src/data_validation.json b/examples/event_handler_rest/src/data_validation.json
new file mode 100644
index 00000000000..f5814ccaa26
--- /dev/null
+++ b/examples/event_handler_rest/src/data_validation.json
@@ -0,0 +1,36 @@
+{
+ "version": "1.0",
+ "resource": "/todos/1",
+ "path": "/todos/1",
+ "httpMethod": "GET",
+ "headers": {
+ "Origin": "https://aws.amazon.com"
+ },
+ "multiValueHeaders": {},
+ "queryStringParameters": {},
+ "multiValueQueryStringParameters": {},
+ "requestContext": {
+ "accountId": "123456789012",
+ "apiId": "id",
+ "authorizer": {
+ "claims": null,
+ "scopes": null
+ },
+ "domainName": "id.execute-api.us-east-1.amazonaws.com",
+ "domainPrefix": "id",
+ "extendedRequestId": "request-id",
+ "httpMethod": "GET",
+ "path": "/todos/1",
+ "protocol": "HTTP/1.1",
+ "requestId": "id=",
+ "requestTime": "04/Mar/2020:19:15:17 +0000",
+ "requestTimeEpoch": 1583349317135,
+ "resourceId": null,
+ "resourcePath": "/todos/1",
+ "stage": "$default"
+ },
+ "pathParameters": null,
+ "stageVariables": null,
+ "body": "",
+ "isBase64Encoded": false
+}
\ No newline at end of file
diff --git a/examples/event_handler_rest/src/data_validation.py b/examples/event_handler_rest/src/data_validation.py
new file mode 100644
index 00000000000..1daa9fb2174
--- /dev/null
+++ b/examples/event_handler_rest/src/data_validation.py
@@ -0,0 +1,35 @@
+from typing import Optional
+
+import requests
+from pydantic import BaseModel, Field
+
+from aws_lambda_powertools import Logger, Tracer
+from aws_lambda_powertools.event_handler import APIGatewayRestResolver
+from aws_lambda_powertools.logging import correlation_paths
+from aws_lambda_powertools.utilities.typing import LambdaContext
+
+tracer = Tracer()
+logger = Logger()
+app = APIGatewayRestResolver(enable_validation=True) # (1)!
+
+
+class Todo(BaseModel): # (2)!
+ userId: int
+ id_: Optional[int] = Field(alias="id", default=None)
+ title: str
+ completed: bool
+
+
+@app.get("/todos/") # (3)!
+@tracer.capture_method
+def get_todo_by_id(todo_id: int) -> Todo: # (4)!
+ todo = requests.get(f"https://jsonplaceholder.typicode.com/todos/{todo_id}")
+ todo.raise_for_status()
+
+ return todo.json() # (5)!
+
+
+@logger.inject_lambda_context(correlation_id_path=correlation_paths.API_GATEWAY_HTTP)
+@tracer.capture_lambda_handler
+def lambda_handler(event: dict, context: LambdaContext) -> dict:
+ return app.resolve(event, context)
diff --git a/examples/event_handler_rest/src/data_validation_error.json b/examples/event_handler_rest/src/data_validation_error.json
new file mode 100644
index 00000000000..6fc2636ad9c
--- /dev/null
+++ b/examples/event_handler_rest/src/data_validation_error.json
@@ -0,0 +1,42 @@
+{
+ "version": "2.0",
+ "routeKey": "$default",
+ "rawPath": "/todos/apples",
+ "rawQueryString": "",
+ "cookies": [
+ "cookie1",
+ "cookie2"
+ ],
+ "headers": {
+ "header1": "value1",
+ "header2": "value1,value2"
+ },
+ "queryStringParameters": {
+ "parameter1": "value1,value2",
+ "parameter2": "value"
+ },
+ "requestContext": {
+ "accountId": "123456789012",
+ "apiId": "api-id",
+ "domainName": "id.execute-api.us-east-1.amazonaws.com",
+ "domainPrefix": "id",
+ "http": {
+ "method": "GET",
+ "path": "/todos/apples",
+ "protocol": "HTTP/1.1",
+ "sourceIp": "192.0.2.1",
+ "userAgent": "agent"
+ },
+ "requestId": "id",
+ "routeKey": "$default",
+ "stage": "$default",
+ "time": "12/Mar/2020:19:03:58 +0000",
+ "timeEpoch": 1583348638390
+ },
+ "pathParameters": {},
+ "isBase64Encoded": false,
+ "stageVariables": {
+ "stageVariable1": "value1",
+ "stageVariable2": "value2"
+ }
+}
diff --git a/examples/event_handler_rest/src/data_validation_error_unsanitized_output.json b/examples/event_handler_rest/src/data_validation_error_unsanitized_output.json
new file mode 100644
index 00000000000..46d22c00eef
--- /dev/null
+++ b/examples/event_handler_rest/src/data_validation_error_unsanitized_output.json
@@ -0,0 +1,9 @@
+{
+ "statusCode": 422,
+ "body": "{\"statusCode\": 422, \"detail\": [{\"type\": \"int_parsing\", \"loc\": [\"path\", \"todo_id\"]}]}",
+ "isBase64Encoded": false,
+ "headers": {
+ "Content-Type": "application/json"
+ },
+ "cookies": []
+}
\ No newline at end of file
diff --git a/examples/event_handler_rest/src/data_validation_output.json b/examples/event_handler_rest/src/data_validation_output.json
new file mode 100644
index 00000000000..ec078c87078
--- /dev/null
+++ b/examples/event_handler_rest/src/data_validation_output.json
@@ -0,0 +1,10 @@
+{
+ "statusCode": 200,
+ "body": "Hello world",
+ "isBase64Encoded": false,
+ "multiValueHeaders": {
+ "Content-Type": [
+ "application/json"
+ ]
+ }
+}
\ No newline at end of file
diff --git a/examples/event_handler_rest/src/data_validation_sanitized_error.py b/examples/event_handler_rest/src/data_validation_sanitized_error.py
new file mode 100644
index 00000000000..71849938f48
--- /dev/null
+++ b/examples/event_handler_rest/src/data_validation_sanitized_error.py
@@ -0,0 +1,46 @@
+from typing import Optional
+
+import requests
+from pydantic import BaseModel, Field
+
+from aws_lambda_powertools import Logger, Tracer
+from aws_lambda_powertools.event_handler import APIGatewayRestResolver, Response, content_types
+from aws_lambda_powertools.event_handler.openapi.exceptions import RequestValidationError
+from aws_lambda_powertools.logging import correlation_paths
+from aws_lambda_powertools.utilities.typing import LambdaContext
+
+tracer = Tracer()
+logger = Logger()
+app = APIGatewayRestResolver(enable_validation=True)
+
+
+class Todo(BaseModel):
+ userId: int
+ id_: Optional[int] = Field(alias="id", default=None)
+ title: str
+ completed: bool
+
+
+@app.exception_handler(RequestValidationError) # (1)!
+def handle_validation_error(ex: RequestValidationError):
+ logger.error("Request failed validation", path=app.current_event.path, errors=ex.errors())
+
+ return Response(
+ status_code=422,
+ content_type=content_types.APPLICATION_JSON,
+ body="Invalid data",
+ )
+
+
+@app.post("/todos")
+def create_todo(todo: Todo) -> int:
+ response = requests.post("https://jsonplaceholder.typicode.com/todos", json=todo.dict(by_alias=True))
+ response.raise_for_status()
+
+ return response.json()["id"]
+
+
+@logger.inject_lambda_context(correlation_id_path=correlation_paths.API_GATEWAY_HTTP)
+@tracer.capture_lambda_handler
+def lambda_handler(event: dict, context: LambdaContext) -> dict:
+ return app.resolve(event, context)
diff --git a/examples/event_handler_rest/src/data_validation_sanitized_error_output.json b/examples/event_handler_rest/src/data_validation_sanitized_error_output.json
new file mode 100644
index 00000000000..aa6ab7e0d57
--- /dev/null
+++ b/examples/event_handler_rest/src/data_validation_sanitized_error_output.json
@@ -0,0 +1,9 @@
+{
+ "statusCode": 422,
+ "body": "Invalid data",
+ "isBase64Encoded": false,
+ "headers": {
+ "Content-Type": "application/json"
+ },
+ "cookies": []
+}
\ No newline at end of file
diff --git a/examples/event_handler_rest/src/enabling_swagger.py b/examples/event_handler_rest/src/enabling_swagger.py
new file mode 100644
index 00000000000..b624af77d32
--- /dev/null
+++ b/examples/event_handler_rest/src/enabling_swagger.py
@@ -0,0 +1,40 @@
+from typing import List
+
+import requests
+from pydantic import BaseModel, Field
+
+from aws_lambda_powertools import Logger, Tracer
+from aws_lambda_powertools.event_handler import APIGatewayRestResolver
+from aws_lambda_powertools.utilities.typing import LambdaContext
+
+tracer = Tracer()
+logger = Logger()
+app = APIGatewayRestResolver(enable_validation=True)
+app.enable_swagger(path="/swagger") # (1)!
+
+
+class Todo(BaseModel):
+ userId: int
+ id_: int = Field(alias="id")
+ title: str
+ completed: bool
+
+
+@app.post("/todos")
+def create_todo(todo: Todo) -> str:
+ response = requests.post("https://jsonplaceholder.typicode.com/todos", json=todo.dict(by_alias=True))
+ response.raise_for_status()
+
+ return response.json()["id"]
+
+
+@app.get("/todos")
+def get_todos() -> List[Todo]:
+ todo = requests.get("https://jsonplaceholder.typicode.com/todos")
+ todo.raise_for_status()
+
+ return todo.json()
+
+
+def lambda_handler(event: dict, context: LambdaContext) -> dict:
+ return app.resolve(event, context)
diff --git a/examples/event_handler_rest/src/skip_validating_query_strings.py b/examples/event_handler_rest/src/skip_validating_query_strings.py
new file mode 100644
index 00000000000..882769239a1
--- /dev/null
+++ b/examples/event_handler_rest/src/skip_validating_query_strings.py
@@ -0,0 +1,40 @@
+from typing import List, Optional
+
+import requests
+from pydantic import BaseModel, Field
+
+from aws_lambda_powertools import Logger, Tracer
+from aws_lambda_powertools.event_handler import APIGatewayRestResolver
+from aws_lambda_powertools.logging import correlation_paths
+from aws_lambda_powertools.utilities.typing import LambdaContext
+
+tracer = Tracer()
+logger = Logger()
+app = APIGatewayRestResolver(enable_validation=True)
+
+
+class Todo(BaseModel):
+ userId: int
+ id_: Optional[int] = Field(alias="id", default=None)
+ title: str
+ completed: bool
+
+
+@app.get("/todos")
+@tracer.capture_method
+def get_todos(completed: Optional[str] = None) -> List[Todo]: # (1)!
+ url = "https://jsonplaceholder.typicode.com/todos"
+
+ if completed is not None:
+ url = f"{url}/?completed={completed}"
+
+ todo = requests.get(url)
+ todo.raise_for_status()
+
+ return todo.json()
+
+
+@logger.inject_lambda_context(correlation_id_path=correlation_paths.API_GATEWAY_HTTP)
+@tracer.capture_lambda_handler
+def lambda_handler(event: dict, context: LambdaContext) -> dict:
+ return app.resolve(event, context)
diff --git a/examples/event_handler_rest/src/validating_path.py b/examples/event_handler_rest/src/validating_path.py
new file mode 100644
index 00000000000..e892e1c8597
--- /dev/null
+++ b/examples/event_handler_rest/src/validating_path.py
@@ -0,0 +1,37 @@
+from typing import Optional
+
+import requests
+from pydantic import BaseModel, Field
+
+from aws_lambda_powertools import Logger, Tracer
+from aws_lambda_powertools.event_handler import APIGatewayRestResolver
+from aws_lambda_powertools.event_handler.openapi.params import Path
+from aws_lambda_powertools.logging import correlation_paths
+from aws_lambda_powertools.shared.types import Annotated
+from aws_lambda_powertools.utilities.typing import LambdaContext
+
+tracer = Tracer()
+logger = Logger()
+app = APIGatewayRestResolver(enable_validation=True)
+
+
+class Todo(BaseModel):
+ userId: int
+ id_: Optional[int] = Field(alias="id", default=None)
+ title: str
+ completed: bool
+
+
+@app.get("/todos/")
+@tracer.capture_method
+def get_todo_by_id(todo_id: Annotated[int, Path(lt=999)]) -> Todo: # (1)!
+ todo = requests.get(f"https://jsonplaceholder.typicode.com/todos/{todo_id}")
+ todo.raise_for_status()
+
+ return todo.json()
+
+
+@logger.inject_lambda_context(correlation_id_path=correlation_paths.API_GATEWAY_HTTP)
+@tracer.capture_lambda_handler
+def lambda_handler(event: dict, context: LambdaContext) -> dict:
+ return app.resolve(event, context)
diff --git a/examples/event_handler_rest/src/validating_payload_subset.json b/examples/event_handler_rest/src/validating_payload_subset.json
new file mode 100644
index 00000000000..b786e9287b8
--- /dev/null
+++ b/examples/event_handler_rest/src/validating_payload_subset.json
@@ -0,0 +1,36 @@
+{
+ "version": "1.0",
+ "body": "{ \"todo\": {\"title\": \"foo\", \"userId\": \"1\", \"completed\": false } }",
+ "resource": "/todos",
+ "path": "/todos",
+ "httpMethod": "POST",
+ "headers": {
+ "Origin": "https://aws.amazon.com"
+ },
+ "multiValueHeaders": {},
+ "queryStringParameters": {},
+ "multiValueQueryStringParameters": {},
+ "requestContext": {
+ "accountId": "123456789012",
+ "apiId": "id",
+ "authorizer": {
+ "claims": null,
+ "scopes": null
+ },
+ "domainName": "id.execute-api.us-east-1.amazonaws.com",
+ "domainPrefix": "id",
+ "extendedRequestId": "request-id",
+ "httpMethod": "POST",
+ "path": "/todos",
+ "protocol": "HTTP/1.1",
+ "requestId": "id=",
+ "requestTime": "04/Mar/2020:19:15:17 +0000",
+ "requestTimeEpoch": 1583349317135,
+ "resourceId": null,
+ "resourcePath": "/todos",
+ "stage": "$default"
+ },
+ "pathParameters": null,
+ "stageVariables": null,
+ "isBase64Encoded": false
+}
\ No newline at end of file
diff --git a/examples/event_handler_rest/src/validating_payload_subset.py b/examples/event_handler_rest/src/validating_payload_subset.py
new file mode 100644
index 00000000000..ac4ee603853
--- /dev/null
+++ b/examples/event_handler_rest/src/validating_payload_subset.py
@@ -0,0 +1,30 @@
+from typing import Optional
+
+import requests
+from pydantic import BaseModel, Field
+
+from aws_lambda_powertools.event_handler import APIGatewayRestResolver
+from aws_lambda_powertools.event_handler.openapi.params import Body # (1)!
+from aws_lambda_powertools.shared.types import Annotated
+from aws_lambda_powertools.utilities.typing import LambdaContext
+
+app = APIGatewayRestResolver(enable_validation=True)
+
+
+class Todo(BaseModel):
+ userId: int
+ id_: Optional[int] = Field(alias="id", default=None)
+ title: str
+ completed: bool
+
+
+@app.post("/todos")
+def create_todo(todo: Annotated[Todo, Body(embed=True)]) -> int: # (2)!
+ response = requests.post("https://jsonplaceholder.typicode.com/todos", json=todo.dict(by_alias=True))
+ response.raise_for_status()
+
+ return response.json()["id"]
+
+
+def lambda_handler(event: dict, context: LambdaContext) -> dict:
+ return app.resolve(event, context)
diff --git a/examples/event_handler_rest/src/validating_payload_subset_output.json b/examples/event_handler_rest/src/validating_payload_subset_output.json
new file mode 100644
index 00000000000..754e3a6c128
--- /dev/null
+++ b/examples/event_handler_rest/src/validating_payload_subset_output.json
@@ -0,0 +1,10 @@
+{
+ "statusCode": 200,
+ "body": "2008822",
+ "isBase64Encoded": false,
+ "multiValueHeaders": {
+ "Content-Type": [
+ "application/json"
+ ]
+ }
+}
diff --git a/examples/event_handler_rest/src/validating_payloads.json b/examples/event_handler_rest/src/validating_payloads.json
new file mode 100644
index 00000000000..125405e0cf2
--- /dev/null
+++ b/examples/event_handler_rest/src/validating_payloads.json
@@ -0,0 +1,36 @@
+{
+ "version": "1.0",
+ "body": "{\"title\": \"foo\", \"userId\": \"1\", \"completed\": false}",
+ "resource": "/todos",
+ "path": "/todos",
+ "httpMethod": "POST",
+ "headers": {
+ "Origin": "https://aws.amazon.com"
+ },
+ "multiValueHeaders": {},
+ "queryStringParameters": {},
+ "multiValueQueryStringParameters": {},
+ "requestContext": {
+ "accountId": "123456789012",
+ "apiId": "id",
+ "authorizer": {
+ "claims": null,
+ "scopes": null
+ },
+ "domainName": "id.execute-api.us-east-1.amazonaws.com",
+ "domainPrefix": "id",
+ "extendedRequestId": "request-id",
+ "httpMethod": "POST",
+ "path": "/todos",
+ "protocol": "HTTP/1.1",
+ "requestId": "id=",
+ "requestTime": "04/Mar/2020:19:15:17 +0000",
+ "requestTimeEpoch": 1583349317135,
+ "resourceId": null,
+ "resourcePath": "/todos",
+ "stage": "$default"
+ },
+ "pathParameters": null,
+ "stageVariables": null,
+ "isBase64Encoded": false
+}
\ No newline at end of file
diff --git a/examples/event_handler_rest/src/validating_payloads.py b/examples/event_handler_rest/src/validating_payloads.py
new file mode 100644
index 00000000000..945cefd8089
--- /dev/null
+++ b/examples/event_handler_rest/src/validating_payloads.py
@@ -0,0 +1,43 @@
+from typing import List, Optional
+
+import requests
+from pydantic import BaseModel, Field
+
+from aws_lambda_powertools import Logger, Tracer
+from aws_lambda_powertools.event_handler import APIGatewayRestResolver
+from aws_lambda_powertools.logging import correlation_paths
+from aws_lambda_powertools.utilities.typing import LambdaContext
+
+tracer = Tracer()
+logger = Logger()
+app = APIGatewayRestResolver(enable_validation=True) # (1)!
+
+
+class Todo(BaseModel): # (2)!
+ userId: int
+ id_: Optional[int] = Field(alias="id", default=None)
+ title: str
+ completed: bool
+
+
+@app.post("/todos")
+def create_todo(todo: Todo) -> str: # (3)!
+ response = requests.post("https://jsonplaceholder.typicode.com/todos", json=todo.dict(by_alias=True))
+ response.raise_for_status()
+
+ return response.json()["id"] # (4)!
+
+
+@app.get("/todos")
+@tracer.capture_method
+def get_todos() -> List[Todo]:
+ todo = requests.get("https://jsonplaceholder.typicode.com/todos")
+ todo.raise_for_status()
+
+ return todo.json() # (5)!
+
+
+@logger.inject_lambda_context(correlation_id_path=correlation_paths.API_GATEWAY_HTTP)
+@tracer.capture_lambda_handler
+def lambda_handler(event: dict, context: LambdaContext) -> dict:
+ return app.resolve(event, context)
diff --git a/examples/event_handler_rest/src/validating_payloads_output.json b/examples/event_handler_rest/src/validating_payloads_output.json
new file mode 100644
index 00000000000..9d72764c3c8
--- /dev/null
+++ b/examples/event_handler_rest/src/validating_payloads_output.json
@@ -0,0 +1,10 @@
+{
+ "statusCode": 200,
+ "body": "2008821",
+ "isBase64Encoded": false,
+ "multiValueHeaders": {
+ "Content-Type": [
+ "application/json"
+ ]
+ }
+}
\ No newline at end of file
diff --git a/examples/event_handler_rest/src/validating_query_strings.py b/examples/event_handler_rest/src/validating_query_strings.py
new file mode 100644
index 00000000000..21d34dbd25a
--- /dev/null
+++ b/examples/event_handler_rest/src/validating_query_strings.py
@@ -0,0 +1,42 @@
+from typing import List, Optional
+
+import requests
+from pydantic import BaseModel, Field
+
+from aws_lambda_powertools import Logger, Tracer
+from aws_lambda_powertools.event_handler import APIGatewayRestResolver
+from aws_lambda_powertools.event_handler.openapi.params import Query # (2)!
+from aws_lambda_powertools.logging import correlation_paths
+from aws_lambda_powertools.shared.types import Annotated # (1)!
+from aws_lambda_powertools.utilities.typing import LambdaContext
+
+tracer = Tracer()
+logger = Logger()
+app = APIGatewayRestResolver(enable_validation=True)
+
+
+class Todo(BaseModel):
+ userId: int
+ id_: Optional[int] = Field(alias="id", default=None)
+ title: str
+ completed: bool
+
+
+@app.get("/todos")
+@tracer.capture_method
+def get_todos(completed: Annotated[Optional[str], Query(min_length=4)] = None) -> List[Todo]: # (3)!
+ url = "https://jsonplaceholder.typicode.com/todos"
+
+ if completed is not None:
+ url = f"{url}/?completed={completed}"
+
+ todo = requests.get(url)
+ todo.raise_for_status()
+
+ return todo.json()
+
+
+@logger.inject_lambda_context(correlation_id_path=correlation_paths.API_GATEWAY_HTTP)
+@tracer.capture_lambda_handler
+def lambda_handler(event: dict, context: LambdaContext) -> dict:
+ return app.resolve(event, context)