Validate What You Receive: Hibernate Validator for Quarkus REST Clients
Go beyond request validation: Learn how to validate REST client responses in Quarkus to stop bad data from upstream services before it breaks your system.
In my previous tutorial, I focused on making our REST endpoints rock solid.
We built a Quarkus API that rejected bad input before it reached our business logic with Hibernate Validator to enforce constraints, guard object graphs, and ensure data consistency throughout the request lifecycle.
That’s half the battle.
In modern microservices, your application is not just an API provider. It’s also an API consumer. It constantly calls other services, often owned by different teams or external partners. These upstream systems might return malformed, incomplete, or outdated data. And if your service trusts that data blindly, you risk polluting your entire domain model with invalid information.
This article continues our Hibernate Validator journey by turning the lens around.
We’ll see how to validate REST client responses in Quarkus, ensuring that everything your service receives is just as trustworthy as what it sends.
You’ll build a second Quarkus project to simulate an external API, integrate validation into REST client calls, and finally automate it across your service boundaries using interceptors.
Keep ./mvnw quarkus:dev running and get ready to see how defensive programming and validation can make your microservices bulletproof.
Why Validate REST Client Responses?
Let’s imagine two microservices:
Order Service — calls
Product Service — to fetch product data.
Now, if the Product Service returns a product with a missing name or a zero price, your Order Service might continue processing it, maybe even persisting invalid data in your database.
By adding validation to REST client responses, we can stop invalid payloads before they enter our business logic. You can follow the steps in this tutorial or just look at the source code in my Github repository.
Setting Up the Quarkus Project
Create a folder that holds both client and server:
mkdir rest-client-validation-demo
cd rest-client-validation-demoCreate a new Quarkus app with REST client support:
quarkus create app org.acme:rest-client:1.0.0 \
-x rest-client-jackson,rest-jackson,hibernate-validator
cd rest-clientDefining the Response Model
Let’s define a Product class representing the data we expect from the remote service.
src/main/java/org/acme/client/Product.java
package org.acme.client;
import jakarta.validation.constraints.Min;
import jakarta.validation.constraints.NotBlank;
public class Product {
@NotBlank
private String name;
@Min(1)
private int price;
// getters and setters
}We want to guarantee that every product has a name and a positive price.
Creating the REST Client Interface
The REST client will call the remote service to fetch product details.
src/main/java/org/acme/client/ProductClient.java
package org.acme.client;
import org.eclipse.microprofile.rest.client.inject.RegisterRestClient;
import jakarta.ws.rs.GET;
import jakarta.ws.rs.Path;
import jakarta.ws.rs.Produces;
import jakarta.ws.rs.core.MediaType;
@Path(”/products”)
@RegisterRestClient(configKey = “product-api”)
public interface ProductClient {
@GET
@Produces(MediaType.APPLICATION_JSON)
Product getProduct();
}Add a simple configuration so Quarkus knows where to reach the remote API:
src/main/resources/application.properties
quarkus.rest-client.product-api.url=http://localhost:8082We’ll soon build that remote service.
Validating the Client Response
The @Valid annotation alone won’t automatically validate REST client responses in Quarkus.
Instead, we can inject a Validator and explicitly check the deserialized object.
src/main/java/org/acme/client/ProductService.java
package org.acme.client;
import org.eclipse.microprofile.rest.client.inject.RestClient;
import jakarta.enterprise.context.ApplicationScoped;
import jakarta.inject.Inject;
import jakarta.validation.ConstraintViolationException;
import jakarta.validation.Validator;
@ApplicationScoped
public class ProductService {
@Inject
@RestClient
ProductClient client;
@Inject
Validator validator;
public Product getValidatedProduct() {
Product product = client.getProduct();
var violations = validator.validate(product);
if (!violations.isEmpty()) {
throw new ConstraintViolationException(violations);
}
return product;
}
}This step ensures that every product we receive from the remote service passes validation before continuing into our business logic.
Exposing a Resource to Test
Let’s make a simple REST endpoint that calls our validated service.
src/main/java/org/acme/client/ProductResource.java
package org.acme.client;
import jakarta.inject.Inject;
import jakarta.ws.rs.GET;
import jakarta.ws.rs.Path;
import jakarta.ws.rs.core.Response;
@Path(”/validated-products”)
public class ProductResource {
@Inject
ProductService service;
@GET
public Response getValidatedProduct() {
return Response.ok(service.getValidatedProduct()).build();
}
}Building a Second Quarkus Service to Simulate the External API
In real-world architectures, your Quarkus application usually calls a separate service managed by another team.
To mirror that setup, let’s create a second Quarkus project that acts as the remote Product Service.
It will run on port 8081 and deliberately return incomplete data so we can test how our validation logic reacts.
Create a new Quarkus app with REST support:
cd ..
quarkus create app org.acme:rest-server:1.0.0 \
-x rest-jackson
--no-code
cd rest-serverOpen application.properties and set a distinct port so it doesn’t clash with your main service:
quarkus.http.port=8082Implement the External Endpoint
Create a simple REST endpoint that pretends to be the remote product provider.
src/main/java/org/acme/server/ProductResource.java
package org.acme.server;
import jakarta.ws.rs.GET;
import jakarta.ws.rs.Path;
import jakarta.ws.rs.Produces;
import jakarta.ws.rs.core.MediaType;
import java.util.Map;
@Path(”/products”)
public class ProductResource {
@GET
@Produces(MediaType.APPLICATION_JSON)
public Map<String, Object> getProduct() {
// Simulate a bad response from an upstream system
return Map.of(
“name”, “”,
“price”, 0
);
}
}Start this second Quarkus app:
./mvnw quarkus:devYou should now have two services running:
Main validation app →
http://localhost:8080
External product API → http://localhost:8081/products
If you open http://localhost:8081/products in your browser, you’ll see the mock JSON response:
{”name”:”“,”price”:0}Perfect. Your “unreliable” external API is alive and ready for testing.
Testing the Validation Behavior
Now we’ll call /validated-products to see how the service handles invalid data. Back to the client project.
src/test/java/org/acme/client/ProductResponseValidationTest.java
package org.acme.client;
import static org.hamcrest.Matchers.containsString;
import org.junit.jupiter.api.Test;
import io.quarkus.test.junit.QuarkusTest;
import io.restassured.RestAssured;
@QuarkusTest
class ProductResponseValidationTest {
@Test
void shouldFailWhenRemoteProductIsInvalid() {
RestAssured.when()
.get(”/validated-products”)
.then()
.statusCode(500)
.body(containsString(”ConstraintViolationException”));
}
}Run the test with:
./mvnw testThe service detects the invalid response and rejects it before it contaminates downstream logic.
[INFO] Tests run: 1, Failures: 0, Errors: 0, Skipped: 0, Time elapsed: 0.663 s -- in org.acme.client.ProductResponseValidationTestAutomating Validation for All REST Clients
If your application calls multiple remote services, you don’t want to trigger the same manual validation everywhere. And also prevent someone from forgetting about triggering it.
We can encapsulate this pattern in an interceptor. Back in the client project:
src/main/java/org/acme/client/interceptor/ValidatedResponse.java
package org.acme.client.interceptor;
import java.lang.annotation.ElementType;
import java.lang.annotation.Inherited;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
import jakarta.interceptor.InterceptorBinding;
@Inherited
@InterceptorBinding
@Target({ ElementType.TYPE, ElementType.METHOD })
@Retention(RetentionPolicy.RUNTIME)
public @interface ValidatedResponse {
}src/main/java/org/acme/client/interceptor/ResponseValidationInterceptor.java
package org.acme.client.interceptor;
import jakarta.inject.Inject;
import jakarta.interceptor.AroundInvoke;
import jakarta.interceptor.Interceptor;
import jakarta.interceptor.InvocationContext;
import jakarta.validation.ConstraintViolationException;
import jakarta.validation.Validator;
@Interceptor
@ValidatedResponse
public class ResponseValidationInterceptor {
@Inject
Validator validator;
@AroundInvoke
Object validateResponse(InvocationContext ctx) throws Exception {
Object result = ctx.proceed();
if (result != null) {
var violations = validator.validate(result);
if (!violations.isEmpty()) {
throw new ConstraintViolationException(violations);
}
}
return result;
}
}Now simply annotate your ProductService method:
@ValidatedResponse
public Product getValidatedProduct() {Every response returned by the client will automatically be validated and it does not require any manual checks.
Why This Matters
Validating REST client responses closes a blind spot in many distributed systems.
It ensures that even if an upstream service breaks its contract or sends incomplete data, your application remains stable and predictable.
Benefits:
Protects internal state from corrupt data
Makes failures explicit and easy to trace
Prevents silent bugs in data pipelines
Strengthens microservice boundaries
Validation doesn’t stop at the edge of your API.
With Quarkus and Hibernate Validator, you can apply the same safety net to incoming requests, internal services, and remote responses alike.
By taking these extra steps, your applications become self-defensive and resilient against invalid inputs from anywhere in the system.



