Backend Development 17 min read

10 Essential Spring Boot Parameter Validation Techniques You Must Know

This guide explains ten practical ways to enforce robust parameter validation in Spring Boot applications, covering built‑in annotations, custom constraints, server‑side checks, meaningful error messages, internationalization, validation groups, cross‑field rules, exception handling, testing strategies, and client‑side considerations.

macrozheng
macrozheng
macrozheng
10 Essential Spring Boot Parameter Validation Techniques You Must Know

Introduction

Parameter validation is essential for the stability and security of Spring Boot applications. The following ten techniques help you implement reliable validation.

1. Use Built‑in Validation Annotations

Spring Boot provides a set of JSR‑303 annotations such as

@NotNull

,

@NotEmpty

,

@NotBlank

,

@Min

,

@Max

,

@Pattern

, and

@Email

to quickly validate fields.

<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 constraints are insufficient, define a custom annotation and validator. Example: a

@UniqueTitle

annotation that checks title uniqueness in the database.

<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>
<code>public interface PostRepository extends JpaRepository<Post, Long> {
    Post findByTitle(String title);
}
</code>
<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>
<code>public class Post {
    @UniqueTitle
    private String title;

    @NotNull
    private String body;
}
</code>

3. Perform Server‑Side Validation

Apply validation annotations to DTOs 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.

<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 Validation Messages (i18n)

<code># messages.properties
user.name.required=Name is required.
user.email.invalid=Invalid email format.
user.age.invalid=Age must be a number between 18 and 99.
</code>
<code># messages_zh_CN.properties
user.name.required=名称不能为空.
user.email.invalid=无效的email格式.
user.age.invalid=年龄必须在18到99岁之间.
</code>
<code>public class User {
    @NotNull(message = "{user.id.required}")
    private Long id;

    @NotBlank(message = "{user.name.required}")
    private String name;

    @Email(message = "{user.email.invalid}")
    private String email;

    @NotNull(message = "{user.age.required}")
    @Min(value = 18, message = "{user.age.invalid}")
    @Max(value = 99, message = "{user.age.invalid}")
    private Integer age;
}
</code>
<code>@Configuration
public class AppConfig {
    @Bean
    public MessageSource messageSource() {
        ResourceBundleMessageSource messageSource = new ResourceBundleMessageSource();
        messageSource.setBasename("messages");
        messageSource.setDefaultEncoding("UTF-8");
        return messageSource;
    }

    @Bean
    public LocalValidatorFactoryBean validator() {
        LocalValidatorFactoryBean validatorFactoryBean = new LocalValidatorFactoryBean();
        validatorFactoryBean.setValidationMessageSource(messageSource());
        return validatorFactoryBean;
    }
}
</code>

6. Use Validation Groups

<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>
<code>@RestController
@RequestMapping("/users")
@Validated
public class UserController {
    public ResponseEntity<String> createUser(
            @Validated({org.example.model.ex6.User.EmailNotEmpty.class}) @RequestBody User userWithEmail,
            @Validated({User.Default.class}) @RequestBody User userWithoutEmail) {
        // create user logic
    }
}
</code>

7. Cross‑Field Validation

<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 taskForm, ConstraintValidatorContext context) {
        if (taskForm.getStartDate() == null || taskForm.getEndDate() == null) {
            return true;
        }
        return taskForm.getEndDate().isAfter(taskForm.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. Centralized Exception Handling for Validation Errors

<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 Your Validation Logic

<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 never replace server‑side validation because they can be bypassed.

BackendJavaJSR-303Spring BootParameter Validation
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.