Before diving into the code, let's ask a simple but important question: why do we even need validation? Think of your REST API as the entrance to a party, and request data as the guests. You can't let everyone in — you need to make sure the data meets certain criteria (for example, age over 18, no suspicious stuff like "NaN" instead of a name). If inappropriate "guests" get in, the app can "put on a show", crash with errors, or "set the server on fire".
Validation solves this by checking request data before it's processed. It helps prevent pointless server errors, keeps data consistent, and makes debugging easier.
Basics of the Bean Validation API
Spring uses the Bean Validation API specification to implement data checks. This spec was introduced in Java EE 6 back in 2009 (yeah, that's ancient magic in IT terms). In practice we usually use the Hibernate Validator implementation, which comes with Spring Boot by default.
The core idea of Bean Validation is using annotations to declare validation rules at the model level (the DTO or entity classes). Here's a list of popular annotations:
| Annotation | Description |
|---|---|
@NotNull |
Field can't be null |
@Size |
Specifies the minimum and maximum allowed length of a string |
@Min |
Minimum value for numeric fields |
@Max |
Maximum value for numeric fields |
@Email |
Validates the string as an email address format |
@Pattern |
Allows you to set a regular expression to validate a string |
These annotations attach to fields and do the validation magic, but their power is unlocked by @Valid.
Applying Bean Validation in a Spring REST API
Now let's move from theory to practice. We'll validate incoming request data. For the example, let's create a simple API to manage users (because everyone loves user APIs).
Creating a User DTO
A DTO (Data Transfer Object) is a class that carries data between client and server. First, let's create a DTO with validation annotations:
package com.example.demo.dto;
import jakarta.validation.constraints.Email;
import jakarta.validation.constraints.NotNull;
import jakarta.validation.constraints.Size;
public class UserDTO {
@NotNull(message = "Username can't be empty")
@Size(min = 2, max = 50, message = "Username must be between 2 and 50 characters")
private String name;
@NotNull(message = "Email is required")
@Email(message = "Invalid email format")
private String email;
@NotNull(message = "Password can't be empty")
@Size(min = 6, message = "Password must be at least 6 characters")
private String password;
// Getters and setters...
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public String getEmail() {
return email;
}
public void setEmail(String email) {
this.email = email;
}
public String getPassword() {
return password;
}
public void setPassword(String password) {
this.password = password;
}
}
Here we've set constraints for each field. For example, the username must be between 2 and 50 characters, and the email must match the proper format.
Adding validation to the controller
Validation happens automatically if you put the @Valid annotation before the request object. Let's create a controller to create a user:
package com.example.demo.controller;
import com.example.demo.dto.UserDTO;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.*;
import jakarta.validation.Valid;
@RestController
@RequestMapping("/users")
public class UserController {
@PostMapping
public ResponseEntity<String> createUser(@Valid @RequestBody UserDTO user) {
// The passed object has already been validated if we're here!
return new ResponseEntity<>("User successfully created: " + user.getName(), HttpStatus.CREATED);
}
}
So, what's happening here?
- We added
@Validbefore theuserparameter. This tells Spring to validate the object before invoking the method. - If the data in
userdoesn't meet the requirements, Spring automatically throws a 400 (Bad Request) and returns the error details.
Now let's simulate a client sending bad data.
Example of a bad request
The client sends an HTTP POST to /users with this body:
{
"name": "",
"email": "not-an-email",
"password": "123"
}
Spring will trigger and respond with an error description like this:
{
"timestamp": "2023-10-10T12:00:00.000+00:00",
"status": 400,
"errors": [
"Username can't be empty",
"Invalid email format",
"Password must be at least 6 characters"
]
}
Note that the error messages come from the message attributes in our DTO annotations.
Handling validation errors
The default error handling isn't always convenient. Let's customize it with an @ExceptionHandler.
Create a global exception handler:
package com.example.demo.exception;
import jakarta.validation.ConstraintViolationException;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.validation.FieldError;
import org.springframework.web.bind.MethodArgumentNotValidException;
import org.springframework.web.bind.annotation.ExceptionHandler;
import org.springframework.web.bind.annotation.RestControllerAdvice;
import java.util.HashMap;
import java.util.Map;
@RestControllerAdvice
public class GlobalExceptionHandler {
@ExceptionHandler(MethodArgumentNotValidException.class)
public ResponseEntity<Map<String, String>> handleValidationExceptions(MethodArgumentNotValidException ex) {
Map<String, String> errors = new HashMap<>();
for (FieldError fieldError : ex.getBindingResult().getFieldErrors()) {
errors.put(fieldError.getField(), fieldError.getDefaultMessage());
}
return new ResponseEntity<>(errors, HttpStatus.BAD_REQUEST);
}
}
Now our response to a bad request will look like this:
{
"name": "Username can't be empty",
"email": "Invalid email format",
"password": "Password must be at least 6 characters"
}
This representation is easier for clients to read and handle.
Real-world use and common mistakes
Where is this useful?
- In interviews. Knowing how to validate data is a basic skill for any backend developer. You'll definitely be asked: "How do you handle invalid data in an API?"
- In real projects. Validation protects the server from junk data and makes the codebase easier to maintain.
Common mistakes
- Forgot
@Valid. If you don't add@Valid, the data simply won't be validated. - Poor error messages. Always try to write clear and friendly messages for users.
- Database-level constraints. Remember that code-level validation doesn't replace DB-level constraints like NOT NULL or CHECK.
Now you not only know how to validate data in Spring, but you can also avoid common traps!
GO TO FULL VERSION