Mastering JSR‑303 Validation in Spring Boot: From Basics to Custom Constraints
This article walks through the fundamentals of JSR‑303 Bean Validation in Spring Boot, explains how to add the starter dependency, lists built‑in constraint annotations, demonstrates simple, group and nested validation, shows how to capture validation errors, and guides you through creating custom constraint annotations and validators with full code examples.
Introduction
The author continues a Spring Boot tutorial series and focuses on integrating JSR‑303 (Bean Validation) for request parameter validation, emphasizing why a deeper understanding of the validation process is essential for building robust APIs.
What is JSR‑303?
JSR‑303 is the Bean Validation specification introduced in Java EE 6. It defines a metadata model and API for validating JavaBeans. The default metadata is expressed through Java annotations (e.g., @NotNull, @Max), but XML can be used to override or extend these definitions. By applying built‑in or custom constraints to fields, getters, classes, or interfaces, developers can ensure data integrity at runtime.
Adding the starter
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-validation</artifactId>
</dependency>Built‑in constraint annotations
@Null– element must be null @NotNull – element must not be null @AssertTrue – element must be true @AssertFalse – element must be false @Min(value) – numeric value must be ≥ value @Max(value) – numeric value must be ≤ value @DecimalMin(value) – numeric value must be ≥ value (as string) @DecimalMax(value) – numeric value must be ≤ value (as string) @Size(min, max) – collection or string length must be within the range @Digits(integer, fraction) – numeric value must have the specified number of integer and fractional digits @Past – date must be in the past @Future – date must be in the future @Pattern(regexp) – string must match the regular expression
Hibernate Validator adds extra annotations such as @Email , @Length , @NotEmpty , and @Range .
How to use validation
Simple validation
Apply constraint annotations directly on fields of a DTO and annotate the controller method parameter with @Valid. Example:
@Data
public class ArticleDTO {
@NotNull(message = "文章id不能为空")
@Min(value = 1, message = "文章ID不能为负数")
private Integer id;
@NotBlank(message = "文章内容不能为空")
private String content;
@NotBlank(message = "作者Id不能为空")
private String authorId;
@Future(message = "提交时间不能为过去时间")
private Date submitTime;
}Multiple constraints can be placed on the same field; the message attribute supplies the error text returned when validation fails.
In the controller, the method must declare a BindingResult to receive the validation outcome:
@PostMapping("/add")
public String add(@Valid @RequestBody ArticleDTO articleDTO,
BindingResult bindingResult) throws JsonProcessingException {
if (bindingResult.hasErrors()) {
Map<String, String> map = new HashMap<>();
bindingResult.getFieldErrors().forEach(item -> {
map.put(item.getField(), item.getDefaultMessage());
});
return objectMapper.writeValueAsString(map);
}
return "success";
}Group validation
When the same DTO is used for different operations (e.g., add vs. update), groups allow selective activation of constraints. Define marker interfaces for each group and assign them via the groups attribute:
@Data
public class ArticleDTO {
@NotNull(message = "文章id不能为空", groups = UpdateArticleDTO.class)
@Min(value = 1, message = "文章ID不能为负数", groups = UpdateArticleDTO.class)
private Integer id;
@NotBlank(message = "文章内容不能为空", groups = {AddArticleDTO.class, UpdateArticleDTO.class})
private String content;
@NotBlank(message = "作者Id不能为空", groups = AddArticleDTO.class)
private String authorId;
@Future(message = "提交时间不能为过去时间", groups = {AddArticleDTO.class, UpdateArticleDTO.class})
private Date submitTime;
}
public interface UpdateArticleDTO {}
public interface AddArticleDTO {}JSR‑303’s @Valid does not support groups, but Spring’s @Validated does. Use @Validated(value = AddArticleDTO.class) on the controller method to trigger the appropriate group.
@PostMapping("/add")
public String add(@Validated(value = ArticleDTO.AddArticleDTO.class)
@RequestBody ArticleDTO articleDTO,
BindingResult bindingResult) throws JsonProcessingException { /* … */ }Nested validation
When a DTO contains another DTO (or a collection of DTOs), annotate the nested property with @Valid so that its constraints are evaluated as well.
@Data
public class CategoryDTO {
@NotNull(message = "分类ID不能为空")
@Min(value = 1, message = "分类ID不能为负数")
private Integer id;
@NotBlank(message = "分类名称不能为空")
private String name;
}
@Data
public class ArticleDTO {
@NotBlank(message = "文章内容不能为空")
private String content;
@Valid
@NotNull(message = "分类不能为空")
private CategoryDTO categoryDTO;
}For collections, place @Valid on the collection field; each element will be validated.
@Data
public class ArticleDTO {
@Valid
@Size(min = 1, message = "至少一个分类")
@NotNull(message = "分类不能为空")
private List<CategoryDTO> categoryDTOS;
}Receiving validation results
BindingResult
Inject BindingResult directly into the controller method. This approach requires handling the result in every method, which can become cumbersome.
An alternative is to use AOP to process BindingResult globally, but the author advises against it due to added complexity.
Global exception handling
When validation fails, Spring throws MethodArgumentNotValidException or BindException. A @RestControllerAdvice can catch these exceptions and return a unified error payload:
@RestControllerAdvice
public class ExceptionRsHandler {
@Autowired
private ObjectMapper objectMapper;
@ExceptionHandler({MethodArgumentNotValidException.class, BindException.class})
public String onException(Exception e) throws JsonProcessingException {
BindingResult bindingResult = null;
if (e instanceof MethodArgumentNotValidException) {
bindingResult = ((MethodArgumentNotValidException) e).getBindingResult();
} else if (e instanceof BindException) {
bindingResult = ((BindException) e).getBindingResult();
}
Map<String, String> errorMap = new HashMap<>(16);
bindingResult.getFieldErrors().forEach(fe ->
errorMap.put(fe.getField(), fe.getDefaultMessage()));
return objectMapper.writeValueAsString(errorMap);
}
}What spring-boot-starter-validation does
The starter’s auto‑configuration class ValidationAutoConfiguration registers a LocalValidatorFactoryBean as the default Validator implementation:
@Bean
@Role(BeanDefinition.ROLE_INFRASTRUCTURE)
@ConditionalOnMissingBean(Validator.class)
public static LocalValidatorFactoryBean defaultValidator() {
LocalValidatorFactoryBean factoryBean = new LocalValidatorFactoryBean();
MessageInterpolatorFactory interpolatorFactory = new MessageInterpolatorFactory();
factoryBean.setMessageInterpolator(interpolatorFactory.getObject());
return factoryBean;
}The Validator interface defines methods such as validate , validateProperty , and validateValue , which can be implemented to provide custom validation logic.
Creating custom constraints
Custom annotation
Define a new annotation and mark it with @Constraint(validatedBy = …). The annotation must declare message, groups, and payload as required by the Bean Validation spec, plus any custom attributes (e.g., int[] values() for an allowed set).
@Documented
@Constraint(validatedBy = { EnumValuesConstraintValidator.class })
@Target({ METHOD, FIELD, ANNOTATION_TYPE })
@Retention(RUNTIME)
@NotNull(message = "不能为空")
public @interface EnumValues {
String message() default "传入的值不在范围内";
Class<?>[] groups() default {};
Class<? extends Payload>[] payload() default {};
int[] values() default {};
}Custom validator
Implement ConstraintValidator<EnumValues, Integer>. In initialize store the allowed values; in isValid check membership.
public class EnumValuesConstraintValidator implements ConstraintValidator<EnumValues, Integer> {
private Set<Integer> ints = new HashSet<>();
@Override
public void initialize(EnumValues enumValues) {
for (int v : enumValues.values()) {
ints.add(v);
}
}
@Override
public boolean isValid(Integer value, ConstraintValidatorContext context) {
return ints.contains(value);
}
}If a constraint needs to validate other data types, create a corresponding validator and reference it in the @Constraint annotation.
Demo usage
Apply the custom annotation to a DTO field:
@Data
public class AuthorDTO {
@EnumValues(values = {1, 2}, message = "性别只能传入1或者2")
private Integer gender;
}Conclusion
JSR‑303 provides a powerful, declarative way to protect APIs from invalid input. By understanding the built‑in constraints, mastering group and nested validation, handling errors centrally, and extending the framework with custom annotations, developers can build clean, maintainable validation layers in Spring Boot applications.
Signed-in readers can open the original source through BestHub's protected redirect.
This article has been distilled and summarized from source material, then republished for learning and reference. If you believe it infringes your rights, please contactand we will review it promptly.
Code Ape Tech Column
Former Ant Group P8 engineer, pure technologist, sharing full‑stack Java, job interview and career advice through a column. Site: java-family.cn
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.
