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) { ... } }
@Validtriggers standard cascaded validation.@Validated(Spring) adds group support and enables method-level validation on@Service/@Component.
Standard Constraint Annotations
Null / Presence
| Annotation | Validates |
|---|---|
@NotNull | Value is not null |
@Null | Value must be null |
@NotEmpty | Not null AND not empty (String, Collection, Map, array) |
@NotBlank | Not null AND contains at least one non-whitespace character |
String
| Annotation | Example |
|---|---|
@Size(min=2, max=50) | Length between 2 and 50 |
@Pattern(regexp="[a-z]+") | Matches regex |
@Email | Valid email address |
@URL | Valid URL (Hibernate Validator extension) |
@CreditCardNumber | Luhn algorithm (HV extension) |
Numbers
| Annotation | Example |
|---|---|
@Min(1) | Value ≥ 1 |
@Max(100) | Value ≤ 100 |
@DecimalMin("0.01") | BigDecimal ≥ 0.01 |
@DecimalMax("999.99") | BigDecimal ≤ 999.99 |
@Positive | Value > 0 |
@PositiveOrZero | Value ≥ 0 |
@Negative | Value < 0 |
@NegativeOrZero | Value ≤ 0 |
@Digits(integer=5, fraction=2) | Up to 5 integer + 2 fraction digits |
Dates / Times
| Annotation | Notes |
|---|---|
@Past | Before now |
@PastOrPresent | Now or before |
@Future | After now |
@FutureOrPresent | Now or after |
Boolean
| Annotation | Notes |
|---|---|
@AssertTrue | Must be true |
@AssertFalse | Must 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
@ConfigurationPropertiesannotated with@Validatedat startup and fails fast if constraints are violated.
Common Pitfalls
@Validmissing — forgetting@Validbefore@RequestBodysilently skips all constraints; the controller receives invalid data with no error.@Validatedvs@Valid—@Valid(Jakarta) enables standard cascaded validation;@Validated(Spring) enables groups and method-level validation outside controllers.- Cascaded validation — nested objects require
@Validon the field/parameter AND the nested class must have its own constraints. - Constraint not applied to
null— most constraints (@Size,@Pattern,@Email) skipnullvalues by design; add@NotNullor@NotBlankexplicitly. - Custom validator as Spring bean — only works if the validator is registered as a Spring bean (via
@Componentor@Bean); otherwise dependency injection inside the validator won't work. BindingResultmust immediately follow@Valid— ifBindingResultis present, Spring does not throw; you must checkresult.hasErrors()yourself.