Fork me on GitHub

Bean Validation in OfficeFloor REST YAML Composition

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.

Tutorial Source

Maven dependency

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>

Application class

@SpringBootApplication
public class SpringRestValidationApplication {

	public static void main(String[] args) {
		SpringApplication.run(SpringRestValidationApplication.class, args);
	}
}

Request model

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; }
}

Automatic rejection — @Validated + @Valid

Placing @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.

Manual error handling — BindingResult

When 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.

Testing

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());
	}
}

Next

The Spring REST CORS tutorial demonstrates the five ways to configure Cross-Origin Resource Sharing for OfficeFloor REST endpoints.