Validation in Spring Boot using Jakarta Bean Validation

Data validation is a key part of building robust APIs. Spring Boot integrates with Jakarta Bean Validation (formerly JSR 380, previously known as Hibernate Validator) to provide a powerful and declarative validation mechanism.

This post will walk you through how to use @Valid along with validation annotations to ensure your incoming data meets the expected constraints.

Validation in Spring Boot using Jakarta Bean Validation

โœ… Basic Setup

Step 1: Add Spring Web and Validation Dependencies

If you’re using Maven, just add spring-boot-starter-web. It already includes validation dependencies.



    org.springframework.boot
    spring-boot-starter-web


๐Ÿงฑ Example: Validating a User Registration Request

๐Ÿ“ Model: User.java


package com.kscodes.springboot.validationdemo.model;

import jakarta.validation.constraints.Email;
import jakarta.validation.constraints.NotBlank;
import jakarta.validation.constraints.NotNull;
import jakarta.validation.constraints.Size;

public class User {

    @NotBlank(message = "Name is mandatory")
    @Size(min = 2, max = 30, message = "Name must be between 2 to 30 characters")
    private String name;

    @NotNull(message = "Age is required")
    private Integer age;

    @Email(message = "Email should be valid")
    private String email;

    // Getters and Setters
}

๐Ÿ“‚ Controller: UserController.java


package com.kscodes.springboot.validationdemo.controller;

import com.kscodes.springboot.validationdemo.model.User;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.*;

import jakarta.validation.Valid;

@RestController
@RequestMapping("/api/users")
public class UserController {

    @PostMapping
    public ResponseEntity createUser(@Valid @RequestBody User user) {
        // If validation passes, this line executes
        return ResponseEntity.ok("User created: " + user.getName());
    }
}

๐Ÿ”ฅ What happens if the input is invalid?

Spring will automatically return a 400 Bad Request with a descriptive error message like:


{
  "timestamp": "2025-06-28T12:34:56.789+00:00",
  "status": 400,
  "errors": [
    "Name is mandatory",
    "Email should be valid"
  ]
}

๐Ÿ“ค Sample JSON Input

โœ… Valid Input


{
  "name": "John Doe",
  "age": 30,
  "email": "john@example.com"
}

โŒ Invalid Input


{
  "name": "",
  "age": null,
  "email": "not-an-email"
}

๐Ÿงฐ Common Jakarta Validation Annotations

AnnotationDescription
@NotNullField must not be null
@NotBlankNot null and trimmed length > 0
@Size(min, max)Checks size of string, list, etc.
@Min, @MaxChecks numerical values
@EmailValidates email format
@Pattern(regexp = "")Validates against regex
@Positive, @NegativeChecks for positive/negative numbers

๐ŸŽฏ Custom Error Handling with @ControllerAdvice

๐Ÿ“ Create GlobalExceptionHandler.java


package com.kscodes.springboot.validationdemo.exception;

import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.MethodArgumentNotValidException;
import org.springframework.web.bind.annotation.ExceptionHandler;
import org.springframework.web.bind.annotation.ControllerAdvice;

import java.util.HashMap;
import java.util.Map;

@ControllerAdvice
public class GlobalExceptionHandler {

    @ExceptionHandler(MethodArgumentNotValidException.class)
    public ResponseEntity> handleValidationErrors(MethodArgumentNotValidException ex) {
        Map errors = new HashMap<>();

        ex.getBindingResult().getFieldErrors().forEach(error ->
            errors.put(error.getField(), error.getDefaultMessage())
        );

        return new ResponseEntity<>(errors, HttpStatus.BAD_REQUEST);
    }
}

๐Ÿ” Sample Error Response


{
  "name": "Name is mandatory",
  "email": "Email should be valid"
}

๐Ÿš€ Validating Nested Objects


public class User {
    @Valid
    private Address address;
}

public class Address {
    @NotBlank
    private String city;
}

Use @Valid on nested fields as well to trigger recursive validation.

โœ๏ธ Custom Validation Annotation (Advanced)

Step 1: Define Annotation


@Constraint(validatedBy = UsernameValidator.class)
@Target({ElementType.FIELD})
@Retention(RetentionPolicy.RUNTIME)
public @interface ValidUsername {
    String message() default "Invalid username";
    Class>?>[] groups() default {};
    Class[] payload() default {};
}

Step 2: Implement the Validator


public class UsernameValidator implements ConstraintValidator {

    public void initialize(ValidUsername constraint) {}

    public boolean isValid(String username, ConstraintValidatorContext context) {
        return username != null && username.matches("[a-zA-Z0-9_]+");
    }
}

Use it in your model:


@ValidUsername
private String username;

๐Ÿงช Testing with curl


curl -X POST http://localhost:8080/api/users \
  -H "Content-Type: application/json" \
  -d '{"name": "", "age": null, "email": "bad-email"}'

Expected response:


{
  "name": "Name is mandatory",
  "age": "Age is required",
  "email": "Email should be valid"
}

๐Ÿงพ External References