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
typescript
import { Injectable, PipeTransform, ArgumentMetadata,
A custom validation pipe in NestJS
validation
dto
recursion
Intermediate
10 steps
ruby
module CreditCard module_function def valid?(number)
Validating credit card numbers with Luhn
checksum
luhn-algorithm
validation
Intermediate
6 steps
go
package handlers type ListFilters struct { Status string `form:"status" binding:"omitempty,oneof=active archived all"`
Cross-field query validation in Gin
validation
struct-tags
query-binding
Intermediate
9 steps
java
public final class Levenshtein { private Levenshtein() { }
Two-row Levenshtein distance in Java
dynamic-programming
edit-distance
space-optimization
Intermediate
8 steps
java
package com.example.lb; import java.util.List; import java.util.concurrent.atomic.AtomicInteger;
A thread-safe round-robin load balancer in Java
concurrency
load-balancing
round-robin
Intermediate
9 steps
typescript
import { NestFactory } from '@nestjs/core'; import { DocumentBuilder, SwaggerModule } from '@nestjs/swagger'; import { ValidationPipe } from '@nestjs/common'; import { ApiProperty } from '@nestjs/swagger';
Wiring validation and Swagger docs in NestJS
validation
openapi
decorators
Intermediate
8 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.