diff --git a/README.md b/README.md
index a78e1773e..f02978071 100644
--- a/README.md
+++ b/README.md
@@ -402,6 +402,13 @@ Vert.x Mutiny webClient exploratory test.
Also see http/vertx-web-client/README.md
+### `http/vertx-web-validation`
+Ensure that you can deploy a simple Quarkus application with predefined routes using schema parser from vertx-json-schema and Router Vert.x approach,
+ incorporating web validation configuration through ValidationHandlerBuilder. One of the goal of vertx-web-validation functionality is to validate parameters and bodies of incoming requests.
+
+It also verifies multiple deployment strategies like:
+- Using Quarkus OpenShift extension
+
### `http/graphql`
This module covers some basic scenarios around GraphQL.
diff --git a/http/vertx-web-validation/pom.xml b/http/vertx-web-validation/pom.xml
new file mode 100644
index 000000000..a3859d72f
--- /dev/null
+++ b/http/vertx-web-validation/pom.xml
@@ -0,0 +1,27 @@
+
+
+ 4.0.0
+
+ io.quarkus.ts.qe
+ parent
+ 1.0.0-SNAPSHOT
+ ../..
+
+ vertx-web-validation
+ jar
+ Quarkus QE TS: HTTP: Vert.x-Web-Validation
+
+
+ io.quarkus
+ quarkus-resteasy-reactive-jackson
+
+
+ io.vertx
+ vertx-web-validation
+
+
+ io.vertx
+ vertx-json-schema
+
+
+
diff --git a/http/vertx-web-validation/src/main/java/io/quarkus/ts/vertx/web/validation/ShopResource.java b/http/vertx-web-validation/src/main/java/io/quarkus/ts/vertx/web/validation/ShopResource.java
new file mode 100644
index 000000000..73ee3bca3
--- /dev/null
+++ b/http/vertx-web-validation/src/main/java/io/quarkus/ts/vertx/web/validation/ShopResource.java
@@ -0,0 +1,33 @@
+package io.quarkus.ts.vertx.web.validation;
+
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.List;
+import java.util.UUID;
+
+import jakarta.ws.rs.GET;
+import jakarta.ws.rs.Path;
+import jakarta.ws.rs.Produces;
+import jakarta.ws.rs.core.MediaType;
+
+@Path("/shoppinglist")
+@Produces(MediaType.APPLICATION_JSON)
+public class ShopResource {
+
+ private static List shoppingList = createSampleProductList();
+
+ private static List createSampleProductList() {
+ shoppingList = new ArrayList<>();
+ shoppingList.add(new ShoppingList(UUID.randomUUID(), "ListName1", 25,
+ new ArrayList<>(Arrays.asList("Carrots", "Water", "Cheese", "Beer"))));
+ shoppingList.add(new ShoppingList(UUID.randomUUID(), "ListName2", 80,
+ new ArrayList<>(Arrays.asList("Meat", "Wine", "Almonds", "Potatoes", "Cake"))));
+ return shoppingList;
+ }
+
+ @GET
+ public List get() {
+ return shoppingList;
+ }
+
+}
diff --git a/http/vertx-web-validation/src/main/java/io/quarkus/ts/vertx/web/validation/ShoppingList.java b/http/vertx-web-validation/src/main/java/io/quarkus/ts/vertx/web/validation/ShoppingList.java
new file mode 100644
index 000000000..45ca3d5d1
--- /dev/null
+++ b/http/vertx-web-validation/src/main/java/io/quarkus/ts/vertx/web/validation/ShoppingList.java
@@ -0,0 +1,60 @@
+package io.quarkus.ts.vertx.web.validation;
+
+import java.util.ArrayList;
+import java.util.UUID;
+
+public class ShoppingList {
+
+ public UUID id;
+
+ public String name;
+
+ public ArrayList products;
+
+ public double price;
+
+ public ShoppingList(UUID id, String name, double price, ArrayList products) {
+ this.id = id;
+ this.name = name;
+ this.price = price;
+ this.products = products;
+ }
+
+ public ArrayList getProducts() {
+ return products;
+ }
+
+ public void setProducts(ArrayList products) {
+ this.products = products;
+ }
+
+ public UUID getId() {
+ return id;
+ }
+
+ public String getName() {
+ return name;
+ }
+
+ public void setName(String name) {
+ this.name = name;
+ }
+
+ public double getPrice() {
+ return price;
+ }
+
+ public void setPrice(double price) {
+ this.price = price;
+ }
+
+ @Override
+ public String toString() {
+ return String.format(
+ "Shopping list{id=%s, name=%s, products=%s, price=%s}",
+ getId(),
+ getName(),
+ getProducts().toString(),
+ getPrice());
+ }
+}
diff --git a/http/vertx-web-validation/src/main/java/io/quarkus/ts/vertx/web/validation/ValidationHandlerOnRoutes.java b/http/vertx-web-validation/src/main/java/io/quarkus/ts/vertx/web/validation/ValidationHandlerOnRoutes.java
new file mode 100644
index 000000000..784badab2
--- /dev/null
+++ b/http/vertx-web-validation/src/main/java/io/quarkus/ts/vertx/web/validation/ValidationHandlerOnRoutes.java
@@ -0,0 +1,153 @@
+package io.quarkus.ts.vertx.web.validation;
+
+import static io.vertx.ext.web.validation.builder.Parameters.param;
+import static io.vertx.json.schema.common.dsl.Schemas.arraySchema;
+import static io.vertx.json.schema.common.dsl.Schemas.numberSchema;
+import static io.vertx.json.schema.common.dsl.Schemas.objectSchema;
+import static io.vertx.json.schema.common.dsl.Schemas.stringSchema;
+import static io.vertx.json.schema.draft7.dsl.Keywords.maximum;
+
+import java.util.List;
+import java.util.concurrent.atomic.AtomicReference;
+import java.util.stream.Collectors;
+
+import jakarta.annotation.PostConstruct;
+import jakarta.enterprise.event.Observes;
+import jakarta.inject.Inject;
+import jakarta.ws.rs.core.HttpHeaders;
+import jakarta.ws.rs.core.MediaType;
+
+import io.netty.handler.codec.http.HttpResponseStatus;
+import io.vertx.core.Vertx;
+import io.vertx.core.json.Json;
+import io.vertx.core.json.JsonArray;
+import io.vertx.ext.web.Router;
+import io.vertx.ext.web.validation.BadRequestException;
+import io.vertx.ext.web.validation.BodyProcessorException;
+import io.vertx.ext.web.validation.ParameterProcessorException;
+import io.vertx.ext.web.validation.RequestParameters;
+import io.vertx.ext.web.validation.RequestPredicate;
+import io.vertx.ext.web.validation.RequestPredicateException;
+import io.vertx.ext.web.validation.ValidationHandler;
+import io.vertx.ext.web.validation.builder.Bodies;
+import io.vertx.ext.web.validation.builder.Parameters;
+import io.vertx.ext.web.validation.builder.ValidationHandlerBuilder;
+import io.vertx.json.schema.SchemaParser;
+import io.vertx.json.schema.SchemaRouter;
+import io.vertx.json.schema.SchemaRouterOptions;
+import io.vertx.json.schema.common.dsl.ObjectSchemaBuilder;
+
+public class ValidationHandlerOnRoutes {
+ //TODO when Quarkus use vert.x version 4.4.6 we can use SchemaRepository instead of SchemaParser with SchemaRouter
+ //private SchemaRepository schemaRepository =SchemaRepository.create(new JsonSchemaOptions().setDraft(Draft.DRAFT7).setBaseUri(BASEURI));
+ private SchemaParser schemaParser;
+ private SchemaRouter schemaRouter;
+
+ @Inject
+ Vertx vertx;
+
+ private static ShopResource shopResource = new ShopResource();
+
+ private static final String ERROR_MESSAGE = "{\"error\": \"%s\"}";
+ private static final String SHOPPINGLIST_NOT_FOUND = "Shopping list not found in the list or does not exist with that name or price";
+
+ @PostConstruct
+ void initialize() {
+ schemaParser = createSchema();
+ }
+
+ private SchemaParser createSchema() {
+ schemaRouter = SchemaRouter.create(vertx, new SchemaRouterOptions());
+ schemaParser = SchemaParser.createDraft7SchemaParser(schemaRouter);
+ return schemaParser;
+ }
+
+ public void validateHandlerShoppingList(@Observes Router router) {
+ AtomicReference queryAnswer = new AtomicReference<>();
+ router.get("/filterList")
+ .handler(ValidationHandlerBuilder
+ .create(schemaParser)
+ .queryParameter(param("shoppingListName", stringSchema()))
+ .queryParameter(param("shoppingListPrice", numberSchema().with(maximum(100)))).build())
+ .handler(routingContext -> {
+ RequestParameters parameters = routingContext.get(ValidationHandler.REQUEST_CONTEXT_KEY);
+ String shoppingListName = parameters.queryParameter("shoppingListName").getString();
+ Double totalPrice = parameters.queryParameter("shoppingListPrice").getDouble();
+
+ // Logic to list shoppingList based on shoppingListName and totalPrice
+ String shoppingListFound = fetchProductDetailsFromQuery(shoppingListName, totalPrice);
+ queryAnswer.set(shoppingListFound);
+
+ if (queryAnswer.get().equalsIgnoreCase(SHOPPINGLIST_NOT_FOUND)) {
+ routingContext.response().setStatusCode(HttpResponseStatus.NOT_FOUND.code());
+ }
+
+ routingContext.response().putHeader(HttpHeaders.CONTENT_TYPE, MediaType.TEXT_PLAIN).end(queryAnswer.get());
+ }).failureHandler(routingContext -> {
+ // Error handling:
+ if (routingContext.failure() instanceof BadRequestException ||
+ routingContext.failure() instanceof ParameterProcessorException ||
+ routingContext.failure() instanceof BodyProcessorException ||
+ routingContext.failure() instanceof RequestPredicateException) {
+
+ String errorMessage = routingContext.failure().toString();
+ routingContext.response()
+ .setStatusCode(HttpResponseStatus.BAD_REQUEST.code())
+ .putHeader(HttpHeaders.CONTENT_TYPE, MediaType.APPLICATION_JSON)
+ .end(String.format(ERROR_MESSAGE, errorMessage));
+ } else {
+ routingContext.next();
+ }
+
+ });
+ // Create a ValidationHandlerBuilder with explodedParam and arraySchema to filter by array items
+ ObjectSchemaBuilder bodySchemaBuilder = objectSchema()
+ .property("shoppingListName", stringSchema());
+ ValidationHandlerBuilder
+ .create(schemaParser)
+ .body(Bodies.json(bodySchemaBuilder));
+ router.get("/filterByArrayItem")
+ .handler(
+ ValidationHandlerBuilder
+ .create(schemaParser)
+ .queryParameter(Parameters.explodedParam("shoppingArray", arraySchema().items(stringSchema())))
+ .body(Bodies.json(bodySchemaBuilder))
+ .build())
+ .handler(routingContext -> {
+ RequestParameters parameters = routingContext.get(ValidationHandler.REQUEST_CONTEXT_KEY);
+ JsonArray myArray = parameters.queryParameter("shoppingArray").getJsonArray();
+ // Retrieve the list of all shoppingLists
+ List shoppingLists = fetchProductDetailsFromArrayQuery(myArray);
+
+ routingContext.response().putHeader(HttpHeaders.CONTENT_TYPE, MediaType.APPLICATION_JSON)
+ .end(Json.encodeToBuffer(shoppingLists));
+ });
+ // Let's allow to create a new item
+ router.post("/createShoppingList").handler(
+ ValidationHandlerBuilder
+ .create(schemaParser)
+ .predicate(RequestPredicate.BODY_REQUIRED)
+ .queryParameter(param("shoppingListName", stringSchema()))
+ .queryParameter(param("shoppingListPrice", numberSchema().with(maximum(100))))
+ .build())
+ .handler(routingContext -> {
+ routingContext.response().setStatusCode(HttpResponseStatus.OK.code()).end("Shopping list created");
+ });
+
+ }
+
+ public List fetchProductDetailsFromArrayQuery(JsonArray myArray) {
+ return shopResource.get().stream()
+ .filter(shoppingList -> myArray.contains(shoppingList.getName()))
+ .collect(Collectors.toList());
+ }
+
+ public String fetchProductDetailsFromQuery(String name, Double price) {
+ return shopResource.get().stream()
+ .filter(product -> name.equalsIgnoreCase(product.getName()) && price.equals(product.getPrice()))
+ .map(ShoppingList::toString)
+ .findFirst()
+ .orElse(SHOPPINGLIST_NOT_FOUND);
+ }
+
+}
diff --git a/http/vertx-web-validation/src/test/java/io/quarkus/ts/vertx/web/validation/OpenShiftVertxWebValidationIT.java b/http/vertx-web-validation/src/test/java/io/quarkus/ts/vertx/web/validation/OpenShiftVertxWebValidationIT.java
new file mode 100644
index 000000000..49497491f
--- /dev/null
+++ b/http/vertx-web-validation/src/test/java/io/quarkus/ts/vertx/web/validation/OpenShiftVertxWebValidationIT.java
@@ -0,0 +1,7 @@
+package io.quarkus.ts.vertx.web.validation;
+
+import io.quarkus.test.scenarios.OpenShiftScenario;
+
+@OpenShiftScenario
+public class OpenShiftVertxWebValidationIT extends VertxWebValidationIT {
+}
diff --git a/http/vertx-web-validation/src/test/java/io/quarkus/ts/vertx/web/validation/VertxWebValidationIT.java b/http/vertx-web-validation/src/test/java/io/quarkus/ts/vertx/web/validation/VertxWebValidationIT.java
new file mode 100644
index 000000000..5117c6cd3
--- /dev/null
+++ b/http/vertx-web-validation/src/test/java/io/quarkus/ts/vertx/web/validation/VertxWebValidationIT.java
@@ -0,0 +1,124 @@
+package io.quarkus.ts.vertx.web.validation;
+
+import static org.hamcrest.MatcherAssert.assertThat;
+import static org.hamcrest.Matchers.containsString;
+import static org.hamcrest.Matchers.equalTo;
+
+import org.apache.http.HttpStatus;
+import org.junit.jupiter.api.Test;
+
+import io.quarkus.test.bootstrap.RestService;
+import io.quarkus.test.scenarios.QuarkusScenario;
+import io.quarkus.test.services.QuarkusApplication;
+import io.restassured.http.ContentType;
+import io.restassured.response.Response;
+
+@QuarkusScenario
+public class VertxWebValidationIT {
+ @QuarkusApplication
+ static RestService restserviceapp = new RestService();
+
+ private static final String SHOPPING_LIST_URL = "/shoppinglist";
+ private static final String LIST_NAME1 = "name=ListName1, products=[Carrots, Water, Cheese, Beer], price=25.0";
+ private static final String LIST_NAME2 = "name=ListName2, products=[Meat, Wine, Almonds, Potatoes, Cake], price=80.0";
+ private static final String FILTER_BY_NAME_PRICE_URL_1 = "/filterList?shoppingListName=ListName1&shoppingListPrice=25";
+ private static final String FILTER_BY_NAME_PRICE_URL_2 = "/filterList?shoppingListName=ListName2&shoppingListPrice=80";
+ private static final String FILTER_BY_WRONG_NAME_PRICE_URL = "/filterList?shoppingListName=ListName35&shoppingListPrice=25";
+ private static final String ONLY_FILTER_BY_NAME = "/filterList?shoppingListName=ListName1";
+ private static final String PRICE_OUT_OF_RANGE = "/filterList?shoppingListName=ListName1&shoppingListPrice=125";
+ private static final String FILTER_BY_LIST_NAMES = "/filterByArrayItem?shoppingArray=ListName1&shoppingArray=ListName2";
+
+ private static final String ERROR_PARAMETER_MISSING = "ParameterProcessorException";
+
+ @Test
+ void checkShoppingListUrl() {
+ Response response = restserviceapp.given()
+ .get(SHOPPING_LIST_URL)
+ .then()
+ .statusCode(200)
+ .contentType(ContentType.JSON)
+ .extract()
+ .response();
+
+ assertThat(response.getBody().jsonPath().getString("name"), containsString("[ListName1, ListName2]"));
+ }
+
+ @Test
+ void checkNamePriceParams() {
+ Response response = restserviceapp
+ .given()
+ .get(FILTER_BY_NAME_PRICE_URL_1)
+ .then()
+ .statusCode(HttpStatus.SC_OK).extract().response();
+ assertThat(response.asString(), containsString(LIST_NAME1));
+ Response response2 = restserviceapp
+ .given()
+ .get(FILTER_BY_NAME_PRICE_URL_2)
+ .then()
+ .statusCode(HttpStatus.SC_OK).extract().response();
+ assertThat(response2.asString(), containsString(LIST_NAME2));
+ }
+
+ @Test
+ void checkWrongNamePriceParams() {
+ Response response = restserviceapp
+ .given()
+ .get(FILTER_BY_WRONG_NAME_PRICE_URL)
+ .then()
+ .statusCode(HttpStatus.SC_NOT_FOUND).extract().response();
+ assertThat(response.asString(),
+ containsString("Shopping list not found in the list or does not exist with that name or price"));
+ }
+
+ @Test
+ void checkParameterMissingError() {
+ Response response = restserviceapp
+ .given()
+ .get(ONLY_FILTER_BY_NAME)
+ .then()
+ .statusCode(HttpStatus.SC_BAD_REQUEST)
+ .extract()
+ .response();
+ assertThat(response.asString(), containsString(ERROR_PARAMETER_MISSING));
+ assertThat(response.asString(), containsString("Missing parameter shoppingListPrice in QUERY"));
+ }
+
+ @Test
+ void checkPriceOutOfRangeError() {
+ Response response = restserviceapp
+ .given()
+ .get(PRICE_OUT_OF_RANGE)
+ .then()
+ .statusCode(HttpStatus.SC_BAD_REQUEST)
+ .extract()
+ .response();
+ assertThat(response.asString(), containsString(ERROR_PARAMETER_MISSING));
+ assertThat(response.asString(), containsString("value should be <= 100.0"));
+ }
+
+ @Test
+ void checkFilterByArrayListName() {
+ Response response = restserviceapp
+ .given()
+ .get(FILTER_BY_LIST_NAMES)
+ .then()
+ .statusCode(HttpStatus.SC_OK).extract().response();
+ assertThat(response.getBody().jsonPath().getString("name"), containsString("[ListName1, ListName2]"));
+ assertThat(response.getBody().jsonPath().getString("price"), equalTo("[25.0, 80.0]"));
+ }
+
+ @Test
+ void createShoppingList() {
+ restserviceapp.given()
+ .contentType(ContentType.JSON)
+ .body("{}")
+ .queryParam("shoppingListName", "MyList3")
+ .queryParam("shoppingListPrice", 50)
+ .when()
+ .post("/createShoppingList")
+ .then()
+ .statusCode(200)
+ .body(equalTo("Shopping list created"));
+ }
+
+}
diff --git a/pom.xml b/pom.xml
index a5593ea9c..1bfc16ac3 100644
--- a/pom.xml
+++ b/pom.xml
@@ -466,6 +466,7 @@
http/rest-client-reactive
http/servlet-undertow
http/vertx-web-client
+ http/vertx-web-validation
http/hibernate-validator
http/graphql
http/graphql-telemetry