Validation in Spring

Spring integrates Bean Validation (JSR 303/380) via annotations such as @NotNull, @Size, @Email. Validation is triggered by @Valid or @Validated on method parameters. This article explains usage, common annotations, validation groups, custom validators, and exception handling with a reference table.

Overview

  • Dependency: spring-boot-starter-validation (includes Hibernate Validator).
  • Usage: Add validation annotations to DTO fields; add @Valid or @Validated to Controller (or Service) parameters. On failure, MethodArgumentNotValidException is thrown; handle globally and return 400 with field errors.
  • Groups: @Validated(Group.class) validates by group; different scenarios (create vs update) can use different groups.
  • Custom: @Constraint for custom annotations; implement ConstraintValidator for the validation logic.

Example

Example 1: Common annotations

Java
public class CreateUserRequest {
    @NotNull
    @Size(min = 2, max = 50)
    private String name;

    @Email
    private String email;

    @Min(0) @Max(150)
    private Integer age;

    @Pattern(regexp = "^[A-Z]{2}[0-9]{2}$")
    private String countryCode;
}
  • Annotations declare constraints; the validator runs when @Valid or @Validated is present.

Example 2: Controller trigger

Java
@PostMapping("/users")
public User create(@RequestBody @Valid CreateUserRequest req) {
    return userService.create(req);
}
  • @Valid on @RequestBody triggers validation before the method runs. If validation fails, the method is not invoked.

Example 3: Method parameter validation

Java
@Validated
@Service
public class UserService {
    public void update(@NotNull @Min(1) Long id, @Valid UpdateRequest req) {
        // ...
    }
}
  • @Validated on the class enables method parameter validation (e.g. @NotNull, @Min on primitive/wrapper params). Without @Validated, only @Valid on object parameters works.

Example 4: Validation groups

Java
public interface Create {}
public interface Update {}

public class UserRequest {
    @NotNull(groups = Update.class)  // Only for update
    private Long id;
    @NotBlank(groups = {Create.class, Update.class})
    private String name;
}

@PostMapping("/users")
public User create(@RequestBody @Validated(Create.class) UserRequest req) { }

@PutMapping("/users/{id}")
public User update(@RequestBody @Validated(Update.class) UserRequest req) { }
  • Groups let you validate different subsets of constraints for different endpoints or scenarios.

Example 5: Custom validator

Java
@Constraint(validatedBy = UniqueEmailValidator.class)
@Target(ElementType.FIELD)
@Retention(RetentionPolicy.RUNTIME)
public @interface UniqueEmail {
    String message() default "Email already exists";
    Class<?>[] groups() default {};
    Class<? extends Payload>[] payload() default {};
}

public class UniqueEmailValidator implements ConstraintValidator<UniqueEmail, String> {
    @Autowired
    private UserRepository userRepo;

    @Override
    public boolean isValid(String email, ConstraintValidatorContext ctx) {
        return email == null || !userRepo.existsByEmail(email);
    }
}
  • For uniqueness and other DB-dependent checks, custom validators are needed. Format and range checks use built-in annotations.

Example 6: Global exception handler

Java
@RestControllerAdvice
public class ValidationHandler {
    @ExceptionHandler(MethodArgumentNotValidException.class)
    public ResponseEntity<Map<String, Object>> handle(MethodArgumentNotValidException ex) {
        Map<String, String> errors = new HashMap<>();
        ex.getBindingResult().getFieldErrors().forEach(e ->
            errors.put(e.getField(), e.getDefaultMessage()));
        return ResponseEntity.badRequest().body(Map.of("errors", errors));
    }
}
  • Return 400 with field-level errors so the client can display them. Include field name and message for each violation.

Example 7: Common annotations

AnnotationDescription
@NotNullNot null
@NotBlankNon-null, non-empty string (trimmed)
@NotEmptyNot null, not empty (string, collection, array)
@SizeLength or size in range
@EmailEmail format
@PatternRegex match
@Min / @MaxNumeric range
@Positive / @PositiveOrZeroNumber constraints
@Future / @PastDate constraints

Core Mechanism / Behavior

  • @Valid: Triggers validation and recursive validation of nested objects (e.g. nested DTOs).
  • @Validated: Enables method parameter validation and supports groups. Must be on the class for method validation.
  • Exception: MethodArgumentNotValidException holds BindingResult with field and global errors. Extract and return in the response.
  • Order: Validation runs before the method; if it fails, the method is not called.

Validation vs Authorization

  • Validation: Checks format, range, required fields. "Is this a valid email? Is age in range?" Use Bean Validation.
  • Authorization: Checks permissions. "Can this user access this resource?" Use Spring Security, custom checks in Service.
  • Do not use validation annotations for authorization; keep them separate. Validation runs early; authorization may depend on loaded entities and context.

Common Pitfalls

  • Forgetting @Validated on class: Method parameter validation (e.g. @NotNull on Long id) requires @Validated on the class.
  • Nested validation: Nested objects need @Valid; without it, nested constraints are not checked.
  • Custom validator with DI: ConstraintValidator can receive injected dependencies; ensure the validator is managed by Spring (e.g. use ConstraintValidatorFactory).

Key Rules

  • @Valid for recursive validation; @Validated for method params and groups.
  • Global exception handler for validation failures; return 400 with structured field errors.
  • Format and required fields: Use annotations. Uniqueness and business rules: Use custom validators or Service layer + DB.
  • Do not rely on validation alone for security; validate for correctness and UX; enforce authorization and integrity in the service layer.

What's Next

See Parameter Binding for argument handling. See API Gateway for upfront validation at the edge.