Bean Validation

Reviewed & published by Brayan K

Bean Validation lets you declare input rules with annotations — @NotNull, @Size, @Email — and have Spring enforce them automatically with @Valid, surfacing problems through a BindingResult or a 400 response.

Part of the free Java course at LearnCodingFast — hands-on lessons with worked examples and the output they print, plus practice exercises and a quick quiz.

Before You Start

You should know how a Spring REST controller binds a @RequestBody. These examples use the jakarta.validation constraints.

What You'll Learn

The Big Idea — A Bouncer at the Door

💡 Analogy: Validation annotations are the guest list rules posted at the door, and @Valid is the bouncer who actually checks them. The rules — "name required, 2-30 characters," "must be 18+" — live on the request object. When a request arrives, the bouncer reads each rule; if anything fails, the visitor is turned away (a 400) or you get a clipboard of every problem (the BindingResult) to handle yourself. Bad data never reaches the party.

Declare the rules once; let the framework enforce them everywhere.

1️⃣ Declaring Constraints

Annotate the fields of a request object. @NotBlank for required text, @Size for length, @Email for format, @Min/@Max for ranges. Each takes an optional message.

import jakarta.validation.constraints.*;

// Constraints declared right on the fields of a request object.
public class CreateUserRequest {

    @NotBlank(message = "name is required")
    @Size(min = 2, max = 30, message = "name must be 2-30 characters")
    private String name;

    @NotNull
    @Email(message = "must be a valid email address")
    private String email;

    @Min(value = 18, message = "must be at least 18")
    @Max(value = 120, message = "age looks invalid")
    private int age;

    // getters/setters omitted for brevity
    public String getName()  { return name; }
    public String getEmail() { return email; }
    public int getAge()      { return age; }
}

2️⃣ Enforcing with @Valid

Put @Valid on the @RequestBody parameter. With no BindingResult, a failure becomes a 400 Bad Request automatically; add a BindingResult right after it to inspect errors yourself.

import jakarta.validation.Valid;
import org.springframework.http.*;
import org.springframework.validation.BindingResult;
import org.springframework.web.bind.annotation.*;

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

    // Option A: let @Valid raise a 400 automatically on failure.
    @PostMapping
    public ResponseEntity<String> create(@Valid @RequestBody CreateUserRequest req) {
        // Reached only when the request is valid.
        return ResponseEntity.ok("created " + req.getName());
    }

    // Option B: add a BindingResult to inspect errors yourself.
    @PostMapping("/manual")
    public ResponseEntity<?> createManual(@Valid @RequestBody CreateUserRequest req,
                                          BindingResult result) {
        if (result.hasErrors()) {
            String first = result.getFieldErrors().get(0).getDefaultMessage();
            return ResponseEntity.badRequest().body("invalid: " + first);
        }
        return ResponseEntity.ok("created " + req.getName());
    }
}

3️⃣ What the Constraints Actually Do

Under the hood, a validator runs rules like the ones below and collects every failure. Here is the same logic in plain Java so you can run it.

import java.util.*;

// PLAIN-JAVA illustration of what @NotBlank / @Size / @Min DO.
// Bean Validation runs rules like these and collects the failures.
public class Main {

    record CreateUser(String name, String email, int age) { }

    static List<String> validate(CreateUser u) {
        List<String> errors = new ArrayList<>();
        // @NotBlank
        if (u.name() == null || u.name().trim().isEmpty())
            errors.add("name is required");
        // @Size(min=2, max=30)
        else if (u.name().length() < 2 || u.name().length() > 30)
            errors.add("name must be 2-30 characters");
        // @Email (very simplified)
        if (u.email() == null || !u.email().contains("@"))
            errors.add("must be a valid email address");
        // @Min(18)
        if (u.age() < 18) errors.add("must be at least 18");
        return errors;
    }

    public static void main(String[] args) {
        CreateUser bad = new CreateUser(" ", "not-an-email", 16);
        List<String> errors = validate(bad);
        System.out.println("valid? " + errors.isEmpty());
        errors.forEach(e -> System.out.println(" - " + e));

        CreateUser good = new CreateUser("Ana", "ana@site.com", 31);
        System.out.println("valid? " + validate(good).isEmpty());
    }
}

🧠 Quick Recall

Predict before you peek.

