Spring Boot Cheatsheet

Validation

Use this Spring Boot reference while you build software engineering projects, review code for technical interview prep, or polish examples for a software engineer resume.

Setup

<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-validation</artifactId>
</dependency>

Adds Hibernate Validator (the Jakarta Bean Validation 3.x reference implementation).

Triggering Validation

// On @RequestBody — throws MethodArgumentNotValidException on failure
@PostMapping("/users")
public UserDto create(@RequestBody @Valid CreateUserRequest req) { ... }

// On @ModelAttribute (form data)
@PostMapping("/register")
public String register(@ModelAttribute @Valid RegistrationForm form,
                        BindingResult result) {
    if (result.hasErrors()) return "register";   // manual check needed with BindingResult
    ...
}

// On @RequestParam / @PathVariable — add @Validated to the controller class
@RestController
@Validated
public class SearchController {
    @GetMapping("/search")
    public List<Item> search(@RequestParam @NotBlank @Size(min=2) String q) { ... }
}

// On service-layer beans — add @Validated to the class
@Service
@Validated
public class OrderService {
    public void place(@Valid CreateOrderRequest req) { ... }
}

@Valid triggers standard cascaded validation. @Validated (Spring) adds group support and enables method-level validation on @Service/@Component.

Standard Constraint Annotations

Null / Presence

AnnotationValidates
@NotNullValue is not null
@NullValue must be null
@NotEmptyNot null AND not empty (String, Collection, Map, array)
@NotBlankNot null AND contains at least one non-whitespace character

String

AnnotationExample
@Size(min=2, max=50)Length between 2 and 50
@Pattern(regexp="[a-z]+")Matches regex
@EmailValid email address
@URLValid URL (Hibernate Validator extension)
@CreditCardNumberLuhn algorithm (HV extension)

Numbers

AnnotationExample
@Min(1)Value ≥ 1
@Max(100)Value ≤ 100
@DecimalMin("0.01")BigDecimal ≥ 0.01
@DecimalMax("999.99")BigDecimal ≤ 999.99
@PositiveValue > 0
@PositiveOrZeroValue ≥ 0
@NegativeValue < 0
@NegativeOrZeroValue ≤ 0
@Digits(integer=5, fraction=2)Up to 5 integer + 2 fraction digits

Dates / Times

AnnotationNotes
@PastBefore now
@PastOrPresentNow or before
@FutureAfter now
@FutureOrPresentNow or after

Boolean

AnnotationNotes
@AssertTrueMust be true
@AssertFalseMust be false

Applying Constraints

public record CreateUserRequest(
    @NotBlank(message = "Name is required")
    @Size(min = 2, max = 100)
    String name,

    @NotBlank
    @Email(message = "Must be a valid email address")
    String email,

    @NotBlank
    @Size(min = 8, message = "Password must be at least 8 characters")
    @Pattern(regexp = ".*[A-Z].*", message = "Password must contain an uppercase letter")
    String password,

    @NotNull
    @Min(18)
    Integer age,

    @NotNull
    @Future
    LocalDate birthDate,

    @Valid   // cascade into nested object
    @NotNull
    AddressRequest address
) {}

public record AddressRequest(
    @NotBlank String street,
    @NotBlank @Size(min=2, max=2) String stateCode,
    @NotBlank @Pattern(regexp="\\d{5}(-\\d{4})?") String zip
) {}

Handling Validation Errors

// Global handler in @ControllerAdvice
@ExceptionHandler(MethodArgumentNotValidException.class)
public ResponseEntity<Map<String, List<String>>> handleValidation(
        MethodArgumentNotValidException ex) {
    Map<String, List<String>> errors = ex.getBindingResult()
        .getFieldErrors()
        .stream()
        .collect(Collectors.groupingBy(
            FieldError::getField,
            Collectors.mapping(FieldError::getDefaultMessage, Collectors.toList())
        ));
    return ResponseEntity.badRequest().body(errors);
}

