diff --git a/docs/utilities/validation.md b/docs/utilities/validation.md
index 928ffb6c8..dfd97e0d4 100644
--- a/docs/utilities/validation.md
+++ b/docs/utilities/validation.md
@@ -156,7 +156,10 @@ We support JSON schema version 4, 6, 7 and 201909 (from [jmespath-jackson librar
`@Validation` annotation is used to validate either inbound events or functions' response.
-It will fail fast with `ValidationException` if an event or response doesn't conform with given JSON Schema.
+It will fail fast if an event or response doesn't conform with given JSON Schema. For most type of events a `ValidationException` will be thrown.
+For API gateway events associated with REST APIs and HTTP APIs - `APIGatewayProxyRequestEvent` and `APIGatewayV2HTTPEvent` - the `@Validation`
+annotation will build and return a custom 400 / "Bad Request" response, with a body containing the validation errors. This saves you from having
+to catch the validation exception and map it back to a meaningful user error yourself.
While it is easier to specify a json schema file in the classpath (using the notation `"classpath:/path/to/schema.json"`), you can also provide a JSON String containing the schema.
diff --git a/powertools-e2e-tests/handlers/pom.xml b/powertools-e2e-tests/handlers/pom.xml
index 1bda3caa5..7c1208470 100644
--- a/powertools-e2e-tests/handlers/pom.xml
+++ b/powertools-e2e-tests/handlers/pom.xml
@@ -32,6 +32,7 @@
metrics
idempotency
parameters
+ validation
@@ -83,6 +84,11 @@
powertools-batch
${lambda.powertools.version}
+
+ software.amazon.lambda
+ powertools-validation
+ ${lambda.powertools.version}
+
com.amazonaws
aws-lambda-java-core
diff --git a/powertools-e2e-tests/handlers/validation-alb-event/pom.xml b/powertools-e2e-tests/handlers/validation-alb-event/pom.xml
new file mode 100644
index 000000000..31570fe4e
--- /dev/null
+++ b/powertools-e2e-tests/handlers/validation-alb-event/pom.xml
@@ -0,0 +1,60 @@
+
+ 4.0.0
+
+
+ software.amazon.lambda
+ e2e-test-handlers-parent
+ 1.0.0
+
+
+ e2e-test-handler-validation-alb-event
+ jar
+ A Lambda function using Powertools for AWS Lambda (Java) validation
+
+
+
+ software.amazon.lambda
+ powertools-validation
+
+
+ com.amazonaws
+ aws-lambda-java-events
+
+
+ org.aspectj
+ aspectjrt
+
+
+
+
+
+
+ dev.aspectj
+ aspectj-maven-plugin
+
+ ${maven.compiler.source}
+ ${maven.compiler.target}
+ ${maven.compiler.target}
+
+
+ software.amazon.lambda
+ powertools-validation
+
+
+
+
+
+
+ compile
+
+
+
+
+
+ org.apache.maven.plugins
+ maven-shade-plugin
+
+
+
+
diff --git a/powertools-e2e-tests/handlers/validation-alb-event/src/main/java/software/amazon/lambda/powertools/e2e/Function.java b/powertools-e2e-tests/handlers/validation-alb-event/src/main/java/software/amazon/lambda/powertools/e2e/Function.java
new file mode 100644
index 000000000..d221ee153
--- /dev/null
+++ b/powertools-e2e-tests/handlers/validation-alb-event/src/main/java/software/amazon/lambda/powertools/e2e/Function.java
@@ -0,0 +1,36 @@
+/*
+ * Copyright 2023 Amazon.com, Inc. or its affiliates.
+ * Licensed under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ * http://www.apache.org/licenses/LICENSE-2.0
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ *
+ */
+
+package software.amazon.lambda.powertools.e2e;
+
+import com.amazonaws.services.lambda.runtime.Context;
+import com.amazonaws.services.lambda.runtime.RequestHandler;
+import com.amazonaws.services.lambda.runtime.events.ApplicationLoadBalancerRequestEvent;
+import com.amazonaws.services.lambda.runtime.events.ApplicationLoadBalancerResponseEvent;
+import com.amazonaws.services.lambda.runtime.events.SQSBatchResponse;
+
+import software.amazon.lambda.powertools.validation.Validation;
+
+public class Function
+ implements RequestHandler {
+ @Validation(inboundSchema = "classpath:/validation/inbound_schema.json", outboundSchema = "classpath:/validation/outbound_schema.json")
+ public ApplicationLoadBalancerResponseEvent handleRequest(ApplicationLoadBalancerRequestEvent input,
+ Context context) {
+ ApplicationLoadBalancerResponseEvent response = new ApplicationLoadBalancerResponseEvent();
+ response.setBody(input.getBody());
+ response.setStatusCode(200);
+ response.setIsBase64Encoded(false);
+ return response;
+ }
+}
\ No newline at end of file
diff --git a/powertools-e2e-tests/handlers/validation-alb-event/src/main/resources/log4j2.xml b/powertools-e2e-tests/handlers/validation-alb-event/src/main/resources/log4j2.xml
new file mode 100644
index 000000000..8925f70b9
--- /dev/null
+++ b/powertools-e2e-tests/handlers/validation-alb-event/src/main/resources/log4j2.xml
@@ -0,0 +1,16 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/powertools-e2e-tests/handlers/validation-alb-event/src/main/resources/validation/inbound_schema.json b/powertools-e2e-tests/handlers/validation-alb-event/src/main/resources/validation/inbound_schema.json
new file mode 100644
index 000000000..3665879eb
--- /dev/null
+++ b/powertools-e2e-tests/handlers/validation-alb-event/src/main/resources/validation/inbound_schema.json
@@ -0,0 +1,32 @@
+{
+ "$schema": "http://json-schema.org/draft-07/schema",
+ "$id": "http://example.com/product.json",
+ "type": "object",
+ "title": "Product schema",
+ "description": "JSON schema to validate Products",
+ "default": {},
+ "examples": [
+ {
+ "id": 43242,
+ "name": "FooBar XY",
+ "price": 258
+ }
+ ],
+ "required": [
+ "price"
+ ],
+ "properties": {
+ "price": {
+ "$id": "#/properties/price",
+ "type": "number",
+ "title": "Price of the product",
+ "description": "Positive price of the product",
+ "default": 0,
+ "exclusiveMinimum": 0,
+ "examples": [
+ 258.99
+ ]
+ }
+ },
+ "additionalProperties": true
+}
\ No newline at end of file
diff --git a/powertools-e2e-tests/handlers/validation-alb-event/src/main/resources/validation/outbound_schema.json b/powertools-e2e-tests/handlers/validation-alb-event/src/main/resources/validation/outbound_schema.json
new file mode 100644
index 000000000..b1f14d025
--- /dev/null
+++ b/powertools-e2e-tests/handlers/validation-alb-event/src/main/resources/validation/outbound_schema.json
@@ -0,0 +1,32 @@
+{
+ "$schema": "http://json-schema.org/draft-07/schema",
+ "$id": "http://example.com/product.json",
+ "type": "object",
+ "title": "Product schema",
+ "description": "JSON schema to validate Products",
+ "default": {},
+ "examples": [
+ {
+ "id": 43242,
+ "name": "FooBar XY",
+ "price": 258
+ }
+ ],
+ "required": [
+ "price"
+ ],
+ "properties": {
+ "price": {
+ "$id": "#/properties/price",
+ "type": "number",
+ "title": "Price of the product",
+ "description": "Positive price of the product",
+ "default": 0,
+ "exclusiveMaximum": 1000,
+ "examples": [
+ 258.99
+ ]
+ }
+ },
+ "additionalProperties": true
+}
\ No newline at end of file
diff --git a/powertools-e2e-tests/handlers/validation-apigw-event/pom.xml b/powertools-e2e-tests/handlers/validation-apigw-event/pom.xml
new file mode 100644
index 000000000..9129abc7d
--- /dev/null
+++ b/powertools-e2e-tests/handlers/validation-apigw-event/pom.xml
@@ -0,0 +1,60 @@
+
+ 4.0.0
+
+
+ software.amazon.lambda
+ e2e-test-handlers-parent
+ 1.0.0
+
+
+ e2e-test-handler-validation-apigw-event
+ jar
+ A Lambda function using Powertools for AWS Lambda (Java) validation
+
+
+
+ software.amazon.lambda
+ powertools-validation
+
+
+ com.amazonaws
+ aws-lambda-java-events
+
+
+ org.aspectj
+ aspectjrt
+
+
+
+
+
+
+ dev.aspectj
+ aspectj-maven-plugin
+
+ ${maven.compiler.source}
+ ${maven.compiler.target}
+ ${maven.compiler.target}
+
+
+ software.amazon.lambda
+ powertools-validation
+
+
+
+
+
+
+ compile
+
+
+
+
+
+ org.apache.maven.plugins
+ maven-shade-plugin
+
+
+
+
diff --git a/powertools-e2e-tests/handlers/validation-apigw-event/src/main/java/software/amazon/lambda/powertools/e2e/Function.java b/powertools-e2e-tests/handlers/validation-apigw-event/src/main/java/software/amazon/lambda/powertools/e2e/Function.java
new file mode 100644
index 000000000..5ac8951d8
--- /dev/null
+++ b/powertools-e2e-tests/handlers/validation-apigw-event/src/main/java/software/amazon/lambda/powertools/e2e/Function.java
@@ -0,0 +1,33 @@
+/*
+ * Copyright 2023 Amazon.com, Inc. or its affiliates.
+ * Licensed under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ * http://www.apache.org/licenses/LICENSE-2.0
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ *
+ */
+
+package software.amazon.lambda.powertools.e2e;
+
+import com.amazonaws.services.lambda.runtime.Context;
+import com.amazonaws.services.lambda.runtime.RequestHandler;
+import com.amazonaws.services.lambda.runtime.events.APIGatewayProxyRequestEvent;
+import com.amazonaws.services.lambda.runtime.events.APIGatewayProxyResponseEvent;
+
+import software.amazon.lambda.powertools.validation.Validation;
+
+public class Function implements RequestHandler {
+ @Validation(inboundSchema = "classpath:/validation/inbound_schema.json", outboundSchema = "classpath:/validation/outbound_schema.json")
+ public APIGatewayProxyResponseEvent handleRequest(APIGatewayProxyRequestEvent input, Context context) {
+ APIGatewayProxyResponseEvent response = new APIGatewayProxyResponseEvent();
+ response.setBody(input.getBody());
+ response.setStatusCode(200);
+ response.setIsBase64Encoded(false);
+ return response;
+ }
+}
\ No newline at end of file
diff --git a/powertools-e2e-tests/handlers/validation-apigw-event/src/main/resources/log4j2.xml b/powertools-e2e-tests/handlers/validation-apigw-event/src/main/resources/log4j2.xml
new file mode 100644
index 000000000..8925f70b9
--- /dev/null
+++ b/powertools-e2e-tests/handlers/validation-apigw-event/src/main/resources/log4j2.xml
@@ -0,0 +1,16 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/powertools-e2e-tests/handlers/validation-apigw-event/src/main/resources/validation/inbound_schema.json b/powertools-e2e-tests/handlers/validation-apigw-event/src/main/resources/validation/inbound_schema.json
new file mode 100644
index 000000000..3665879eb
--- /dev/null
+++ b/powertools-e2e-tests/handlers/validation-apigw-event/src/main/resources/validation/inbound_schema.json
@@ -0,0 +1,32 @@
+{
+ "$schema": "http://json-schema.org/draft-07/schema",
+ "$id": "http://example.com/product.json",
+ "type": "object",
+ "title": "Product schema",
+ "description": "JSON schema to validate Products",
+ "default": {},
+ "examples": [
+ {
+ "id": 43242,
+ "name": "FooBar XY",
+ "price": 258
+ }
+ ],
+ "required": [
+ "price"
+ ],
+ "properties": {
+ "price": {
+ "$id": "#/properties/price",
+ "type": "number",
+ "title": "Price of the product",
+ "description": "Positive price of the product",
+ "default": 0,
+ "exclusiveMinimum": 0,
+ "examples": [
+ 258.99
+ ]
+ }
+ },
+ "additionalProperties": true
+}
\ No newline at end of file
diff --git a/powertools-e2e-tests/handlers/validation-apigw-event/src/main/resources/validation/outbound_schema.json b/powertools-e2e-tests/handlers/validation-apigw-event/src/main/resources/validation/outbound_schema.json
new file mode 100644
index 000000000..b1f14d025
--- /dev/null
+++ b/powertools-e2e-tests/handlers/validation-apigw-event/src/main/resources/validation/outbound_schema.json
@@ -0,0 +1,32 @@
+{
+ "$schema": "http://json-schema.org/draft-07/schema",
+ "$id": "http://example.com/product.json",
+ "type": "object",
+ "title": "Product schema",
+ "description": "JSON schema to validate Products",
+ "default": {},
+ "examples": [
+ {
+ "id": 43242,
+ "name": "FooBar XY",
+ "price": 258
+ }
+ ],
+ "required": [
+ "price"
+ ],
+ "properties": {
+ "price": {
+ "$id": "#/properties/price",
+ "type": "number",
+ "title": "Price of the product",
+ "description": "Positive price of the product",
+ "default": 0,
+ "exclusiveMaximum": 1000,
+ "examples": [
+ 258.99
+ ]
+ }
+ },
+ "additionalProperties": true
+}
\ No newline at end of file
diff --git a/powertools-e2e-tests/src/test/java/software/amazon/lambda/powertools/ValidationALBE2ET.java b/powertools-e2e-tests/src/test/java/software/amazon/lambda/powertools/ValidationALBE2ET.java
new file mode 100644
index 000000000..324c77a34
--- /dev/null
+++ b/powertools-e2e-tests/src/test/java/software/amazon/lambda/powertools/ValidationALBE2ET.java
@@ -0,0 +1,104 @@
+/*
+ * Copyright 2023 Amazon.com, Inc. or its affiliates.
+ * Licensed under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ * http://www.apache.org/licenses/LICENSE-2.0
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ *
+ */
+
+package software.amazon.lambda.powertools;
+
+import static org.assertj.core.api.Assertions.assertThat;
+import static software.amazon.lambda.powertools.testutils.Infrastructure.FUNCTION_NAME_OUTPUT;
+import static software.amazon.lambda.powertools.testutils.lambda.LambdaInvoker.invokeFunction;
+
+import java.io.IOException;
+import java.io.InputStream;
+import java.nio.charset.StandardCharsets;
+import java.util.Map;
+import java.util.concurrent.TimeUnit;
+
+import org.apache.commons.io.IOUtils;
+import org.junit.jupiter.api.AfterAll;
+import org.junit.jupiter.api.BeforeAll;
+import org.junit.jupiter.api.Test;
+import org.junit.jupiter.api.Timeout;
+
+import com.fasterxml.jackson.databind.JsonNode;
+import com.fasterxml.jackson.databind.ObjectMapper;
+
+import software.amazon.lambda.powertools.testutils.Infrastructure;
+import software.amazon.lambda.powertools.testutils.lambda.InvocationResult;
+
+class ValidationALBE2ET {
+
+ private static final ObjectMapper objectMapper = new ObjectMapper();
+
+ private static Infrastructure infrastructure;
+ private static String functionName;
+
+ @BeforeAll
+ @Timeout(value = 5, unit = TimeUnit.MINUTES)
+ public static void setup() {
+ infrastructure = Infrastructure.builder().testName(ValidationALBE2ET.class.getSimpleName())
+ .pathToFunction("validation-alb-event").build();
+ Map outputs = infrastructure.deploy();
+ functionName = outputs.get(FUNCTION_NAME_OUTPUT);
+ }
+
+ @AfterAll
+ public static void tearDown() {
+ if (infrastructure != null) {
+ infrastructure.destroy();
+ }
+ }
+
+ @Test
+ void test_validInboundSQSEvent() throws IOException {
+ InputStream inputStream = this.getClass().getResourceAsStream("/validation/valid_alb_in_out_event.json");
+ String validEvent = IOUtils.toString(inputStream, StandardCharsets.UTF_8);
+
+ // WHEN
+ InvocationResult invocationResult = invokeFunction(functionName, validEvent);
+
+ // THEN
+ // invocation should pass validation and return 200
+ JsonNode validJsonNode = objectMapper.readTree(invocationResult.getResult());
+ assertThat(validJsonNode.get("statusCode").asInt()).isEqualTo(200);
+ assertThat(validJsonNode.get("body").asText()).isEqualTo("{\"price\": 150}");
+ }
+
+ @Test
+ void test_invalidInboundSQSEvent() throws IOException {
+ InputStream inputStream = this.getClass().getResourceAsStream("/validation/invalid_alb_in_event.json");
+ String invalidEvent = IOUtils.toString(inputStream, StandardCharsets.UTF_8);
+
+ // WHEN
+ InvocationResult invocationResult = invokeFunction(functionName, invalidEvent);
+
+ // THEN
+ // invocation should fail inbound validation and return an error message
+ JsonNode validJsonNode = objectMapper.readTree(invocationResult.getResult());
+ assertThat(validJsonNode.get("errorMessage").asText()).contains("$.price: is missing but it is required");
+ }
+
+ @Test
+ void test_invalidOutboundSQSEvent() throws IOException {
+ InputStream inputStream = this.getClass().getResourceAsStream("/validation/invalid_alb_out_event.json");
+ String invalidEvent = IOUtils.toString(inputStream, StandardCharsets.UTF_8);
+
+ // WHEN
+ InvocationResult invocationResult = invokeFunction(functionName, invalidEvent);
+
+ // THEN
+ // invocation should fail outbound validation and return 400
+ JsonNode validJsonNode = objectMapper.readTree(invocationResult.getResult());
+ assertThat(validJsonNode.get("errorMessage").asText()).contains("$.price: must have an exclusive maximum value of 1000");
+ }
+}
diff --git a/powertools-e2e-tests/src/test/java/software/amazon/lambda/powertools/ValidationApiGWE2ET.java b/powertools-e2e-tests/src/test/java/software/amazon/lambda/powertools/ValidationApiGWE2ET.java
new file mode 100644
index 000000000..af7c7d87c
--- /dev/null
+++ b/powertools-e2e-tests/src/test/java/software/amazon/lambda/powertools/ValidationApiGWE2ET.java
@@ -0,0 +1,107 @@
+/*
+ * Copyright 2023 Amazon.com, Inc. or its affiliates.
+ * Licensed under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ * http://www.apache.org/licenses/LICENSE-2.0
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ *
+ */
+
+package software.amazon.lambda.powertools;
+
+import static org.assertj.core.api.Assertions.assertThat;
+import static software.amazon.lambda.powertools.testutils.Infrastructure.FUNCTION_NAME_OUTPUT;
+import static software.amazon.lambda.powertools.testutils.lambda.LambdaInvoker.invokeFunction;
+
+import java.io.IOException;
+import java.io.InputStream;
+import java.nio.charset.StandardCharsets;
+import java.util.Map;
+import java.util.concurrent.TimeUnit;
+
+import org.apache.commons.io.IOUtils;
+import org.junit.jupiter.api.AfterAll;
+import org.junit.jupiter.api.BeforeAll;
+import org.junit.jupiter.api.Test;
+import org.junit.jupiter.api.Timeout;
+
+import com.fasterxml.jackson.databind.JsonNode;
+import com.fasterxml.jackson.databind.ObjectMapper;
+
+import software.amazon.lambda.powertools.testutils.Infrastructure;
+import software.amazon.lambda.powertools.testutils.lambda.InvocationResult;
+
+class ValidationApiGWE2ET {
+
+ private static final ObjectMapper objectMapper = new ObjectMapper();
+
+ private static Infrastructure infrastructure;
+ private static String functionName;
+
+ @BeforeAll
+ @Timeout(value = 5, unit = TimeUnit.MINUTES)
+ public static void setup() {
+ infrastructure = Infrastructure.builder().testName(ValidationApiGWE2ET.class.getSimpleName())
+ .pathToFunction("validation-apigw-event").build();
+ Map outputs = infrastructure.deploy();
+ functionName = outputs.get(FUNCTION_NAME_OUTPUT);
+ }
+
+ @AfterAll
+ public static void tearDown() {
+ if (infrastructure != null) {
+ infrastructure.destroy();
+ }
+ }
+
+ @Test
+ void test_validInboundApiGWEvent() throws IOException {
+ InputStream inputStream = this.getClass().getResourceAsStream("/validation/valid_api_gw_in_out_event.json");
+ String validEvent = IOUtils.toString(inputStream, StandardCharsets.UTF_8);
+
+ // WHEN
+ InvocationResult invocationResult = invokeFunction(functionName, validEvent);
+
+ // THEN
+ // invocation should pass validation and return 200
+ JsonNode validJsonNode = objectMapper.readTree(invocationResult.getResult());
+ assertThat(validJsonNode.get("statusCode").asInt()).isEqualTo(200);
+ assertThat(validJsonNode.get("body").asText()).isEqualTo("{\"price\": 150}");
+ }
+
+ @Test
+ void test_invalidInboundApiGWEvent() throws IOException {
+ InputStream inputStream = this.getClass().getResourceAsStream("/validation/invalid_api_gw_in_event.json");
+ String invalidEvent = IOUtils.toString(inputStream, StandardCharsets.UTF_8);
+
+ // WHEN
+ InvocationResult invocationResult = invokeFunction(functionName, invalidEvent);
+
+ // THEN
+ // invocation should fail inbound validation and return 400
+ JsonNode validJsonNode = objectMapper.readTree(invocationResult.getResult());
+ assertThat(validJsonNode.get("statusCode").asInt()).isEqualTo(400);
+ assertThat(validJsonNode.get("body").asText()).contains("$.price: is missing but it is required");
+ }
+
+ @Test
+ void test_invalidOutboundApiGWEvent() throws IOException {
+ InputStream inputStream = this.getClass().getResourceAsStream("/validation/invalid_api_gw_out_event.json");
+ String invalidEvent = IOUtils.toString(inputStream, StandardCharsets.UTF_8);
+
+ // WHEN
+ InvocationResult invocationResult = invokeFunction(functionName, invalidEvent);
+
+ // THEN
+ // invocation should fail outbound validation and return 400
+ JsonNode validJsonNode = objectMapper.readTree(invocationResult.getResult());
+ assertThat(validJsonNode.get("statusCode").asInt()).isEqualTo(400);
+ assertThat(validJsonNode.get("body").asText())
+ .contains("$.price: must have an exclusive maximum value of 1000");
+ }
+}
diff --git a/powertools-e2e-tests/src/test/resources/validation/invalid_alb_in_event.json b/powertools-e2e-tests/src/test/resources/validation/invalid_alb_in_event.json
new file mode 100644
index 000000000..ebad834d8
--- /dev/null
+++ b/powertools-e2e-tests/src/test/resources/validation/invalid_alb_in_event.json
@@ -0,0 +1,28 @@
+{
+ "requestContext": {
+ "elb": {
+ "targetGroupArn": "arn:aws:elasticloadbalancing:us-east-2:123456789012:targetgroup/lambda-279XGJDqGZ5rsrHC2Fjr/49e9d65c45c6791a"
+ }
+ },
+ "httpMethod": "POST",
+ "path": "/path/to/resource",
+ "queryStringParameters": {
+ "query": "1234ABCD"
+ },
+ "headers": {
+ "accept": "text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,image/apng,*/*;q=0.8",
+ "accept-encoding": "gzip",
+ "accept-language": "en-US,en;q=0.9",
+ "connection": "keep-alive",
+ "host": "lambda-alb-123578498.us-east-2.elb.amazonaws.com",
+ "upgrade-insecure-requests": "1",
+ "user-agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/71.0.3578.98 Safari/537.36",
+ "x-amzn-trace-id": "Root=1-5c536348-3d683b8b04734faae651f476",
+ "x-forwarded-for": "72.12.164.125",
+ "x-forwarded-port": "80",
+ "x-forwarded-proto": "http",
+ "x-imforwards": "20"
+ },
+ "body": "{\"message\": \"Lambda rocks\"}",
+ "isBase64Encoded": true
+}
\ No newline at end of file
diff --git a/powertools-e2e-tests/src/test/resources/validation/invalid_alb_out_event.json b/powertools-e2e-tests/src/test/resources/validation/invalid_alb_out_event.json
new file mode 100644
index 000000000..1b1961063
--- /dev/null
+++ b/powertools-e2e-tests/src/test/resources/validation/invalid_alb_out_event.json
@@ -0,0 +1,28 @@
+{
+ "requestContext": {
+ "elb": {
+ "targetGroupArn": "arn:aws:elasticloadbalancing:us-east-2:123456789012:targetgroup/lambda-279XGJDqGZ5rsrHC2Fjr/49e9d65c45c6791a"
+ }
+ },
+ "httpMethod": "POST",
+ "path": "/path/to/resource",
+ "queryStringParameters": {
+ "query": "1234ABCD"
+ },
+ "headers": {
+ "accept": "text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,image/apng,*/*;q=0.8",
+ "accept-encoding": "gzip",
+ "accept-language": "en-US,en;q=0.9",
+ "connection": "keep-alive",
+ "host": "lambda-alb-123578498.us-east-2.elb.amazonaws.com",
+ "upgrade-insecure-requests": "1",
+ "user-agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/71.0.3578.98 Safari/537.36",
+ "x-amzn-trace-id": "Root=1-5c536348-3d683b8b04734faae651f476",
+ "x-forwarded-for": "72.12.164.125",
+ "x-forwarded-port": "80",
+ "x-forwarded-proto": "http",
+ "x-imforwards": "20"
+ },
+ "body": "{\"price\": 50000}",
+ "isBase64Encoded": true
+}
\ No newline at end of file
diff --git a/powertools-e2e-tests/src/test/resources/validation/invalid_api_gw_in_event.json b/powertools-e2e-tests/src/test/resources/validation/invalid_api_gw_in_event.json
new file mode 100644
index 000000000..014ef9f05
--- /dev/null
+++ b/powertools-e2e-tests/src/test/resources/validation/invalid_api_gw_in_event.json
@@ -0,0 +1,62 @@
+{
+ "body": "{\"message\": \"Lambda rocks\"}",
+ "resource": "/{proxy+}",
+ "path": "/path/to/resource",
+ "httpMethod": "POST",
+ "isBase64Encoded": false,
+ "queryStringParameters": {
+ "foo": "bar"
+ },
+ "pathParameters": {
+ "proxy": "/path/to/resource"
+ },
+ "stageVariables": {
+ "baz": "qux"
+ },
+ "headers": {
+ "Accept": "text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,*/*;q=0.8",
+ "Accept-Encoding": "gzip, deflate, sdch",
+ "Accept-Language": "en-US,en;q=0.8",
+ "Cache-Control": "max-age=0",
+ "CloudFront-Forwarded-Proto": "https",
+ "CloudFront-Is-Desktop-Viewer": "true",
+ "CloudFront-Is-Mobile-Viewer": "false",
+ "CloudFront-Is-SmartTV-Viewer": "false",
+ "CloudFront-Is-Tablet-Viewer": "false",
+ "CloudFront-Viewer-Country": "US",
+ "Host": "1234567890.execute-api.us-east-1.amazonaws.com",
+ "Upgrade-Insecure-Requests": "1",
+ "User-Agent": "Custom User Agent String",
+ "Via": "1.1 08f323deadbeefa7af34d5feb414ce27.cloudfront.net (CloudFront)",
+ "X-Amz-Cf-Id": "cDehVQoZnx43VYQb9j2-nvCh-9z396Uhbp027Y2JvkCPNLmGJHqlaA==",
+ "X-Forwarded-For": "127.0.0.1, 127.0.0.2",
+ "X-Forwarded-Port": "443",
+ "X-Forwarded-Proto": "https"
+ },
+ "requestContext": {
+ "accountId": "123456789012",
+ "resourceId": "123456",
+ "stage": "prod",
+ "requestId": "c6af9ac6-7b61-11e6-9a41-93e8deadbeef",
+ "requestTime": "09/Apr/2015:12:34:56 +0000",
+ "requestTimeEpoch": 1428582896000,
+ "identity": {
+ "cognitoIdentityPoolId": null,
+ "accountId": null,
+ "cognitoIdentityId": null,
+ "caller": null,
+ "accessKey": null,
+ "sourceIp": "127.0.0.1",
+ "cognitoAuthenticationType": null,
+ "cognitoAuthenticationProvider": null,
+ "userArn": null,
+ "userAgent": "Custom User Agent String",
+ "user": null
+ },
+ "path": "/prod/path/to/resource",
+ "resourcePath": "/{proxy+}",
+ "httpMethod": "POST",
+ "apiId": "1234567890",
+ "protocol": "HTTP/1.1"
+ }
+}
\ No newline at end of file
diff --git a/powertools-e2e-tests/src/test/resources/validation/invalid_api_gw_out_event.json b/powertools-e2e-tests/src/test/resources/validation/invalid_api_gw_out_event.json
new file mode 100644
index 000000000..b7ef1780c
--- /dev/null
+++ b/powertools-e2e-tests/src/test/resources/validation/invalid_api_gw_out_event.json
@@ -0,0 +1,62 @@
+{
+ "body": "{\"price\": 50000}",
+ "resource": "/{proxy+}",
+ "path": "/path/to/resource",
+ "httpMethod": "POST",
+ "isBase64Encoded": false,
+ "queryStringParameters": {
+ "foo": "bar"
+ },
+ "pathParameters": {
+ "proxy": "/path/to/resource"
+ },
+ "stageVariables": {
+ "baz": "qux"
+ },
+ "headers": {
+ "Accept": "text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,*/*;q=0.8",
+ "Accept-Encoding": "gzip, deflate, sdch",
+ "Accept-Language": "en-US,en;q=0.8",
+ "Cache-Control": "max-age=0",
+ "CloudFront-Forwarded-Proto": "https",
+ "CloudFront-Is-Desktop-Viewer": "true",
+ "CloudFront-Is-Mobile-Viewer": "false",
+ "CloudFront-Is-SmartTV-Viewer": "false",
+ "CloudFront-Is-Tablet-Viewer": "false",
+ "CloudFront-Viewer-Country": "US",
+ "Host": "1234567890.execute-api.us-east-1.amazonaws.com",
+ "Upgrade-Insecure-Requests": "1",
+ "User-Agent": "Custom User Agent String",
+ "Via": "1.1 08f323deadbeefa7af34d5feb414ce27.cloudfront.net (CloudFront)",
+ "X-Amz-Cf-Id": "cDehVQoZnx43VYQb9j2-nvCh-9z396Uhbp027Y2JvkCPNLmGJHqlaA==",
+ "X-Forwarded-For": "127.0.0.1, 127.0.0.2",
+ "X-Forwarded-Port": "443",
+ "X-Forwarded-Proto": "https"
+ },
+ "requestContext": {
+ "accountId": "123456789012",
+ "resourceId": "123456",
+ "stage": "prod",
+ "requestId": "c6af9ac6-7b61-11e6-9a41-93e8deadbeef",
+ "requestTime": "09/Apr/2015:12:34:56 +0000",
+ "requestTimeEpoch": 1428582896000,
+ "identity": {
+ "cognitoIdentityPoolId": null,
+ "accountId": null,
+ "cognitoIdentityId": null,
+ "caller": null,
+ "accessKey": null,
+ "sourceIp": "127.0.0.1",
+ "cognitoAuthenticationType": null,
+ "cognitoAuthenticationProvider": null,
+ "userArn": null,
+ "userAgent": "Custom User Agent String",
+ "user": null
+ },
+ "path": "/prod/path/to/resource",
+ "resourcePath": "/{proxy+}",
+ "httpMethod": "POST",
+ "apiId": "1234567890",
+ "protocol": "HTTP/1.1"
+ }
+}
\ No newline at end of file
diff --git a/powertools-e2e-tests/src/test/resources/validation/valid_alb_in_out_event.json b/powertools-e2e-tests/src/test/resources/validation/valid_alb_in_out_event.json
new file mode 100644
index 000000000..35560f109
--- /dev/null
+++ b/powertools-e2e-tests/src/test/resources/validation/valid_alb_in_out_event.json
@@ -0,0 +1,28 @@
+{
+ "requestContext": {
+ "elb": {
+ "targetGroupArn": "arn:aws:elasticloadbalancing:us-east-2:123456789012:targetgroup/lambda-279XGJDqGZ5rsrHC2Fjr/49e9d65c45c6791a"
+ }
+ },
+ "httpMethod": "POST",
+ "path": "/path/to/resource",
+ "queryStringParameters": {
+ "query": "1234ABCD"
+ },
+ "headers": {
+ "accept": "text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,image/apng,*/*;q=0.8",
+ "accept-encoding": "gzip",
+ "accept-language": "en-US,en;q=0.9",
+ "connection": "keep-alive",
+ "host": "lambda-alb-123578498.us-east-2.elb.amazonaws.com",
+ "upgrade-insecure-requests": "1",
+ "user-agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/71.0.3578.98 Safari/537.36",
+ "x-amzn-trace-id": "Root=1-5c536348-3d683b8b04734faae651f476",
+ "x-forwarded-for": "72.12.164.125",
+ "x-forwarded-port": "80",
+ "x-forwarded-proto": "http",
+ "x-imforwards": "20"
+ },
+ "body": "{\"price\": 150}",
+ "isBase64Encoded": true
+}
\ No newline at end of file
diff --git a/powertools-e2e-tests/src/test/resources/validation/valid_api_gw_in_out_event.json b/powertools-e2e-tests/src/test/resources/validation/valid_api_gw_in_out_event.json
new file mode 100644
index 000000000..8cb8ea27a
--- /dev/null
+++ b/powertools-e2e-tests/src/test/resources/validation/valid_api_gw_in_out_event.json
@@ -0,0 +1,62 @@
+{
+ "body": "{\"price\": 150}",
+ "resource": "/{proxy+}",
+ "path": "/path/to/resource",
+ "httpMethod": "POST",
+ "isBase64Encoded": false,
+ "queryStringParameters": {
+ "foo": "bar"
+ },
+ "pathParameters": {
+ "proxy": "/path/to/resource"
+ },
+ "stageVariables": {
+ "baz": "qux"
+ },
+ "headers": {
+ "Accept": "text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,*/*;q=0.8",
+ "Accept-Encoding": "gzip, deflate, sdch",
+ "Accept-Language": "en-US,en;q=0.8",
+ "Cache-Control": "max-age=0",
+ "CloudFront-Forwarded-Proto": "https",
+ "CloudFront-Is-Desktop-Viewer": "true",
+ "CloudFront-Is-Mobile-Viewer": "false",
+ "CloudFront-Is-SmartTV-Viewer": "false",
+ "CloudFront-Is-Tablet-Viewer": "false",
+ "CloudFront-Viewer-Country": "US",
+ "Host": "1234567890.execute-api.us-east-1.amazonaws.com",
+ "Upgrade-Insecure-Requests": "1",
+ "User-Agent": "Custom User Agent String",
+ "Via": "1.1 08f323deadbeefa7af34d5feb414ce27.cloudfront.net (CloudFront)",
+ "X-Amz-Cf-Id": "cDehVQoZnx43VYQb9j2-nvCh-9z396Uhbp027Y2JvkCPNLmGJHqlaA==",
+ "X-Forwarded-For": "127.0.0.1, 127.0.0.2",
+ "X-Forwarded-Port": "443",
+ "X-Forwarded-Proto": "https"
+ },
+ "requestContext": {
+ "accountId": "123456789012",
+ "resourceId": "123456",
+ "stage": "prod",
+ "requestId": "c6af9ac6-7b61-11e6-9a41-93e8deadbeef",
+ "requestTime": "09/Apr/2015:12:34:56 +0000",
+ "requestTimeEpoch": 1428582896000,
+ "identity": {
+ "cognitoIdentityPoolId": null,
+ "accountId": null,
+ "cognitoIdentityId": null,
+ "caller": null,
+ "accessKey": null,
+ "sourceIp": "127.0.0.1",
+ "cognitoAuthenticationType": null,
+ "cognitoAuthenticationProvider": null,
+ "userArn": null,
+ "userAgent": "Custom User Agent String",
+ "user": null
+ },
+ "path": "/prod/path/to/resource",
+ "resourcePath": "/{proxy+}",
+ "httpMethod": "POST",
+ "apiId": "1234567890",
+ "protocol": "HTTP/1.1"
+ }
+}
\ No newline at end of file
diff --git a/powertools-validation/src/main/java/software/amazon/lambda/powertools/validation/internal/ValidationAspect.java b/powertools-validation/src/main/java/software/amazon/lambda/powertools/validation/internal/ValidationAspect.java
index 0d71104f3..978be16de 100644
--- a/powertools-validation/src/main/java/software/amazon/lambda/powertools/validation/internal/ValidationAspect.java
+++ b/powertools-validation/src/main/java/software/amazon/lambda/powertools/validation/internal/ValidationAspect.java
@@ -22,6 +22,10 @@
import static software.amazon.lambda.powertools.validation.ValidationUtils.getJsonSchema;
import static software.amazon.lambda.powertools.validation.ValidationUtils.validate;
+import java.util.Collections;
+import java.util.List;
+import java.util.Map;
+
import com.amazonaws.services.lambda.runtime.events.APIGatewayProxyRequestEvent;
import com.amazonaws.services.lambda.runtime.events.APIGatewayProxyResponseEvent;
import com.amazonaws.services.lambda.runtime.events.APIGatewayV2HTTPEvent;
@@ -51,6 +55,7 @@
import org.slf4j.LoggerFactory;
import software.amazon.lambda.powertools.validation.Validation;
import software.amazon.lambda.powertools.validation.ValidationConfig;
+import software.amazon.lambda.powertools.validation.ValidationException;
/**
* Aspect for {@link Validation} annotation
@@ -73,6 +78,11 @@ public Object around(ProceedingJoinPoint pjp,
if (validation.schemaVersion() != V201909) {
ValidationConfig.get().setSchemaVersion(validation.schemaVersion());
}
+
+ // we need this result object to be null at this point as validation of API events, if
+ // it fails, will catch the ValidationException and generate a 400 API response. This response
+ // will be stored in the result object to prevent executing the lambda
+ Object result = null;
if (placedOnRequestHandler(pjp)) {
validationNeeded = true;
@@ -85,10 +95,10 @@ public Object around(ProceedingJoinPoint pjp,
validate(obj, inboundJsonSchema, validation.envelope());
} else if (obj instanceof APIGatewayProxyRequestEvent) {
APIGatewayProxyRequestEvent event = (APIGatewayProxyRequestEvent) obj;
- validate(event.getBody(), inboundJsonSchema);
+ result = validateAPIGatewayProxyBody(event.getBody(), inboundJsonSchema, null, null);
} else if (obj instanceof APIGatewayV2HTTPEvent) {
APIGatewayV2HTTPEvent event = (APIGatewayV2HTTPEvent) obj;
- validate(event.getBody(), inboundJsonSchema);
+ result = validateAPIGatewayV2HTTPBody(event.getBody(), inboundJsonSchema, null, null);
} else if (obj instanceof SNSEvent) {
SNSEvent event = (SNSEvent) obj;
event.getRecords().forEach(record -> validate(record.getSNS().getMessage(), inboundJsonSchema));
@@ -140,33 +150,100 @@ record -> validate(decode(record.getData()), inboundJsonSchema)));
}
}
- Object result = pjp.proceed(proceedArgs);
-
- if (validationNeeded && !validation.outboundSchema().isEmpty()) {
- JsonSchema outboundJsonSchema = getJsonSchema(validation.outboundSchema(), true);
-
- if (result instanceof APIGatewayProxyResponseEvent) {
- APIGatewayProxyResponseEvent response = (APIGatewayProxyResponseEvent) result;
- validate(response.getBody(), outboundJsonSchema);
- } else if (result instanceof APIGatewayV2HTTPResponse) {
- APIGatewayV2HTTPResponse response = (APIGatewayV2HTTPResponse) result;
- validate(response.getBody(), outboundJsonSchema);
- } else if (result instanceof APIGatewayV2WebSocketResponse) {
- APIGatewayV2WebSocketResponse response = (APIGatewayV2WebSocketResponse) result;
- validate(response.getBody(), outboundJsonSchema);
- } else if (result instanceof ApplicationLoadBalancerResponseEvent) {
- ApplicationLoadBalancerResponseEvent response = (ApplicationLoadBalancerResponseEvent) result;
- validate(response.getBody(), outboundJsonSchema);
- } else if (result instanceof KinesisAnalyticsInputPreprocessingResponse) {
- KinesisAnalyticsInputPreprocessingResponse response =
- (KinesisAnalyticsInputPreprocessingResponse) result;
- response.getRecords().forEach(record -> validate(decode(record.getData()), outboundJsonSchema));
- } else {
- LOG.warn("Unhandled response type {}, please use the 'envelope' parameter to specify what to validate",
- result.getClass().getName());
- }
+ // don't execute the lambda if result was set by previous validation step
+ // in that case result should already hold a response with validation information
+ if (result != null) {
+ LOG.error("Incoming API event's body failed inbound schema validation.");
}
+ else {
+ result = pjp.proceed(proceedArgs);
+
+ if (validationNeeded && !validation.outboundSchema().isEmpty()) {
+ JsonSchema outboundJsonSchema = getJsonSchema(validation.outboundSchema(), true);
+
+ Object overridenResponse = null;
+ // The normal behavior of @Validation is to throw an exception if response's validation fails.
+ // but in the case of APIGatewayProxyResponseEvent and APIGatewayV2HTTPResponse we want to return
+ // a 400 response with the validation errors instead of throwing an exception.
+ if (result instanceof APIGatewayProxyResponseEvent) {
+ APIGatewayProxyResponseEvent response = (APIGatewayProxyResponseEvent) result;
+ overridenResponse = validateAPIGatewayProxyBody(response.getBody(), outboundJsonSchema, response.getHeaders(),
+ response.getMultiValueHeaders());
+ } else if (result instanceof APIGatewayV2HTTPResponse) {
+ APIGatewayV2HTTPResponse response = (APIGatewayV2HTTPResponse) result;
+ overridenResponse = validateAPIGatewayV2HTTPBody(response.getBody(), outboundJsonSchema, response.getHeaders(),
+ response.getMultiValueHeaders());
+ // all type of below responses will throw an exception if validation fails
+ } else if (result instanceof APIGatewayV2WebSocketResponse) {
+ APIGatewayV2WebSocketResponse response = (APIGatewayV2WebSocketResponse) result;
+ validate(response.getBody(), outboundJsonSchema);
+ } else if (result instanceof ApplicationLoadBalancerResponseEvent) {
+ ApplicationLoadBalancerResponseEvent response = (ApplicationLoadBalancerResponseEvent) result;
+ validate(response.getBody(), outboundJsonSchema);
+ } else if (result instanceof KinesisAnalyticsInputPreprocessingResponse) {
+ KinesisAnalyticsInputPreprocessingResponse response =
+ (KinesisAnalyticsInputPreprocessingResponse) result;
+ response.getRecords().forEach(record -> validate(decode(record.getData()), outboundJsonSchema));
+ } else {
+ LOG.warn("Unhandled response type {}, please use the 'envelope' parameter to specify what to validate",
+ result.getClass().getName());
+ }
+
+ if (overridenResponse != null) {
+ result = overridenResponse;
+ LOG.error("API response failed outbound schema validation.");
+ }
+ }
+ }
return result;
}
+
+ /**
+ * Validates the given body against the provided JsonSchema. If validation fails the ValidationException
+ * will be catched and transformed to a 400, bad request, API response
+ * @param body body of the event to validate
+ * @param inboundJsonSchema validation schema
+ * @return null if validation passed, or a 400 response object otherwise
+ */
+ private APIGatewayProxyResponseEvent validateAPIGatewayProxyBody(final String body, final JsonSchema jsonSchema,
+ final Map headers, Map> multivalueHeaders) {
+ APIGatewayProxyResponseEvent result = null;
+ try {
+ validate(body, jsonSchema);
+ } catch (ValidationException e) {
+ LOG.error("There were validation errors: {}", e.getMessage());
+ result = new APIGatewayProxyResponseEvent();
+ result.setBody(e.getMessage());
+ result.setHeaders(headers == null ? Collections.emptyMap() : headers);
+ result.setMultiValueHeaders(multivalueHeaders == null ? Collections.emptyMap() : multivalueHeaders);
+ result.setStatusCode(400);
+ result.setIsBase64Encoded(false);
+ }
+ return result;
+ }
+
+ /**
+ * Validates the given body against the provided JsonSchema. If validation fails the ValidationException
+ * will be catched and transformed to a 400, bad request, API response
+ * @param body body of the event to validate
+ * @param inboundJsonSchema validation schema
+ * @return null if validation passed, or a 400 response object otherwise
+ */
+ private APIGatewayV2HTTPResponse validateAPIGatewayV2HTTPBody(final String body, final JsonSchema jsonSchema,
+ final Map headers, Map> multivalueHeaders) {
+ APIGatewayV2HTTPResponse result = null;
+ try {
+ validate(body, jsonSchema);
+ } catch (ValidationException e) {
+ LOG.error("There were validation errors: {}", e.getMessage());
+ result = new APIGatewayV2HTTPResponse();
+ result.setBody(e.getMessage());
+ result.setHeaders(headers == null ? Collections.emptyMap() : headers);
+ result.setMultiValueHeaders(multivalueHeaders == null ? Collections.emptyMap() : multivalueHeaders);
+ result.setStatusCode(400);
+ result.setIsBase64Encoded(false);
+ }
+ return result;
+ }
}
diff --git a/powertools-validation/src/test/java/software/amazon/lambda/powertools/validation/handlers/GenericSchemaV7APIGatewayProxyRequestEventHandler.java b/powertools-validation/src/test/java/software/amazon/lambda/powertools/validation/handlers/GenericSchemaV7APIGatewayProxyRequestEventHandler.java
new file mode 100644
index 000000000..74e8605a5
--- /dev/null
+++ b/powertools-validation/src/test/java/software/amazon/lambda/powertools/validation/handlers/GenericSchemaV7APIGatewayProxyRequestEventHandler.java
@@ -0,0 +1,34 @@
+/*
+ * Copyright 2023 Amazon.com, Inc. or its affiliates.
+ * Licensed under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ * http://www.apache.org/licenses/LICENSE-2.0
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ *
+ */
+
+package software.amazon.lambda.powertools.validation.handlers;
+
+import com.amazonaws.services.lambda.runtime.Context;
+import com.amazonaws.services.lambda.runtime.RequestHandler;
+import com.amazonaws.services.lambda.runtime.events.APIGatewayProxyRequestEvent;
+import com.amazonaws.services.lambda.runtime.events.APIGatewayProxyResponseEvent;
+
+import software.amazon.lambda.powertools.validation.Validation;
+
+public class GenericSchemaV7APIGatewayProxyRequestEventHandler implements RequestHandler {
+
+ @Validation(inboundSchema = "classpath:/schema_v7.json")
+ @Override
+ public APIGatewayProxyResponseEvent handleRequest(APIGatewayProxyRequestEvent input, Context context) {
+ APIGatewayProxyResponseEvent response = new APIGatewayProxyResponseEvent();
+ response.setBody("valid-test");
+ response.setStatusCode(200);
+ return response;
+ }
+}
\ No newline at end of file
diff --git a/powertools-validation/src/test/java/software/amazon/lambda/powertools/validation/handlers/GenericSchemaV7Handler.java b/powertools-validation/src/test/java/software/amazon/lambda/powertools/validation/handlers/GenericSchemaV7StringHandler.java
similarity index 92%
rename from powertools-validation/src/test/java/software/amazon/lambda/powertools/validation/handlers/GenericSchemaV7Handler.java
rename to powertools-validation/src/test/java/software/amazon/lambda/powertools/validation/handlers/GenericSchemaV7StringHandler.java
index 5b8343d1b..ab0645f29 100644
--- a/powertools-validation/src/test/java/software/amazon/lambda/powertools/validation/handlers/GenericSchemaV7Handler.java
+++ b/powertools-validation/src/test/java/software/amazon/lambda/powertools/validation/handlers/GenericSchemaV7StringHandler.java
@@ -16,13 +16,14 @@
import com.amazonaws.services.lambda.runtime.Context;
import com.amazonaws.services.lambda.runtime.RequestHandler;
+
import software.amazon.lambda.powertools.validation.Validation;
-public class GenericSchemaV7Handler implements RequestHandler {
+public class GenericSchemaV7StringHandler implements RequestHandler {
@Validation(inboundSchema = "classpath:/schema_v7.json")
@Override
public String handleRequest(T input, Context context) {
return "OK";
}
-}
+}
\ No newline at end of file
diff --git a/powertools-validation/src/test/java/software/amazon/lambda/powertools/validation/handlers/ValidationInboundStringHandler.java b/powertools-validation/src/test/java/software/amazon/lambda/powertools/validation/handlers/ValidationInboundAPIGatewayV2HTTPEventHandler.java
similarity index 87%
rename from powertools-validation/src/test/java/software/amazon/lambda/powertools/validation/handlers/ValidationInboundStringHandler.java
rename to powertools-validation/src/test/java/software/amazon/lambda/powertools/validation/handlers/ValidationInboundAPIGatewayV2HTTPEventHandler.java
index fd5692884..b8c67b1eb 100644
--- a/powertools-validation/src/test/java/software/amazon/lambda/powertools/validation/handlers/ValidationInboundStringHandler.java
+++ b/powertools-validation/src/test/java/software/amazon/lambda/powertools/validation/handlers/ValidationInboundAPIGatewayV2HTTPEventHandler.java
@@ -17,10 +17,12 @@
import com.amazonaws.services.lambda.runtime.Context;
import com.amazonaws.services.lambda.runtime.RequestHandler;
import com.amazonaws.services.lambda.runtime.events.APIGatewayV2HTTPEvent;
+import com.amazonaws.services.lambda.runtime.events.APIGatewayV2HTTPResponse;
+
import software.amazon.lambda.powertools.validation.Validation;
-public class ValidationInboundStringHandler implements RequestHandler {
+public class ValidationInboundAPIGatewayV2HTTPEventHandler implements RequestHandler {
private static final String schema = "{\n" +
" \"$schema\": \"http://json-schema.org/draft-07/schema\",\n" +
@@ -80,7 +82,10 @@ public class ValidationInboundStringHandler implements RequestHandler provideArguments(ExtensionContext context) {
+
+ String body = "{id";
+
+ Map headers = new HashMap<>();
+ headers.put("header1", "value1,value2,value3");
+ Map> headersList = new HashMap<>();
+ List headerValues = new ArrayList<>();
+ headerValues.add("value1");
+ headerValues.add("value2");
+ headerValues.add("value3");
+ headersList.put("header1", headerValues);
+
+ final APIGatewayProxyResponseEvent apiGWProxyResponseEvent = new APIGatewayProxyResponseEvent()
+ .withBody(body)
+ .withHeaders(headers)
+ .withMultiValueHeaders(headersList);
+
+ APIGatewayV2HTTPResponse apiGWV2HTTPResponse = new APIGatewayV2HTTPResponse();
+ apiGWV2HTTPResponse.setBody(body);
+ apiGWV2HTTPResponse.setHeaders(headers);
+ apiGWV2HTTPResponse.setMultiValueHeaders(headersList);
+
+ return Stream.of(apiGWProxyResponseEvent, apiGWV2HTTPResponse).map(Arguments::of);
+ }
+}
diff --git a/powertools-validation/src/test/java/software/amazon/lambda/powertools/validation/internal/ResponseEventsArgumentsProvider.java b/powertools-validation/src/test/java/software/amazon/lambda/powertools/validation/internal/ResponseEventsArgumentsProvider.java
index b634d6f8c..74803a05a 100644
--- a/powertools-validation/src/test/java/software/amazon/lambda/powertools/validation/internal/ResponseEventsArgumentsProvider.java
+++ b/powertools-validation/src/test/java/software/amazon/lambda/powertools/validation/internal/ResponseEventsArgumentsProvider.java
@@ -35,11 +35,6 @@ public Stream extends Arguments> provideArguments(ExtensionContext context) {
String body = "{id";
- final APIGatewayProxyResponseEvent apiGWProxyResponseEvent = new APIGatewayProxyResponseEvent().withBody(body);
-
- APIGatewayV2HTTPResponse apiGWV2HTTPResponse = new APIGatewayV2HTTPResponse();
- apiGWV2HTTPResponse.setBody(body);
-
APIGatewayV2WebSocketResponse apiGWV2WebSocketResponse = new APIGatewayV2WebSocketResponse();
apiGWV2WebSocketResponse.setBody(body);
@@ -53,7 +48,7 @@ public Stream extends Arguments> provideArguments(ExtensionContext context) {
KinesisAnalyticsInputPreprocessingResponse.Result.Ok, buffer));
kaipResponse.setRecords(records);
- return Stream.of(apiGWProxyResponseEvent, apiGWV2HTTPResponse, apiGWV2WebSocketResponse, albResponseEvent,
+ return Stream.of(apiGWV2WebSocketResponse, albResponseEvent,
kaipResponse).map(Arguments::of);
}
}
diff --git a/powertools-validation/src/test/java/software/amazon/lambda/powertools/validation/internal/ValidationAspectTest.java b/powertools-validation/src/test/java/software/amazon/lambda/powertools/validation/internal/ValidationAspectTest.java
index c8d5e7ade..1708ebeeb 100644
--- a/powertools-validation/src/test/java/software/amazon/lambda/powertools/validation/internal/ValidationAspectTest.java
+++ b/powertools-validation/src/test/java/software/amazon/lambda/powertools/validation/internal/ValidationAspectTest.java
@@ -17,11 +17,31 @@
import static org.assertj.core.api.Assertions.assertThat;
import static org.assertj.core.api.Assertions.assertThatExceptionOfType;
import static org.assertj.core.api.Assertions.assertThatNoException;
+import static org.junit.jupiter.api.Assertions.fail;
import static org.mockito.Mockito.when;
+import java.io.IOException;
+import java.util.ArrayList;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+import java.util.stream.Stream;
+
+import org.aspectj.lang.ProceedingJoinPoint;
+import org.aspectj.lang.Signature;
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.Test;
+import org.junit.jupiter.params.ParameterizedTest;
+import org.junit.jupiter.params.provider.Arguments;
+import org.junit.jupiter.params.provider.ArgumentsSource;
+import org.junit.jupiter.params.provider.MethodSource;
+import org.mockito.Mock;
+import org.mockito.MockitoAnnotations;
+
import com.amazonaws.services.lambda.runtime.Context;
import com.amazonaws.services.lambda.runtime.RequestHandler;
import com.amazonaws.services.lambda.runtime.events.APIGatewayProxyRequestEvent;
+import com.amazonaws.services.lambda.runtime.events.APIGatewayProxyResponseEvent;
import com.amazonaws.services.lambda.runtime.events.APIGatewayV2HTTPEvent;
import com.amazonaws.services.lambda.runtime.events.APIGatewayV2HTTPResponse;
import com.amazonaws.services.lambda.runtime.events.ActiveMQEvent;
@@ -40,25 +60,15 @@
import com.amazonaws.services.lambda.runtime.serialization.PojoSerializer;
import com.amazonaws.services.lambda.runtime.serialization.events.LambdaEventSerializers;
import com.networknt.schema.SpecVersion;
-import java.io.IOException;
-import java.util.stream.Stream;
-import org.aspectj.lang.ProceedingJoinPoint;
-import org.aspectj.lang.Signature;
-import org.junit.jupiter.api.BeforeEach;
-import org.junit.jupiter.api.Test;
-import org.junit.jupiter.params.ParameterizedTest;
-import org.junit.jupiter.params.provider.Arguments;
-import org.junit.jupiter.params.provider.ArgumentsSource;
-import org.junit.jupiter.params.provider.MethodSource;
-import org.mockito.Mock;
-import org.mockito.MockitoAnnotations;
+
import software.amazon.lambda.powertools.validation.Validation;
import software.amazon.lambda.powertools.validation.ValidationConfig;
import software.amazon.lambda.powertools.validation.ValidationException;
-import software.amazon.lambda.powertools.validation.handlers.GenericSchemaV7Handler;
+import software.amazon.lambda.powertools.validation.handlers.GenericSchemaV7APIGatewayProxyRequestEventHandler;
+import software.amazon.lambda.powertools.validation.handlers.GenericSchemaV7StringHandler;
import software.amazon.lambda.powertools.validation.handlers.SQSWithCustomEnvelopeHandler;
import software.amazon.lambda.powertools.validation.handlers.SQSWithWrongEnvelopeHandler;
-import software.amazon.lambda.powertools.validation.handlers.ValidationInboundStringHandler;
+import software.amazon.lambda.powertools.validation.handlers.ValidationInboundAPIGatewayV2HTTPEventHandler;
import software.amazon.lambda.powertools.validation.model.MyCustomEvent;
@@ -99,7 +109,7 @@ void setUp() {
@ParameterizedTest
@ArgumentsSource(ResponseEventsArgumentsProvider.class)
- public void testValidateOutboundJsonSchema(Object object) throws Throwable {
+ void testValidateOutboundJsonSchemaWithExceptions(Object object) throws Throwable {
when(validation.schemaVersion()).thenReturn(SpecVersion.VersionFlag.V7);
when(pjp.getSignature()).thenReturn(signature);
when(pjp.getSignature().getDeclaringType()).thenReturn(RequestHandler.class);
@@ -114,6 +124,47 @@ public void testValidateOutboundJsonSchema(Object object) throws Throwable {
validationAspect.around(pjp, validation);
});
}
+
+ @ParameterizedTest
+ @ArgumentsSource(HandledResponseEventsArgumentsProvider.class)
+ void testValidateOutboundJsonSchemaWithHandledExceptions(Object object) throws Throwable {
+ when(validation.schemaVersion()).thenReturn(SpecVersion.VersionFlag.V7);
+ when(pjp.getSignature()).thenReturn(signature);
+ when(pjp.getSignature().getDeclaringType()).thenReturn(RequestHandler.class);
+ Object[] args = {new Object(), context};
+ when(pjp.getArgs()).thenReturn(args);
+ when(pjp.proceed(args)).thenReturn(object);
+ when(validation.inboundSchema()).thenReturn("");
+ when(validation.outboundSchema()).thenReturn("classpath:/schema_v7.json");
+
+ Object response = validationAspect.around(pjp, validation);
+ assertThat(response).isInstanceOfAny(APIGatewayProxyResponseEvent.class, APIGatewayV2HTTPResponse.class);
+
+ List headerValues = new ArrayList<>();
+ headerValues.add("value1");
+ headerValues.add("value2");
+ headerValues.add("value3");
+
+ if (response instanceof APIGatewayProxyResponseEvent) {
+ assertThat(response).isInstanceOfSatisfying(APIGatewayProxyResponseEvent.class, t -> {
+ assertThat(t.getStatusCode()).isEqualTo(400);
+ assertThat(t.getBody()).isNotBlank();
+ assertThat(t.getIsBase64Encoded()).isFalse();
+ assertThat(t.getHeaders()).containsEntry("header1", "value1,value2,value3");
+ assertThat(t.getMultiValueHeaders()).containsEntry("header1", headerValues);
+ });
+ } else if (response instanceof APIGatewayV2HTTPResponse) {
+ assertThat(response).isInstanceOfSatisfying(APIGatewayV2HTTPResponse.class, t -> {
+ assertThat(t.getStatusCode()).isEqualTo(400);
+ assertThat(t.getBody()).isNotBlank();
+ assertThat(t.getIsBase64Encoded()).isFalse();
+ assertThat(t.getHeaders()).containsEntry("header1", "value1,value2,value3");
+ assertThat(t.getMultiValueHeaders()).containsEntry("header1", headerValues);
+ });
+ } else {
+ fail();
+ }
+ }
@Test
public void testValidateOutboundJsonSchema_APIGWV2() throws Throwable {
@@ -137,50 +188,84 @@ public void testValidateOutboundJsonSchema_APIGWV2() throws Throwable {
@Test
public void validate_inputOK_schemaInClasspath_shouldValidate() {
- GenericSchemaV7Handler handler = new GenericSchemaV7Handler();
+ GenericSchemaV7APIGatewayProxyRequestEventHandler handler = new GenericSchemaV7APIGatewayProxyRequestEventHandler();
APIGatewayProxyRequestEvent event = new APIGatewayProxyRequestEvent();
event.setBody("{" +
" \"id\": 1," +
" \"name\": \"Lampshade\"," +
" \"price\": 42" +
"}");
- assertThat(handler.handleRequest(event, context)).isEqualTo("OK");
+
+
+ APIGatewayProxyResponseEvent response = handler.handleRequest(event, context);
+ assertThat(response.getBody()).isEqualTo("valid-test");
+ assertThat(response.getStatusCode()).isEqualTo(200);
+
}
@Test
public void validate_inputKO_schemaInClasspath_shouldThrowValidationException() {
- GenericSchemaV7Handler handler = new GenericSchemaV7Handler();
+ GenericSchemaV7APIGatewayProxyRequestEventHandler handler = new GenericSchemaV7APIGatewayProxyRequestEventHandler();
+
+ Map headers = new HashMap<>();
+ headers.put("header1", "value1");
+ Map> headersList = new HashMap<>();
+ List headerValues = new ArrayList<>();
+ headerValues.add("value1");
+ headersList.put("header1", headerValues);
+
APIGatewayProxyRequestEvent event = new APIGatewayProxyRequestEvent();
event.setBody("{" +
- " \"id\": 1," +
- " \"name\": \"Lampshade\"," +
- " \"price\": -2" +
- "}");
+ " \"id\": 1," +
+ " \"name\": \"Lampshade\"," +
+ " \"price\": -2" +
+ "}");
+ event.setHeaders(headers);
+ event.setMultiValueHeaders(headersList);
+
// price is negative
- assertThatExceptionOfType(ValidationException.class).isThrownBy(() -> handler.handleRequest(event, context));
+ APIGatewayProxyResponseEvent response = handler.handleRequest(event, context);
+ assertThat(response.getBody()).isNotBlank();
+ assertThat(response.getStatusCode()).isEqualTo(400);
+ assertThat(response.getHeaders()).isEmpty();
+ assertThat(response.getMultiValueHeaders()).isEmpty();
}
@Test
public void validate_inputOK_schemaInString_shouldValidate() {
- ValidationInboundStringHandler handler = new ValidationInboundStringHandler();
+ ValidationInboundAPIGatewayV2HTTPEventHandler handler = new ValidationInboundAPIGatewayV2HTTPEventHandler();
APIGatewayV2HTTPEvent event = new APIGatewayV2HTTPEvent();
event.setBody("{" +
- " \"id\": 1," +
- " \"name\": \"Lampshade\"," +
- " \"price\": 42" +
- "}");
- assertThat(handler.handleRequest(event, context)).isEqualTo("OK");
+ " \"id\": 1," +
+ " \"name\": \"Lampshade\"," +
+ " \"price\": 42" +
+ "}");
+
+ APIGatewayV2HTTPResponse response = handler.handleRequest(event, context);
+ assertThat(response.getBody()).isEqualTo("valid-test");
+ assertThat(response.getStatusCode()).isEqualTo(200);
}
+
@Test
public void validate_inputKO_schemaInString_shouldThrowValidationException() {
- ValidationInboundStringHandler handler = new ValidationInboundStringHandler();
+ ValidationInboundAPIGatewayV2HTTPEventHandler handler = new ValidationInboundAPIGatewayV2HTTPEventHandler();
+
+ Map headers = new HashMap<>();
+ headers.put("header1", "value1");
+
APIGatewayV2HTTPEvent event = new APIGatewayV2HTTPEvent();
event.setBody("{" +
- " \"id\": 1," +
- " \"name\": \"Lampshade\"" +
- "}");
- assertThatExceptionOfType(ValidationException.class).isThrownBy(() -> handler.handleRequest(event, context));
+ " \"id\": 1," +
+ " \"name\": \"Lampshade\"" +
+ "}");
+ event.setHeaders(headers);
+
+ APIGatewayV2HTTPResponse response = handler.handleRequest(event, context);
+ assertThat(response.getBody()).isNotBlank();
+ assertThat(response.getStatusCode()).isEqualTo(400);
+ assertThat(response.getHeaders()).isEmpty();
+ assertThat(response.getMultiValueHeaders()).isEmpty();
}
@Test
@@ -189,7 +274,7 @@ public void validate_SQS() {
LambdaEventSerializers.serializerFor(SQSEvent.class, ClassLoader.getSystemClassLoader());
SQSEvent event = pojoSerializer.fromJson(this.getClass().getResourceAsStream("/sqs.json"));
- GenericSchemaV7Handler handler = new GenericSchemaV7Handler();
+ GenericSchemaV7StringHandler