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.
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
@Emailto 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
@UniqueTitleannotation 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
@Validatedand
@Validin 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
messageattribute 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.
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.
How this landed with the community
Was this worth your time?
0 Comments
Thoughtful readers leave field notes, pushback, and hard-won operational detail here.