So far, your Categories API accepts whatever the client sends, even a category with no name or a missing image.
Try POST /api/categories with empty image and name:
{
"name": "",
"image": ""
}
Currently it works. That is a problem.
In production, invalid data causes bugs downstream: corrupted databases, NullPointerException errors in business logic, and confused users. Validation checks data at the door, rejects what is wrong, and tells the client exactly what to fix. Spring Boot makes this declarative: you annotate fields, then Spring checks them when validation is triggered.
WeekFourRestAPI the category API we made in Writing Endpoints.
The CreateCategoryRequest record from Message Formats
The client can be anything: a browser, a mobile app, a script, a malicious actor. You cannot assume the data it sends is correct, complete, or safe.
Three reasons to validate:
name or an image value of "!! NOT AN IMAGE !!" should never be stored.image URL, or unexpected field values. Validation is one layer of defense."Bad Request" helps nobody; "Name is required" helps everyone.<aside> 💡
Always validate on the server; optionally validate on the client too: Frontend validation is a UX convenience. It can be bypassed with curl, Postman, or browser dev tools. Server-side validation is the real security boundary.
</aside>
Always validate on the server; optionally validate on the client too.
Like with DTOs and domain models, this is also a separation of concerns issue.
Validation on the frontend serves a different purpose. It improves the user experience by giving fast feedback before the request is sent. It can still be bypassed with curl, Postman, browser dev tools, or a custom script.
Server-side validation is connected to business logic and application safety. It protects your server from invalid input that could break functionality or support an attack. It is the real security boundary because the application runs on a server you control, not inside a client browser.
The same principle from Core Program applies here, except for never trust user input, which becomes never trust input. At the API level, anything can arrive:
null values in fields, or a null request bodySpring Boot's validation support comes from the spring-boot-starter-validation dependency. It is not included in the default Spring Web starter, so you need to add it explicitly.
Here is the Maven Central Repository page for it: https://central.sonatype.com/artifact/org.springframework.boot/spring-boot-starter-validation
Add the dependency:
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-validation</artifactId>
</dependency>
<aside> 💡
In Spring Initializr, you can add Validation as a dependency during project creation. If you already have a project, add it manually to pom.xml and reload your Maven project in the IDE.
</aside>
If your project uses spring-boot-starter-parent, Spring Boot manages the dependency version for you. That keeps the validation starter compatible with the rest of your Spring Boot dependencies.
Under the hood, this starter pulls in Hibernate Validator, which implements the Jakarta Bean Validation specification.
You do not interact directly with Hibernate Validator in this chapter. Instead, you use standard validation annotations, such as @NotNull, @Size, and @Email, from the jakarta.validation package. These annotations are part of the Jakarta Validation API included through the validation starter.
The jakarta.validation.constraints package provides annotations you place on fields or record components to declare validation rules. When Spring validates the object, it checks each annotated value.
Here are the most commonly used annotations:
| Annotation | What it checks | Example use case |
|---|---|---|
@NotNull |
Field is not null |
Required object references |
@NotBlank |
String is not null, not empty, not only whitespace |
Required text fields (title, name) |
@NotEmpty |
String/collection is not null and not empty |
Required lists |
@Size(min=, max=) |
String length or collection size is within range | Category name between 1–100 chars |
@Min(value) |
Numeric value ≥ minimum | Minimum allowed price on a product |
@Max(value) |
Numeric value ≤ maximum | Maximum allowed price on a product |
@Email |
String matches email format | Contact email fields |
@Pattern(regexp=) |
String matches regex | URL-safe slug, image URL |
@Positive / |
||
@PositiveOrZero |
Numeric value > 0 / ≥ 0 | Price, quantity |
<aside> ❗
@NotNull vs @NotBlank: @NotNull only checks for null, so an empty string "" passes. For String fields, use @NotBlank to reject null, empty strings, and whitespace-only strings. This is one of the most common validation mistakes beginners make.
</aside>
<aside> 💡
In real-world projects, you often don’t write the validation annotations by hand. Just like with DTOs, they can be generated from OpenAPI specifications.
Still, if you know these annotations, you can do much more with your OpenAPI specifications because you know what to expect. Eg: If there is an @Email annotation, then very likely there is a way to declare that in your OpenAPI spec.
</aside>
Imagine you already know a request is going to fail. You can’t make it succeed, but you can control exactly when it fails.
In such cases, failing as early as possible is the best approach from both a security and resource-efficiency perspective.
This makes DTOs the ideal place for our input validations because invalid input would be rejected before reaching our domain logic, services, or database.
💬 Can you think of a few other advantages of putting validations on the DTOs rather than on the domain models?
CreateCategoryRequestLet’s apply some input validation to our CreateCategoryRequest DTO ( The Java record from previous chapter, it’s also shared in the pre-requisites section above ) .
For name and image , we can add the below :
public record CreateCategoryRequest(
@NotBlank(message = "Name is required")
@Size(max = 100, message = "Name must be 100 characters or fewer")
String name,
@NotBlank(message = "Image is required")
@Pattern(regexp = "^https?://.*", message = "Image must be a valid http(s) URL")
String image) {}
If you test whether the above annotations work on our server by making a bad request from Postman, you’ll notice that the request still succeeds.

This is because, if you recall from Java Annotations chapter, these annotations have NO effect by themselves. They are just metadata. Spring needs to be told to check them and that's where @Valid comes into picture.
@Valid@Valid tells Spring: "Before calling this method, validate the object using its Bean Validation annotations." If validation fails, Spring does not call the method. It throws MethodArgumentNotValidException instead.
Add it to the request body parameter for createCategory:
@PostMapping
public ResponseEntity<Category> createCategory(@Valid @RequestBody CreateCategoryRequest request) {
long nextId = categories.stream()
.map(Category::getId)
.max(Comparator.naturalOrder())
.orElse(0L) + 1;
Category category = new Category(nextId, request.name(), request.image());
categories.add(category);
URI location = URI.create("/api/v1/categories/" + nextId);
return ResponseEntity.created(location).body(category);
}
Now the request correctly fails and it also has the right HTTP status:

However, as you can see, the message we passed to @NotBlank(message = "Name is required") is not there in the error response.
A DTO can have multiple invalid fields, so Spring cannot always guess the response shape you want. You have to handle this yourself, which we’ll cover shortly.
When validation fails, Spring automatically returns HTTP status 400 Bad Request which is the correct status code for invalid input.
But the default response body is a generic Spring error object with a timestamp, status, and error type. Field-level error messages may be buried or missing, depending on configuration. That is not useful for clients trying to fix their request.
We need to fix this so the client sees exactly which fields failed and why.
This is the most important practical section of this chapter. You’ll intercept validation exceptions and return clean, client-friendly error messages.
@ExceptionHandler@ExceptionHandler is a method-level annotation that tells Spring:
When this specific exception type is thrown? Call this method instead of the default error handler.
It is the same concept as try/catch, but at the framework level.
@ExceptionHandler Methods@ExceptionHandler methods don’t replace try/catch blocks.