java
45 lines · 9 steps
Bean Validation in a Spring REST controller
A Spring controller validates request bodies with annotations and turns validation failures into structured 400 responses.
Explained by
highlit
1@RestController
2@RequestMapping("/api/users")
3public class UserController {
4
5 private final UserService userService;
6
7 public UserController(UserService userService) {
8 this.userService = userService;
9 }
10
11 @PostMapping
12 public ResponseEntity<UserResponse> create(@Valid @RequestBody CreateUserRequest request) {
13 User user = userService.register(request);
14 return ResponseEntity.status(HttpStatus.CREATED).body(UserResponse.from(user));
15 }
16}
17
18public record CreateUserRequest(
19 @NotBlank(message = "name is required")
20 String name,
21
22 @Email(message = "must be a valid email address")
23 @NotBlank(message = "email is required")
24 String email,
25
26 @Size(min = 8, message = "password must be at least 8 characters")
27 String password
28) {}
29
30@RestControllerAdvice
31public class ValidationExceptionHandler {
32
33 @ExceptionHandler(MethodArgumentNotValidException.class)
34 @ResponseStatus(HttpStatus.BAD_REQUEST)
35 public ValidationErrorResponse handleValidation(MethodArgumentNotValidException ex) {
36 List<FieldErrorDetail> errors = ex.getBindingResult().getFieldErrors().stream()
37 .map(err -> new FieldErrorDetail(err.getField(), err.getDefaultMessage()))
38 .toList();
39 return new ValidationErrorResponse("Validation failed", Instant.now(), errors);
40 }
41
42 public record ValidationErrorResponse(String message, Instant timestamp, List<FieldErrorDetail> errors) {}
43
44 public record FieldErrorDetail(String field, String message) {}
45}
01 / 01
STEP 01
‹ swipe to step through ›
Walkthrough
Space play
←→ step
click any line
Three takeaways
- 1Pairing @Valid with @RequestBody makes Spring enforce Bean Validation constraints before your handler runs.
- 2A @RestControllerAdvice centralizes error formatting so every controller returns consistent failure responses.
- 3Records make concise, immutable DTOs for both incoming requests and outgoing error payloads.
Related explainers
java
@Service public class ProductCatalogService { private final ProductRepository productRepository;
How Spring cache annotations keep data fresh
caching
cache-invalidation
dependency-injection
Intermediate
7 steps
python
from flask import Blueprint, jsonify, request, abort from .models import Article, db from .schemas import article_schema, articles_schema
Building a REST articles API with Flask Blueprints
rest-api
blueprints
serialization
Intermediate
7 steps
javascript
const asyncHandler = (fn) => (req, res, next) => { Promise.resolve(fn(req, res, next)).catch(next); };
Async error handling in Express routes
async-await
error-handling
middleware
Intermediate
7 steps
java
@Service public class OrderService { private final OrderRepository orderRepository;
Decoupling side effects with Spring events
event-driven
transactions
decoupling
Intermediate
8 steps
java
public Map<Long, List<Order>> ordersByCustomer(List<Order> orders) { return orders.stream() .collect(Collectors.groupingBy(Order::getCustomerId)); }
Grouping streams with Java Collectors
streams
grouping
collectors
Intermediate
5 steps
php
<?php namespace App\Http\Controllers;
Building a filtered product index in Laravel
query-builder
validation
conditional-queries
Intermediate
9 steps
Share this explainer
Here's the card — post it anywhere.
Made with highlit — turn any snippet into a walkthrough like this in about a minute.
Explain your code
Embed this explainer
Drop the interactive walkthrough into a blog or docs. Views never cost a credit.
<iframe src="https://highlit.co/explainers/bean-validation-in-a-spring-rest-controller-explained-java-e741/embed?autoplay=1" width="100%" height="520" loading="lazy" style="border:0"></iframe>
Autoplay is on by default — add ?autoplay=0 to start paused.