Backend Development 17 min read

Master Spring Boot Validation: 10 Essential Tips and Custom Techniques

This article presents ten practical techniques—including built‑in annotations, custom constraints, server‑side checks, internationalized messages, validation groups, cross‑field rules, exception handling, testing, and client‑side considerations—to help developers implement robust validation in Spring Boot applications.

macrozheng
macrozheng
macrozheng
Master Spring Boot Validation: 10 Essential Tips and Custom Techniques

Preface

Parameter validation is essential for stability and security; many backend developers skip it, risking system reliability. This article presents ten practical techniques for effective validation in Spring Boot applications.

1. Use Validation Annotations

Spring Boot provides built‑in validation annotations such as @NotNull, @NotEmpty, @NotBlank, @Min, @Max, @Pattern, @Email, etc., to quickly 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 limits.

@Pattern

: regular‑expression 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 (e.g.,

@UniqueTitle

) and its validator to encapsulate reusable validation logic.

<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>

Validator implementation:

<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 entity:

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

    @NotNull
    private String body;
}</code>

3. Server‑Side Validation

Use DTOs with validation annotations and the

@Validated

/

@Valid

annotations on controller methods to enforce constraints before processing requests.

<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 the

message

attribute of each constraint to give clear feedback when validation fails.

<code>public class User {
    @NotBlank(message = "Username cannot be empty")
    private String name;

    @NotBlank(message = "Email cannot be empty")
    @Email(message = "Invalid email address")
    private String email;

    @NotNull(message = "Age cannot be null")
    @Min(value = 18, message = "Age must be greater than 18")
    @Max(value = 99, message = "Age must be less than 99")
    private Integer age;
}</code>

5. Internationalize Error Messages

Place default messages in

messages.properties

and locale‑specific files (e.g.,

messages_zh_CN.properties

), then configure a

MessageSource

bean and a

LocalValidatorFactoryBean

to use them.

6. Use Validation Groups

Define marker interfaces (e.g.,

EmailNotEmpty

,

Default

) and assign constraints to groups to apply different validation 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>

Controller example:

<code>@RestController
@RequestMapping("/users")
@Validated
public class UserController {
    public ResponseEntity<String> createUser(
        @Validated({User.EmailNotEmpty.class}) @RequestBody User userWithEmail,
        @Validated({User.Default.class}) @RequestBody User userWithoutEmail) {
        // create user
    }
}</code>

7. Cross‑Field Validation for Complex Logic

Define a class‑level annotation (e.g.,

@EndDateAfterStartDate

) and its validator to ensure that the end date is after the start date.

<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 EndDateAfterStartDateValidator implements ConstraintValidator<EndDateAfterStartDate, TaskForm> {
    @Override
    public boolean isValid(TaskForm form, ConstraintValidatorContext context) {
        if (form.getStartDate() == null || form.getEndDate() == null) {
            return true;
        }
        return form.getEndDate().isAfter(form.getStartDate());
    }
}</code>
<code>@EndDateAfterStartDate
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

for

MethodArgumentNotValidException

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 JUnit and the

Validator

API to verify that constraints behave as expected.

<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 not replace server‑side validation, as they can be bypassed.

Conclusion

Effective validation is crucial for web application stability and security. Spring Boot offers a comprehensive set of tools to simplify validation, and following the practices outlined above helps build robust, maintainable services.

JavaBackend DevelopmentJSR-303ValidationSpring Bootcustom annotations
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.