// For @RequestParam / @PathVariable violations
@ExceptionHandler(ConstraintViolationException.class)
public ResponseEntity<Map<String, String>> handleConstraint(
        ConstraintViolationException ex) {
    Map<String, String> errors = ex.getConstraintViolations().stream()
        .collect(Collectors.toMap(
            v -> v.getPropertyPath().toString(),
            v -> v.getMessage()
        ));
    return ResponseEntity.badRequest().body(errors);
}

Custom Constraint Annotation

// 1. Define the annotation
@Documented
@Constraint(validatedBy = UniqueEmailValidator.class)
@Target({ElementType.FIELD, ElementType.PARAMETER})
@Retention(RetentionPolicy.RUNTIME)
public @interface UniqueEmail {
    String message() default "Email already in use";
    Class<?>[] groups() default {};
    Class<? extends Payload>[] payload() default {};
}

// 2. Implement the validator
@Component
public class UniqueEmailValidator implements ConstraintValidator<UniqueEmail, String> {

    private final UserRepository userRepository;

    public UniqueEmailValidator(UserRepository userRepository) {
        this.userRepository = userRepository;
    }

    @Override
    public boolean isValid(String email, ConstraintValidatorContext ctx) {
        if (email == null) return true;   // let @NotBlank handle null
        return !userRepository.existsByEmail(email);
    }
}

// 3. Use it
public record CreateUserRequest(
    @UniqueEmail @Email @NotBlank String email
) {}

Class-Level (Cross-Field) Validation

// Annotation
@Documented
@Constraint(validatedBy = PasswordMatchValidator.class)
@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
public @interface PasswordMatch {
    String message() default "Passwords do not match";
    Class<?>[] groups() default {};
    Class<? extends Payload>[] payload() default {};
}

// Validator
public class PasswordMatchValidator
        implements ConstraintValidator<PasswordMatch, PasswordChangeRequest> {
    @Override
    public boolean isValid(PasswordChangeRequest req, ConstraintValidatorContext ctx) {
        return req.password() != null
            && req.password().equals(req.confirmPassword());
    }
}

// DTO
@PasswordMatch
public record PasswordChangeRequest(
    @NotBlank @Size(min=8) String password,
    @NotBlank String confirmPassword
) {}

Validation Groups

// Define marker interfaces
public interface OnCreate {}
public interface OnUpdate {}

// Apply per group
public class UserRequest {
    @Null(groups = OnUpdate.class)           // id must be null on create
    @NotNull(groups = OnCreate.class)        // id must be set on update
    private Long id;

    @NotBlank(groups = {OnCreate.class, OnUpdate.class})
    private String name;
}

// Trigger a specific group with @Validated
@PostMapping
public UserDto create(@RequestBody @Validated(OnCreate.class) UserRequest req) { ... }

@PutMapping("/{id}")
public UserDto update(@PathVariable Long id,
                      @RequestBody @Validated(OnUpdate.class) UserRequest req) { ... }

@ConfigurationProperties Validation

@ConfigurationProperties(prefix = "app.mail")
@Validated
public record MailProperties(
    @NotBlank String host,
    @Min(1) @Max(65535) int port,
    @Email String from
) {}

Spring Boot validates @ConfigurationProperties annotated with @Validated at startup and fails fast if constraints are violated.

Common Pitfalls

  • @Valid missing — forgetting @Valid before @RequestBody silently skips all constraints; the controller receives invalid data with no error.
  • @Validated vs @Valid@Valid (Jakarta) enables standard cascaded validation; @Validated (Spring) enables groups and method-level validation outside controllers.
  • Cascaded validation — nested objects require @Valid on the field/parameter AND the nested class must have its own constraints.
  • Constraint not applied to null — most constraints (@Size, @Pattern, @Email) skip null values by design; add @NotNull or @NotBlank explicitly.
  • Custom validator as Spring bean — only works if the validator is registered as a Spring bean (via @Component or @Bean); otherwise dependency injection inside the validator won't work.
  • BindingResult must immediately follow @Valid — if BindingResult is present, Spring does not throw; you must check result.hasErrors() yourself.