# Validation and Exception Handling — @Valid, @RestControllerAdvice and ProblemDetail

Source: https://skills101.in/learn/spring-boot/spring-boot-validation-exception-handling — Skills101 free tutorial (Advanced Java & Spring Boot). Updated 2026-11-09.

Never trust input. A mobile app might send an empty title, a negative fee or a missing field. Your API must reject it with a clear **400 Bad Request** — and every error should look the same.

![A request is checked by @Valid; valid data reaches the controller, invalid data and exceptions thrown by the service go to the @RestControllerAdvice, which returns ProblemDetail JSON](https://skills101.in/learn-img/spring-boot/validation-flow.svg)

## 1. Bean Validation on the request DTO

```java
// File: course-api/src/main/java/com/skills101/courses/course/CourseRequest.java
package com.skills101.courses.course;

import jakarta.validation.constraints.Max;
import jakarta.validation.constraints.Min;
import jakarta.validation.constraints.NotBlank;
import jakarta.validation.constraints.Size;

// What a client sends to create or update a course
public record CourseRequest(
        @NotBlank @Size(max = 80) String title,
        @Min(0) @Max(100000) int fee,
        @Min(1) @Max(500) int seats) {
}
```

| Annotation | Rule |
|---|---|
| `@NotBlank` | not null and not just spaces (strings) |
| `@NotNull` | not null (any type) |
| `@Size(min, max)` | length of a string or collection |
| `@Min`, `@Max` | number range |
| `@Email`, `@Pattern(regexp=…)` | format |
| `@Past`, `@Future` | dates |

The rules only run when the controller parameter has **`@Valid`** (see `@Valid @RequestBody CourseRequest request` in the controller). The starter is `spring-boot-starter-validation`.

## 2. One place for all errors

Our service throws plain exceptions:

```java
// File: course-api/src/main/java/com/skills101/courses/course/CourseNotFoundException.java
package com.skills101.courses.course;

public class CourseNotFoundException extends RuntimeException {

    public CourseNotFoundException(Long id) {
        super("Course " + id + " not found");
    }
}
```

A **`@RestControllerAdvice`** class catches exceptions from every controller and turns them into responses:

```java
// File: course-api/src/main/java/com/skills101/courses/common/ApiExceptionHandler.java
package com.skills101.courses.common;

import com.skills101.courses.course.CourseNotFoundException;
import com.skills101.courses.enrollment.EnrollmentException;
import java.util.Map;
import java.util.TreeMap;
import org.springframework.http.HttpStatus;
import org.springframework.http.ProblemDetail;
import org.springframework.web.bind.MethodArgumentNotValidException;
import org.springframework.web.bind.annotation.ExceptionHandler;
import org.springframework.web.bind.annotation.RestControllerAdvice;

// One place that turns exceptions into clean JSON errors (RFC 9457 "problem details")
@RestControllerAdvice
public class ApiExceptionHandler {

    @ExceptionHandler(CourseNotFoundException.class)
    public ProblemDetail notFound(CourseNotFoundException e) {
        return ProblemDetail.forStatusAndDetail(HttpStatus.NOT_FOUND, e.getMessage());
    }

    @ExceptionHandler(EnrollmentException.class)
    public ProblemDetail conflict(EnrollmentException e) {
        return ProblemDetail.forStatusAndDetail(HttpStatus.CONFLICT, e.getMessage());
    }

    @ExceptionHandler(MethodArgumentNotValidException.class)
    public ProblemDetail invalid(MethodArgumentNotValidException e) {
        ProblemDetail problem = ProblemDetail.forStatusAndDetail(HttpStatus.BAD_REQUEST, "Some fields are invalid");
        Map<String, String> errors = new TreeMap<>();   // sorted, so the order is predictable
        e.getBindingResult().getFieldErrors().forEach(f -> errors.put(f.getField(), f.getDefaultMessage()));
        problem.setProperty("errors", errors);
        return problem;
    }
}
```

`ProblemDetail` is Spring's built-in support for **RFC 9457 "problem details"** — a standard JSON error shape with `title`, `status`, `detail` and `instance` (the path), plus any extra properties you add.

## 3. What clients see

Invalid course (`{"title":"","fee":-5,"seats":0}`) → **400**:

<!-- expect: course-api/validation-error.json -->
```json
{
  "title": "Bad Request",
  "status": 400,
  "detail": "Some fields are invalid",
  "instance": "/api/courses",
  "errors": {
    "fee": "must be greater than or equal to 0",
    "seats": "must be greater than or equal to 1",
    "title": "must not be blank"
  }
}
```

Unknown course (`GET /api/courses/99`) → **404**:

<!-- expect: course-api/course-not-found.json -->
```json
{
  "title": "Not Found",
  "status": 404,
  "detail": "Course 99 not found",
  "instance": "/api/courses/99"
}
```

The content type is `application/problem+json`. Field errors are sorted (we used a `TreeMap`) so the output is predictable.

> **Spring Boot 4 / Spring 7 note:** the default `"type": "about:blank"` is no longer written into the JSON. Many older tutorials still show it — our tests run on Boot 4.1, so the responses above are exactly what you'll get.

## 4. Which status for which error?

| Situation | Status |
|---|---|
| Malformed JSON or failed validation | 400 Bad Request |
| No / invalid login token | 401 Unauthorized |
| Logged in but not allowed | 403 Forbidden |
| Resource doesn't exist | 404 Not Found |
| Breaks a business rule (course full, duplicate) | 409 Conflict |
| Bug in your code | 500 — and log it; never show stack traces to clients |

## Common mistakes

- Forgetting `@Valid` — the annotations on the DTO are then ignored.
- `@NotNull` on a primitive `int` (it can never be null) — use `@Min` or an `Integer`.
- Catching `Exception` in every controller method instead of one advice class.
- Returning error messages that reveal SQL or stack traces.

## Quick recap

- Put constraints on request DTOs; add `@Valid` on the parameter.
- `@RestControllerAdvice` + `@ExceptionHandler` centralise error handling.
- `ProblemDetail` gives standard JSON errors (400, 404, 409…).
- Choose status codes that describe what went wrong.


Practice quiz and free certificate: https://skills101.in/learn/spring-boot/spring-boot-validation-exception-handling