spring-validation
Bean Validation (Jakarta Validation) with Spring Boot. Custom validators, validation groups, cross-field validation, and internationalized error messages.
Best use case
spring-validation is best used when you need a repeatable AI agent workflow instead of a one-off prompt.
Bean Validation (Jakarta Validation) with Spring Boot. Custom validators, validation groups, cross-field validation, and internationalized error messages.
Teams using spring-validation should expect a more consistent output, faster repeated execution, less prompt rewriting, better workflow continuity with your supporting tools.
When to use this skill
- You want a reusable workflow that can be run more than once with consistent structure.
- You already have the supporting tools or dependencies needed by this skill.
When not to use this skill
- You only need a quick one-off answer and do not need a reusable workflow.
- You cannot install or maintain the underlying files, dependencies, or repository context.
Installation
Claude Code / Cursor / Codex
Manual Installation
- Download SKILL.md from GitHub
- Place it in
.claude/skills/spring-validation/SKILL.mdinside your project - Restart your AI agent — it will auto-discover the skill
How spring-validation Compares
| Feature / Agent | spring-validation | Standard Approach |
|---|---|---|
| Platform Support | Not specified | Limited / Varies |
| Context Awareness | High | Baseline |
| Installation Complexity | Unknown | N/A |
Frequently Asked Questions
What does this skill do?
Bean Validation (Jakarta Validation) with Spring Boot. Custom validators, validation groups, cross-field validation, and internationalized error messages.
Where can I find the source code?
You can find the source code on GitHub using the link provided at the top of the page.
SKILL.md Source
# Spring Validation
Bean Validation (Jakarta Validation) patterns for Spring Boot applications.
## Use this skill when
- Adding input validation to DTOs / request bodies
- Creating custom constraint annotations
- Implementing cross-field validation (e.g., password confirmation, date ranges)
- Setting up validation groups (different rules for create vs update)
- Customizing validation error messages or adding i18n
- Implementing method-level validation on service classes
- Building structured API error responses for validation failures
- User mentions "validation", "constraints", "@Valid", "@NotNull", "@NotBlank", "BindingResult"
## Do not use this skill when
- User needs authorization rules (use spring-security-oauth2)
- User wants business rule validation that depends on database state (use service-layer logic)
- User needs client-side / JavaScript validation only
- User asks about OpenAPI schema validation (use springdoc annotations)
## Dependencies
Add to `pom.xml`:
```xml
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-validation</artifactId>
</dependency>
```
This transitively brings in:
- `jakarta.validation:jakarta.validation-api` (constraint annotations)
- `org.hibernate.validator:hibernate-validator` (reference implementation)
> **Note:** `spring-boot-starter-web` does NOT include validation automatically since Spring Boot 2.3+. You must add `spring-boot-starter-validation` explicitly.
## Built-in Constraints Quick Reference
| Annotation | Applies to | Description |
|---|---|---|
| `@NotNull` | Any type | Must not be `null` (empty string `""` passes) |
| `@NotBlank` | `CharSequence` | Must not be `null`, must contain at least one non-whitespace character |
| `@NotEmpty` | `CharSequence`, `Collection`, `Map`, `Array` | Must not be `null` or empty (blank string `" "` passes) |
| `@Size(min, max)` | `CharSequence`, `Collection`, `Map`, `Array` | Length/size must be within bounds. `null` is valid. |
| `@Min(value)` | Numeric types | Must be >= value |
| `@Max(value)` | Numeric types | Must be <= value |
| `@Positive` | Numeric types | Must be > 0 |
| `@PositiveOrZero` | Numeric types | Must be >= 0 |
| `@Negative` | Numeric types | Must be < 0 |
| `@NegativeOrZero` | Numeric types | Must be <= 0 |
| `@DecimalMin(value)` | `BigDecimal`, `BigInteger`, `CharSequence`, numeric | Must be >= value (string comparison) |
| `@DecimalMax(value)` | `BigDecimal`, `BigInteger`, `CharSequence`, numeric | Must be <= value (string comparison) |
| `@Email` | `CharSequence` | Must be a valid email format. `null` is valid. |
| `@Pattern(regexp)` | `CharSequence` | Must match the regex. `null` is valid. |
| `@Past` | Date/time types | Must be a date in the past |
| `@PastOrPresent` | Date/time types | Must be a date in the past or present |
| `@Future` | Date/time types | Must be a date in the future |
| `@FutureOrPresent` | Date/time types | Must be a date in the future or present |
| `@Digits(integer, fraction)` | Numeric types | Must have at most N integer digits and F fraction digits |
| `@AssertTrue` | `boolean` | Must be `true` |
| `@AssertFalse` | `boolean` | Must be `false` |
> **Critical:** Most constraints pass on `null`. Combine with `@NotNull` if the field is required. For example, `@Email` alone allows `null`; use `@NotNull @Email` to make it mandatory.
See `references/builtin-constraints.md` for the complete reference with parameters, examples, and common gotchas.
## @Valid vs @Validated
| Feature | `@Valid` (Jakarta) | `@Validated` (Spring) |
|---|---|---|
| Package | `jakarta.validation` | `org.springframework.validation.annotation` |
| Validation groups | No | Yes |
| Method-level validation | No | Yes (on class) |
| Nested object cascade | Yes | Yes |
| Use on parameter | Yes | Yes |
| Use on class | No | Yes |
**Rule of thumb:**
- Use `@Valid` on `@RequestBody` parameters and nested fields when you do not need groups.
- Use `@Validated` on classes (controllers, services) to enable method-level validation and on parameters when you need validation groups.
## Validation in Controllers
### Basic validation with @Valid
```java
@RestController
@RequestMapping("/api/users")
public class UserController {
@PostMapping
public ResponseEntity<UserResponse> create(@Valid @RequestBody CreateUserRequest request) {
// If validation fails, MethodArgumentNotValidException is thrown automatically
UserResponse response = userService.create(request);
return ResponseEntity.status(HttpStatus.CREATED).body(response);
}
}
```
### Using BindingResult for manual error handling
```java
@PostMapping
public ResponseEntity<?> create(
@Valid @RequestBody CreateUserRequest request,
BindingResult bindingResult) {
if (bindingResult.hasErrors()) {
Map<String, String> errors = bindingResult.getFieldErrors().stream()
.collect(Collectors.toMap(
FieldError::getField,
fe -> fe.getDefaultMessage() != null ? fe.getDefaultMessage() : "Invalid value",
(a, b) -> a // keep first error per field
));
return ResponseEntity.badRequest().body(errors);
}
return ResponseEntity.status(HttpStatus.CREATED).body(userService.create(request));
}
```
> **Warning:** When you declare `BindingResult`, Spring will NOT throw `MethodArgumentNotValidException`. You must check errors manually. If you forget, invalid data silently passes through.
### Validation with groups
```java
@RestController
@RequestMapping("/api/users")
@Validated // Required for group-based validation on controller methods
public class UserController {
@PostMapping
public ResponseEntity<UserResponse> create(
@Validated(OnCreate.class) @RequestBody CreateUserRequest request) {
return ResponseEntity.status(HttpStatus.CREATED).body(userService.create(request));
}
@PutMapping("/{id}")
public ResponseEntity<UserResponse> update(
@PathVariable UUID id,
@Validated(OnUpdate.class) @RequestBody UpdateUserRequest request) {
return ResponseEntity.ok(userService.update(id, request));
}
}
```
## Validation Groups
Define marker interfaces to apply different validation rules for create vs update:
```java
// Marker interfaces
public interface OnCreate {}
public interface OnUpdate {}
```
```java
public record CreateUserRequest(
@NotBlank(groups = OnCreate.class)
@Size(min = 2, max = 100, groups = {OnCreate.class, OnUpdate.class})
String name,
@NotBlank(groups = OnCreate.class)
@Email(groups = {OnCreate.class, OnUpdate.class})
String email,
@NotBlank(groups = OnCreate.class)
@Size(min = 8, max = 72, groups = OnCreate.class)
String password
) {}
```
- On **create**: all fields required, all constraints enforced.
- On **update**: `@NotBlank` is skipped (field can be `null` meaning "don't change"), but format constraints still apply if a value is provided.
## Custom Constraint Annotations
### Step 1: Define the annotation
```java
@Documented
@Constraint(validatedBy = StrongPasswordValidator.class)
@Target({ElementType.FIELD, ElementType.PARAMETER})
@Retention(RetentionPolicy.RUNTIME)
public @interface StrongPassword {
String message() default "{validation.strongPassword}";
Class<?>[] groups() default {};
Class<? extends Payload>[] payload() default {};
int minLength() default 8;
}
```
### Step 2: Implement the validator
```java
public class StrongPasswordValidator implements ConstraintValidator<StrongPassword, String> {
private int minLength;
@Override
public void initialize(StrongPassword annotation) {
this.minLength = annotation.minLength();
}
@Override
public boolean isValid(String value, ConstraintValidatorContext context) {
if (value == null) return true; // Let @NotNull handle null checks
if (value.length() < minLength) return false;
if (!value.matches(".*[A-Z].*")) return false; // uppercase
if (!value.matches(".*[a-z].*")) return false; // lowercase
if (!value.matches(".*\\d.*")) return false; // digit
if (!value.matches(".*[!@#$%^&*].*")) return false; // special char
return true;
}
}
```
### Cross-field validation (class-level constraint)
```java
@Documented
@Constraint(validatedBy = ValidDateRangeValidator.class)
@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
public @interface ValidDateRange {
String message() default "End date must be after start date";
Class<?>[] groups() default {};
Class<? extends Payload>[] payload() default {};
String startField() default "startDate";
String endField() default "endDate";
}
```
```java
public class ValidDateRangeValidator implements ConstraintValidator<ValidDateRange, Object> {
private String startField;
private String endField;
@Override
public void initialize(ValidDateRange annotation) {
this.startField = annotation.startField();
this.endField = annotation.endField();
}
@Override
public boolean isValid(Object obj, ConstraintValidatorContext context) {
try {
BeanWrapper wrapper = new BeanWrapperImpl(obj);
LocalDate start = (LocalDate) wrapper.getPropertyValue(startField);
LocalDate end = (LocalDate) wrapper.getPropertyValue(endField);
if (start == null || end == null) return true;
boolean valid = end.isAfter(start);
if (!valid) {
context.disableDefaultConstraintViolation();
context.buildConstraintViolationWithTemplate(context.getDefaultConstraintMessageTemplate())
.addPropertyNode(endField)
.addConstraintViolation();
}
return valid;
} catch (Exception e) {
return false;
}
}
}
```
## Nested Object Validation
Use `@Valid` on nested fields to cascade validation:
```java
public record CreateOrderRequest(
@NotNull
UUID customerId,
@NotEmpty(message = "Order must have at least one item")
@Valid // Cascade validation into each OrderItemRequest
List<OrderItemRequest> items,
@Valid // Cascade validation into the address object
@NotNull
AddressRequest shippingAddress
) {}
public record OrderItemRequest(
@NotNull UUID productId,
@Positive int quantity
) {}
public record AddressRequest(
@NotBlank String street,
@NotBlank String city,
@NotBlank @Size(min = 2, max = 2) String state,
@NotBlank @Pattern(regexp = "\\d{5}(-\\d{4})?") String zipCode
) {}
```
> **Critical:** Without `@Valid` on the nested field, constraints on `OrderItemRequest` and `AddressRequest` will NOT be checked.
## Collection Validation
Validate elements inside collections using `@Valid` and container element constraints:
```java
public record BulkCreateRequest(
@NotEmpty
@Size(max = 100, message = "Cannot create more than 100 items at once")
List<@Valid CreateUserRequest> users
) {}
```
## Method-Level Validation
Apply validation to service method parameters and return values:
```java
@Service
@Validated // Enables method-level validation for this bean
public class UserService {
public UserResponse createUser(@Valid CreateUserRequest request) {
// @Valid triggers validation on the request parameter
// ConstraintViolationException thrown if invalid
// ...
}
@NotNull
public UserResponse findById(@NotNull UUID id) {
// Both parameter and return value validated
// ...
}
public void updateEmails(@NotEmpty List<@Email String> emails) {
// Validates list is not empty and each element is a valid email
// ...
}
}
```
> **Note:** Method-level validation throws `ConstraintViolationException` (not `MethodArgumentNotValidException`). Your exception handler must catch both.
## Error Message Customization
### Inline messages
```java
@NotBlank(message = "Name is required")
@Size(min = 2, max = 100, message = "Name must be between {min} and {max} characters")
private String name;
```
### Using messages.properties
```java
@NotBlank(message = "{user.name.required}")
private String name;
```
```properties
# src/main/resources/messages.properties
user.name.required=Name is required
user.email.invalid=Please provide a valid email address
```
### Message interpolation variables
```properties
# {min}, {max}, {value} are automatically available
user.name.size=Name must be between {min} and {max} characters
order.quantity.min=Quantity must be at least {value}
```
See `references/error-message-patterns.md` for complete i18n setup and custom message resolvers.
## Programmatic Validation
Use `Validator` directly when validation must happen outside the annotation flow:
```java
@Service
@RequiredArgsConstructor
public class ImportService {
private final Validator validator;
public ImportResult importUsers(List<CreateUserRequest> requests) {
List<String> errors = new ArrayList<>();
for (int i = 0; i < requests.size(); i++) {
Set<ConstraintViolation<CreateUserRequest>> violations =
validator.validate(requests.get(i), OnCreate.class);
if (!violations.isEmpty()) {
for (ConstraintViolation<CreateUserRequest> v : violations) {
errors.add("Row %d: %s %s".formatted(
i + 1, v.getPropertyPath(), v.getMessage()));
}
}
}
if (!errors.isEmpty()) {
return ImportResult.failure(errors);
}
// Proceed with import...
return ImportResult.success(requests.size());
}
}
```
## Exception Handling for Validation Errors
```java
@RestControllerAdvice
public class ValidationExceptionHandler {
// Handles @Valid @RequestBody failures
@ExceptionHandler(MethodArgumentNotValidException.class)
public ResponseEntity<ProblemDetail> handleValidation(MethodArgumentNotValidException ex) {
ProblemDetail problem = ProblemDetail.forStatus(HttpStatus.BAD_REQUEST);
problem.setTitle("Validation Failed");
problem.setDetail("One or more fields have validation errors");
Map<String, List<String>> fieldErrors = ex.getBindingResult()
.getFieldErrors().stream()
.collect(Collectors.groupingBy(
FieldError::getField,
Collectors.mapping(fe ->
fe.getDefaultMessage() != null ? fe.getDefaultMessage() : "Invalid",
Collectors.toList())
));
problem.setProperty("fieldErrors", fieldErrors);
return ResponseEntity.badRequest().body(problem);
}
// Handles @Validated method-level failures
@ExceptionHandler(ConstraintViolationException.class)
public ResponseEntity<ProblemDetail> handleConstraintViolation(ConstraintViolationException ex) {
ProblemDetail problem = ProblemDetail.forStatus(HttpStatus.BAD_REQUEST);
problem.setTitle("Constraint Violation");
Map<String, String> errors = ex.getConstraintViolations().stream()
.collect(Collectors.toMap(
v -> extractFieldName(v.getPropertyPath()),
ConstraintViolation::getMessage,
(a, b) -> a
));
problem.setProperty("errors", errors);
return ResponseEntity.badRequest().body(problem);
}
private String extractFieldName(Path propertyPath) {
String path = propertyPath.toString();
return path.contains(".") ? path.substring(path.lastIndexOf('.') + 1) : path;
}
}
```
## Code Quality Checklist
- [ ] `spring-boot-starter-validation` is in `pom.xml`
- [ ] DTOs use `@NotNull` combined with format constraints (`@Email`, `@Size`, etc.) for required fields
- [ ] Nested objects annotated with `@Valid` to cascade validation
- [ ] Collections use `List<@Valid ItemRequest>` for element validation
- [ ] Validation groups defined for create vs update scenarios
- [ ] Custom constraints return `true` for `null` values (let `@NotNull` handle nulls)
- [ ] Class-level constraints used for cross-field validation
- [ ] `@RestControllerAdvice` handles both `MethodArgumentNotValidException` and `ConstraintViolationException`
- [ ] Error responses use `ProblemDetail` (RFC 7807) format
- [ ] Validation messages externalized to `messages.properties`
- [ ] i18n messages provided for supported locales
- [ ] Method-level validation uses `@Validated` on the class, not `@Valid`
- [ ] `BindingResult` is only used when manual error handling is intentional
## References
- See `references/builtin-constraints.md` for the complete constraint reference
- See `references/error-message-patterns.md` for message customization and i18n
- See `examples/` for complete code examplesRelated Skills
bio-alignment-validation
Validate alignment quality with insert size distribution, proper pairing rates, GC bias, strand balance, and other post-alignment metrics. Use when verifying alignment data quality before variant calling or quantification.
date-validation
Use when editing Planning Hubs, timelines, calendars, or any file with day-name + date combinations (Wed Nov 12), relative dates (tomorrow), or countdowns (18 days until) - validates day-of-week accuracy, relative date calculations, and countdown math with two-source ground truth verification before allowing edits
springfield-max
Simpsons-themed autonomous workflow orchestrator v7.0 for platform building. Powered by Opus 4.6 Agent Teams, 1M context, adaptive thinking, and effort levels. 17 characters, full MCP access, 50 iteration limits, orchestrator promises, and mandatory quality gates. Domain-agnostic - works for any software platform.
spring-boot-rest-api-standards
Provides REST API design standards and best practices for Spring Boot projects. Use when creating or reviewing REST endpoints, DTOs, error handling, pagination, security headers, HATEOAS and architecture patterns.
spring-boot-project-creator
Creates and scaffolds a new Spring Boot project (3.x or 4.x) by downloading from Spring Initializr, generating package structure (DDD or Layered architecture), configuring JPA, SpringDoc OpenAPI, and Docker Compose services (PostgreSQL, Redis, MongoDB). Use when creating a new Java Spring Boot project from scratch, bootstrapping a microservice, or initializing a backend application.
spring-boot-performance
Guide for optimizing Spring Boot application performance including caching, pagination, async processing, and JPA optimization. Use this when addressing performance issues or implementing high-traffic features.
spring-boot-event-driven-patterns
Implement Event-Driven Architecture (EDA) in Spring Boot using ApplicationEvent, @EventListener, and Kafka. Use for building loosely-coupled microservices with domain events, transactional event listeners, and distributed messaging patterns.
spring-boot-engineer
Use when building Spring Boot 3.x applications, microservices, or reactive Java applications. Invoke for Spring Data JPA, Spring Security 6, WebFlux, Spring Cloud integration.
fullstack-validation
Comprehensive validation methodology for multi-component applications including backend, frontend, database, and infrastructure
api-validation
Apply when validating API request inputs: body, query params, path params, and headers. This skill covers Zod v4 patterns.
api-request-validation
A skill for implementing robust API request validation in Python web frameworks like FastAPI using Pydantic. Covers Pydantic models, custom validators (email, password), field-level and cross-field validation, query/file validation, and structured error responses. Use when you need to validate incoming API requests.
api-contracts-and-zod-validation
Generate Zod schemas and TypeScript types for forms, API routes, and Server Actions with runtime validation. Use this skill when creating API contracts, validating request/response payloads, generating form schemas, adding input validation to Server Actions or route handlers, or ensuring type safety across client-server boundaries. Trigger terms include zod, schema, validation, API contract, form validation, type inference, runtime validation, parse, safeParse, input validation, request validation, Server Action validation.