This tutorial demonstrates that Bean Validation integrates with OfficeFloor REST YAML composition without any extra plumbing. The same @Valid, @Validated, and BindingResult patterns that work in a @RestController work unchanged in OfficeFloor service methods.
Two endpoints are covered:
POST /order — annotated with @Validated and @Valid; invalid requests are rejected automatically with 400 Bad Request before the service method body executes.POST /order/binding — uses BindingResult to receive validation errors inside the service method and build a structured error response.Add spring-boot-starter-validation alongside the OfficeFloor starter. No additional OfficeFloor-specific validation modules are needed:
<dependency> <groupId>net.officefloor.springboot</groupId> <artifactId>officefloor-rest-spring-boot-starter</artifactId> </dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-web</artifactId> </dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-validation</artifactId> </dependency>
@SpringBootApplication
public class SpringRestValidationApplication {
public static void main(String[] args) {
SpringApplication.run(SpringRestValidationApplication.class, args);
}
}
Constraints are declared directly on the request class fields using standard Jakarta Validation annotations:
public class OrderRequest {
@NotBlank(message = "Product name is required")
private String product;
@Min(value = 1, message = "Quantity must be at least 1")
private int quantity;
public String getProduct() { return product; }
public void setProduct(String product) { this.product = product; }
public int getQuantity() { return quantity; }
public void setQuantity(int quantity) { this.quantity = quantity; }
}
@Validated + @ValidPlacing @Validated on the service class and @Valid on the @RequestBody parameter is all that is required. When a request fails validation, OfficeFloor returns 400 Bad Request before the method body is entered — the service method is never called for an invalid request:
service: class: net.officefloor.tutorial.springrestvalidation.PlaceOrderService
@Validated
public class PlaceOrderService {
public void service(@Valid @RequestBody OrderRequest request,
ObjectResponse<OrderResponse> response) {
response.send(new OrderResponse("Ordered " + request.getQuantity()
+ " x " + request.getProduct()));
}
}
This is identical to how @Validated works on a Spring @RestController — OfficeFloor delegates to Spring's validation infrastructure without any additional configuration.
BindingResultWhen the service method needs to inspect validation errors itself — for example to return a structured error body — declare a BindingResult parameter immediately after the @Valid-annotated parameter. OfficeFloor passes validation errors through BindingResult and the method body always executes, even when there are errors:
service: class: net.officefloor.tutorial.springrestvalidation.PlaceOrderBindingService
@Validated
public class PlaceOrderBindingService {
public void service(@Valid @RequestBody OrderRequest request, BindingResult result,
ObjectResponse<ResponseEntity<OrderResponse>> response) {
if (result.hasErrors()) {
String errors = result.getFieldErrors().stream()
.map(e -> e.getField() + ": " + e.getDefaultMessage())
.reduce((a, b) -> a + "; " + b)
.orElse("Validation failed");
response.send(ResponseEntity.badRequest().body(new OrderResponse(errors)));
return;
}
response.send(ResponseEntity.ok(new OrderResponse(
"Ordered " + request.getQuantity() + " x " + request.getProduct())));
}
}
The BindingResult parameter must immediately follow the @Valid-annotated parameter, exactly as required in Spring MVC.
Tests use Spring Boot's MockMvc — no OfficeFloor-specific test utilities are required:
@SpringBootTest
@AutoConfigureMockMvc
public class SpringRestValidationTest {
@Autowired
private MockMvc mvc;
@Autowired
private ObjectMapper mapper;
@Test
public void valid_request_is_accepted() throws Exception {
OrderRequest request = new OrderRequest();
request.setProduct("Widget");
request.setQuantity(3);
mvc.perform(post("/order")
.contentType(MediaType.APPLICATION_JSON)
.content(mapper.writeValueAsString(request)))
.andExpect(status().isOk())
.andExpect(jsonPath("$.message").value("Ordered 3 x Widget"));
}
@Test
public void invalid_request_is_rejected_with_400() throws Exception {
OrderRequest request = new OrderRequest();
request.setProduct(""); // blank — violates @NotBlank
request.setQuantity(0); // zero — violates @Min(1)
mvc.perform(post("/order")
.contentType(MediaType.APPLICATION_JSON)
.content(mapper.writeValueAsString(request)))
.andExpect(status().isBadRequest());
}
@Test
public void binding_result_valid_request_is_accepted() throws Exception {
OrderRequest request = new OrderRequest();
request.setProduct("Gadget");
request.setQuantity(5);
mvc.perform(post("/order/binding")
.contentType(MediaType.APPLICATION_JSON)
.content(mapper.writeValueAsString(request)))
.andExpect(status().isOk())
.andExpect(jsonPath("$.message").value("Ordered 5 x Gadget"));
}
@Test
public void binding_result_invalid_request_returns_error_details() throws Exception {
OrderRequest request = new OrderRequest();
request.setProduct(""); // blank — violates @NotBlank
request.setQuantity(0); // zero — violates @Min(1)
mvc.perform(post("/order/binding")
.contentType(MediaType.APPLICATION_JSON)
.accept(MediaType.APPLICATION_JSON)
.content(mapper.writeValueAsString(request)))
.andExpect(status().isBadRequest())
.andExpect(jsonPath("$.message").isNotEmpty());
}
}
The Spring REST CORS tutorial demonstrates the five ways to configure Cross-Origin Resource Sharing for OfficeFloor REST endpoints.