Backend Development 17 min read

Master Spring Boot Validation: 10 Essential Tips & Custom Annotations

This article explains ten practical techniques for implementing robust parameter validation in Spring Boot applications, covering built‑in annotations, custom constraints, server‑side checks, internationalization, validation groups, cross‑field rules, exception handling, testing, and the importance of complementary client‑side validation.

macrozheng
macrozheng
macrozheng
Master Spring Boot Validation: 10 Essential Tips & Custom Annotations

Introduction

Parameter validation is crucial for stability and security in Spring Boot applications. This article presents ten practical techniques for effective validation.

1. Use Validation Annotations

Spring Boot provides built‑in validation annotations such as

@NotNull

,

@NotEmpty

,

@NotBlank

,

@Min

,

@Max

,

@Pattern

, and

@Email

to enforce constraints on fields.

@NotNull

: field must not be null.

@NotEmpty

: collection must not be empty.

@NotBlank

: string must contain non‑whitespace characters.

@Min

/

@Max

: numeric range.

@Pattern

: regex pattern.

@Email

: valid email address.

Example:

<code>public class User {
    @NotNull
    private Long id;

    @NotBlank
    @Size(min = 2, max = 50)
    private String firstName;

    @NotBlank
    @Size(min = 2, max = 50)
    private String lastName;

    @Email
    private String email;

    @NotNull
    @Min(18)
    @Max(99)
    private Integer age;

    @NotEmpty
    private List<String> hobbies;

    @Pattern(regexp = "[A-Z]{2}\\d{4}")
    private String employeeId;
}</code>

2. Create Custom Validation Annotations

When built‑in annotations are insufficient, define a custom constraint annotation and validator, e.g.,

@UniqueTitle

to ensure a post title is unique.

<code>@Target({ElementType.FIELD})
@Retention(RetentionPolicy.RUNTIME)
@Constraint(validatedBy = UniqueTitleValidator.class)
public @interface UniqueTitle {
    String message() default "Title must be unique";
    Class<?>[] groups() default {};
    Class<? extends Payload>[] payload() default {};
}</code>

Implement the validator:

<code>@Component
public class UniqueTitleValidator implements ConstraintValidator<UniqueTitle, String> {
    @Autowired
    private PostRepository postRepository;

    @Override
    public boolean isValid(String title, ConstraintValidatorContext context) {
        if (title == null) {
            return true;
        }
        return Objects.isNull(postRepository.findByTitle(title));
    }
}</code>

Apply to the entity:

<code>public class Post {
    @UniqueTitle
    private String title;

    @NotNull
    private String body;
}</code>

3. Server‑Side Validation

Use DTOs with validation annotations and enable method‑level validation with

@Validated

and

@Valid

in controllers.

<code>public class UserDTO {
    @NotBlank
    private String username;

    @NotBlank
    private String password;
}</code>
<code>@RestController
@RequestMapping("/users")
@Validated
public class UserController {
    @Autowired
    private UserService userService;

    @PostMapping
    public ResponseEntity<String> createUser(@Valid @RequestBody UserDTO userDto) {
        userService.createUser(userDto);
        return ResponseEntity.status(HttpStatus.CREATED).body("User created successfully");
    }
}</code>

4. Provide Meaningful Error Messages

Customize messages via the

message

attribute or externalized i18n files.

5. Internationalize Error Messages

Define

messages.properties

and locale‑specific files, then configure

MessageSource

and

LocalValidatorFactoryBean

to use them.

6. Group Validation

Define validation groups (e.g.,

EmailNotEmpty

and

Default

) to apply different rules based on context.

<code>public class User {
    @NotBlank(groups = Default.class)
    private String firstName;

    @NotBlank(groups = Default.class)
    private String lastName;

    @Email(groups = EmailNotEmpty.class)
    private String email;

    public interface EmailNotEmpty {}
    public interface Default {}
}</code>

Use

@Validated({EmailNotEmpty.class})

or

@Validated({Default.class})

on controller parameters.

7. Cross‑Field Validation

Create a class‑level annotation such as

@EndDateAfterStartDate

with a validator that checks

endDate.isAfter(startDate)

.

<code>@Target({ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
@Constraint(validatedBy = EndDateAfterStartDateValidator.class)
public @interface EndDateAfterStartDate {
    String message() default "End date must be after start date";
    Class<?>[] groups() default {};
    Class<? extends Payload>[] payload() default {};
}</code>
<code>public class TaskForm {
    @NotNull @DateTimeFormat(pattern = "yyyy-MM-dd")
    private LocalDate startDate;

    @NotNull @DateTimeFormat(pattern = "yyyy-MM-dd")
    private LocalDate endDate;
}</code>

8. Exception Handling for Validation Errors

Use

@RestControllerAdvice

with an

@ExceptionHandler(MethodArgumentNotValidException.class)

to return a structured error response.

<code>@RestControllerAdvice
public class RestExceptionHandler extends ResponseEntityExceptionHandler {
    @ExceptionHandler(MethodArgumentNotValidException.class)
    protected ResponseEntity<Object> handleMethodArgumentNotValid(
            MethodArgumentNotValidException ex,
            HttpHeaders headers,
            HttpStatus status,
            WebRequest request) {
        Map<String, Object> body = new LinkedHashMap<>();
        body.put("timestamp", LocalDateTime.now());
        body.put("status", status.value());
        List<String> errors = ex.getBindingResult()
                .getFieldErrors()
                .stream()
                .map(x -> x.getDefaultMessage())
                .collect(Collectors.toList());
        body.put("errors", errors);
        return new ResponseEntity<>(body, headers, status);
    }
}</code>

9. Test Validation Logic

Write unit tests with

Validator

to assert expected constraint violations.

<code>@DataJpaTest
public class UserValidationTest {
    @Autowired private TestEntityManager entityManager;
    @Autowired private Validator validator;

    @Test
    public void testValidation() {
        User user = new User();
        user.setFirstName("John");
        user.setLastName("Doe");
        user.setEmail("invalid email");
        Set<ConstraintViolation<User>> violations = validator.validate(user);
        assertEquals(1, violations.size());
        assertEquals("must be a well‑formed email address", violations.iterator().next().getMessage());
    }
}</code>

10. Consider Client‑Side Validation

Client‑side checks improve user experience but must be complemented by server‑side validation for security.

Conclusion

Effective validation is essential for stable and secure web applications. Spring Boot offers a comprehensive set of tools to simplify validation, and following the practices above helps maintain robust, user‑friendly services.

backendJavaValidationSpring BootAnnotations
macrozheng
Written by

macrozheng

Dedicated to Java tech sharing and dissecting top open-source projects. Topics include Spring Boot, Spring Cloud, Docker, Kubernetes and more. Author’s GitHub project “mall” has 50K+ stars.

0 followers
Reader feedback

How this landed with the community

login Sign in to like

Rate this article

Was this worth your time?

Sign in to rate
Discussion

0 Comments

Thoughtful readers leave field notes, pushback, and hard-won operational detail here.