Validation & Exception Handling

Bean Validation (Jakarta Validation)

18 min Lesson 2 of 13

Bean Validation (Jakarta Validation)

Before a request ever reaches your business logic, you need confidence that the data it carries is structurally sound. Jakarta Bean Validation (formerly javax.validation, now jakarta.validation) is the standard Java specification that lets you express constraints directly on your model classes using annotations. Spring Boot 3 integrates it out of the box through Hibernate Validator, the reference implementation.

The Dependency You Need

Add the Spring Boot starter — it pulls in Hibernate Validator and the Jakarta Validation API automatically:

<!-- Maven (pom.xml) --> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-validation</artifactId> </dependency>

Without this starter the annotations compile fine but constraints are silently ignored at runtime — a common source of confusion for developers migrating from older Spring projects.

Do not rely on transitive inclusion. spring-boot-starter-web no longer bundles the validation starter since Spring Boot 2.3. Declare it explicitly in every module that validates input.

How Bean Validation Works

The specification defines a Validator that inspects an object's fields, properties, and constructor parameters against a set of declared constraints. Each constraint is an annotation backed by a ConstraintValidator implementation that contains the actual check logic. When Spring processes a @Valid or @Validated annotation on a method parameter, it invokes the validator before the method body executes. If any constraint fails, a MethodArgumentNotValidException is thrown.

Specification vs implementation: The API lives in jakarta.validation:jakarta.validation-api. Hibernate Validator is the implementation that provides the actual constraint validators, plus many extras beyond the spec. You code against the API; the implementation is swappable.

The Standard Constraint Annotations

The Jakarta Validation specification ships around 25 built-in constraints. The ones you will reach for most often are:

  • @NotNull — the field must not be null. Passes for empty strings and blank strings.
  • @NotEmpty — must not be null and must have at least one character (or one element, for collections).
  • @NotBlank — must not be null, not empty, and must contain at least one non-whitespace character. The right choice for user-supplied text fields.
  • @Size(min, max) — constrains the length (String, array, Collection, Map) or count between min and max. Both bounds are inclusive.
  • @Min(value) / @Max(value) — numeric minimum and maximum (works on int, long, BigDecimal, etc.).
  • @Email — the string must match a valid email address format according to the RFC standard.
  • @Pattern(regexp) — the string must match the given regular expression.
  • @Positive / @PositiveOrZero / @Negative — sign constraints for numeric types.
  • @Past / @Future — temporal constraints for LocalDate, Instant, and related types.
  • @Digits(integer, fraction) — limits the number of integer and fractional digits.

Annotating a DTO

The idiomatic pattern in Spring Boot is to annotate a plain Java class (usually called a request DTO or command object) that represents incoming data. Here is a realistic user-registration request:

package com.example.api.dto; import jakarta.validation.constraints.*; public class RegisterRequest { @NotBlank(message = "Username is required") @Size(min = 3, max = 50, message = "Username must be between 3 and 50 characters") private String username; @NotBlank(message = "Email is required") @Email(message = "Must be a valid email address") @Size(max = 255) private String email; @NotBlank(message = "Password is required") @Size(min = 8, message = "Password must be at least 8 characters") private String password; @NotNull(message = "Age is required") @Min(value = 18, message = "Must be at least 18 years old") @Max(value = 120, message = "Age does not look realistic") private Integer age; // getters and setters omitted for brevity }

Notice several patterns here worth internalizing:

  • Use @NotBlank rather than @NotNull for String fields where an empty or blank string should also be rejected.
  • Always supply a message attribute that speaks to the end-user or API consumer, not to the developer.
  • Stack multiple annotations on the same field — they all run, and all failures are reported in one pass.
  • For primitive wrapper types (Integer, Long) use @NotNull separately from @Min/@Max because those numeric constraints do not imply non-null.

@NotNull vs @NotEmpty vs @NotBlank

These three are the source of most beginner mistakes. A side-by-side comparison:

  • @NotNull: passes for "", passes for " ", fails only for null.
  • @NotEmpty: fails for null and "", passes for " ".
  • @NotBlank: fails for null, "", and " " — the strictest of the three for strings.
Rule of thumb for text fields: Use @NotBlank on user-facing strings (name, username, title). Reserve @NotNull for non-string fields (numbers, booleans, nested objects) where you simply need to assert presence. @NotEmpty is useful for collections and arrays.

The @Email Constraint in Practice

The spec's @Email annotation validates the structural format of an email address. By default Hibernate Validator's check is fairly lenient — it accepts some technically valid but unusual formats. For stricter validation matching RFC 5322, pass the regexp attribute or use Hibernate's own @Email with regexp:

// Standard Jakarta — lenient but spec-compliant @Email private String email; // Stricter: must contain a dot in the domain part @Email(regexp = "^[\\w._%+\\-]+@[\\w.\\-]+\\.[a-zA-Z]{2,}$", message = "Email address is not valid") private String email;

In most APIs the standard @Email is sufficient. Reserve the regexp override for domains where you control the address format tightly (e.g., corporate internal tools).

@Size on Collections and Arrays

@Size is not limited to strings. It applies to any type with a measurable size:

import jakarta.validation.constraints.Size; import java.util.List; public class CreateTagsRequest { @Size(min = 1, max = 10, message = "Provide between 1 and 10 tags") private List<String> tags; }

When the list is null, @Size passes (null is considered valid by most constraints unless combined with @NotNull). Stack them when both presence and size matter.

Customising Default Messages with a Message Interpolator

Every constraint has a default message template stored in ValidationMessages.properties on the classpath. You can override them globally by placing your own ValidationMessages.properties at src/main/resources/ValidationMessages.properties:

# src/main/resources/ValidationMessages.properties jakarta.validation.constraints.NotBlank.message=This field is required and cannot be blank. jakarta.validation.constraints.Email.message=Please provide a valid email address. jakarta.validation.constraints.Size.message=Length must be between {min} and {max}.

The {min} and {max} placeholders are resolved from the annotation's attributes automatically.

Summary

Jakarta Bean Validation gives you a declarative, annotation-driven way to express data constraints directly on your model classes. The core constraints — @NotNull, @NotBlank, @NotEmpty, @Size, @Email, @Min, @Max, and their companions — cover the vast majority of real-world validation needs. Stack them freely, supply meaningful message values, and place them on request DTOs rather than domain entities to keep your validation layer decoupled from your persistence model. The next lesson shows how Spring Boot activates all of this automatically when you add @Valid to a controller method parameter.