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 { registerLocaleData } from '@angular/common'; import localeFr from '@angular/common/locales/fr'; import localeFrExtra from '@angular/common/locales/extra/fr'; import localeDe from '@angular/common/locales/de';
Locale-aware bootstrapping in Angular
i18n
localization
dependency-injection
Intermediate
8 steps
typescript
import { Module } from '@nestjs/common'; import { ConfigModule } from '@nestjs/config'; import * as Joi from 'joi';
Validating env config at boot in NestJS
configuration
schema-validation
environment-variables
Intermediate
8 steps
php
<?php namespace App\Services\Checkout;
Validating coupons with Laravel's Pipeline
pipeline
chain of responsibility
transactions
Intermediate
7 steps
java
@Component @Converter public class EncryptedStringConverter implements AttributeConverter<String, String> {
Transparent column encryption in Spring & JPA
encryption
aes-gcm
jpa-converter
Advanced
10 steps
typescript
import { Inject, Injectable, Logger } from '@nestjs/common'; import { CACHE_MANAGER } from '@nestjs/cache-manager'; import { Cache } from 'cache-manager'; import { InjectRepository } from '@nestjs/typeorm';
A cache-aside country lookup in NestJS
cache-aside
dependency-injection
batch-lookup
Intermediate
8 steps
php
<?php namespace App\Services;
How a password strength validator works in PHP
validation
regular-expressions
data-driven
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.