Answer: it passes @NotNull (it isn't null) but fails @NotBlank (no non-whitespace characters).

Answer: 400 Bad Request, via MethodArgumentNotValidException.

Answer: immediately after the @Valid parameter it reports on.

🎯 YOUR TURN — Validate a ProductRequest

Annotate a ProductRequest with constraints for name, sku, price, and an optional description.

import jakarta.validation.constraints.*;

// YOUR TURN: annotate a ProductRequest.
//  - name:  not blank, 1-100 characters
//  - sku:   not null, exactly... use @Size(min=8, max=8)
//  - priceCents: @Min(1) (must cost something)
//  - description: optional, but if present @Size(max=500)
//
// public class ProductRequest {
//     @NotBlank @Size(min = 1, max = 100) private String name;
//     ...
// }

🧩 MINI-CHALLENGE — Return all field errors

Build a controller that collects every validation error into a Map<String,String> and returns it as a 400.

import jakarta.validation.Valid;
import org.springframework.http.*;
import org.springframework.validation.BindingResult;
import org.springframework.web.bind.annotation.*;
import java.util.*;

// MINI-CHALLENGE: a controller that returns ALL field errors as a map.
//  - @PostMapping that takes @Valid @RequestBody ProductRequest + BindingResult
//  - if result.hasErrors(): build Map<String,String> of field -> message
//    from result.getFieldErrors() and return 400 with that body
//  - otherwise return 200 with the created product
//
// Think: why collect every error instead of just the first?

Common Errors

Pro Tips

📋 Quick Reference

AnnotationChecksNote
@NotNullnot nullEmpty string OK
@NotEmptynot null + non-emptyString/collection
@NotBlanknon-whitespace textStrings only
@Size(min,max)length/size rangeString/collection
@Emailemail formatFormat only
@Min / @Maxnumeric boundsInclusive
@Validtriggers validationOn the parameter
BindingResultholds errorsAfter @Valid arg
message = "..."custom error textPer constraint
default failure400 Bad RequestNo BindingResult

📚 Resources / Keep Learning

🎉 Lesson Complete!

You can now declare input rules with jakarta.validation constraints, choose between @NotNull, @NotEmpty, and @NotBlank, enforce them with @Valid, and inspect failures through a BindingResult or a default 400.

Practice quiz

Which package do the standard Bean Validation annotations live in (modern Spring Boot)?

  • javax.validation
  • jakarta.validation.constraints
  • org.springframework.validation
  • java.validation

Answer: jakarta.validation.constraints. Modern Spring Boot uses Jakarta Bean Validation; constraints live in jakarta.validation.constraints.

What does @NotNull check?

  • The value is not null
  • The String is not blank
  • The collection is not empty
  • The number is positive

Answer: The value is not null. @NotNull simply requires the annotated value to be non-null (it allows an empty string).

Which annotation rejects null, empty, AND whitespace-only strings?

  • @NotNull
  • @NotEmpty
  • @NotBlank
  • @Size(min=1)

Answer: @NotBlank. @NotBlank requires a non-null string with at least one non-whitespace character.

What does @Size(min = 2, max = 30) constrain?

  • Only numbers
  • The byte size of a file
  • The number of fields
  • The length/size of a string, collection, or array

Answer: The length/size of a string, collection, or array. @Size constrains the length of a String or the size of a collection/array/map to the given range.

What triggers validation of a @RequestBody in a controller?

  • @Validated on the class only
  • Annotating the parameter with @Valid
  • It is always automatic
  • A try/catch block

Answer: Annotating the parameter with @Valid. Placing @Valid (or @Validated) on the method parameter tells Spring to validate the bound object.

Where do you inspect validation errors when you add a BindingResult parameter?

  • In the response headers
  • In the BindingResult immediately after the @Valid parameter
  • In a static field
  • In application.properties

Answer: In the BindingResult immediately after the @Valid parameter. A BindingResult declared right after the validated parameter holds the field errors for manual handling.

What does @Email validate?

  • The string is a syntactically valid email address
  • The address exists
  • The domain is reachable
  • The inbox is empty

Answer: The string is a syntactically valid email address. @Email only checks that the string matches an email-address format; it cannot confirm deliverability.

Which annotation requires a numeric value to be at least a minimum?

  • @Max
  • @Min
  • @Positive
  • @Size

Answer: @Min. @Min(value) requires the number to be greater than or equal to the given minimum.

By default, what HTTP status does Spring return when @Valid fails on a @RequestBody (no BindingResult)?

  • 200 OK
  • 500 Internal Server Error
  • 400 Bad Request
  • 404 Not Found

Answer: 400 Bad Request. A failed @Valid on a request body raises MethodArgumentNotValidException, mapped to 400 Bad Request by default.

What is the role of the message attribute on a constraint, e.g. @Size(message = "...")?

  • It changes the field name
  • It disables the constraint
  • It sets a default value
  • It customizes the validation error message

Answer: It customizes the validation error message. The message attribute customizes the error text reported when the constraint is violated.

Continue this course

Frequently asked questions

What is Bean Validation and where do the annotations come from?

Bean Validation (the Jakarta Validation specification) is a standard way to declare constraints on object fields using annotations like @NotNull, @Size, @Email, and @Min, then have a validator check them. In modern Spring Boot the annotations live in jakarta.validation.constraints, and the spring-boot-starter-validation dependency brings in an implementation (Hibernate Validator). You annotate the fields once and the framework enforces them, rather than scattering if-checks throughout your code.

What is the difference between @NotNull, @NotEmpty, and @NotBlank?

@NotNull only rejects a null value — an empty string "" passes. @NotEmpty rejects null and also empty: for a string it requires length greater than zero, and for a collection it requires at least one element, but a string of spaces " " still passes. @NotBlank applies only to strings and is the strictest: it rejects null, empty, and whitespace-only values, requiring at least one non-whitespace character. For required text fields, @NotBlank is usually what you want.

How does @Valid actually trigger validation in a controller?

When you annotate a controller method parameter — typically a @RequestBody — with @Valid (or Spring's @Validated), Spring runs the bean validator on the bound object after deserialization. If a constraint fails and there is no BindingResult parameter, Spring throws MethodArgumentNotValidException, which by default is translated into a 400 Bad Request response with the error details. Without @Valid, the annotations on the object are present but never checked for that request.

When should I use a BindingResult parameter?

Add a BindingResult immediately after the @Valid parameter when you want to handle validation failures yourself instead of letting Spring throw. With a BindingResult you can call hasErrors(), iterate getFieldErrors(), build a custom error response, log, or apply fallback logic. Without it, validation failures short-circuit into an exception and the default error handling. The order matters: the BindingResult must directly follow the object it reports on.

Can @Email guarantee the address is real?

No. @Email only checks that the string is syntactically a plausible email address — it matches the expected format. It cannot verify that the mailbox exists, that the domain is reachable, or that mail would be delivered. Confirming a real, working address requires sending a verification message and having the user confirm it. Treat @Email as a cheap first filter against obviously malformed input, not as proof of deliverability.

Related lessons