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.

Code Ape Tech Column
Code Ape Tech Column
Code Ape Tech Column
Mastering JSR‑303 Validation in Spring Boot: From Basics to Custom Constraints

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.

Original Source

Signed-in readers can open the original source through BestHub's protected redirect.

Sign in to view source
Republication Notice

This article has been distilled and summarized from source material, then republished for learning and reference. If you believe it infringes your rights, please contactadmin@besthub.devand we will review it promptly.

Bean ValidationJSR-303Spring BootGroup ValidationCustom ConstraintNested Validation
Code Ape Tech Column
Written by

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

0 followers
Reader feedback

How this landed with the community

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.