How can I create a custom annotation in Jakarta Validation with dynamic error messages similar to @Size?
I’m working with Jakarta Validation and would like to create a custom annotation similar to @Size. For example, for a String field:
public class RegForm {
@Size(min=3, max=20)
private String login;
}
When validation fails by default, it outputs a message containing the min and max attribute values:
“size must be between 3 and 20”
Looking at the source code, the message attribute of the annotation looks like this:
String message() default "{jakarta.validation.constraints.Size.message}";
This means the default message should be in MessageSource. I’m using Spring Boot, but I couldn’t find similar functionality in its autoconfiguration. I’m also unclear about how the min and max values get into MessageSource.
Could you please explain how to create a custom annotation whose message can vary depending on the annotation attribute values?
Creating Custom Jakarta Validation Annotations with Dynamic Error Messages
To create your own Jakarta Validation annotation with dynamic error messages, you’ll need to create three components: an annotation, a validator, and a message configuration. The system uses message interpolation, where parameters in curly braces are automatically replaced with annotation attribute values.
Table of Contents
- Jakarta Validation Basics and Message Interpolation
- Creating a Custom Annotation
- Implementing ConstraintValidator
- Message Configuration in Spring Boot
- Complete Implementation Example
- Advanced Features
- Troubleshooting Common Problems
Jakarta Validation Basics and Message Interpolation
Jakarta Validation uses a message interpolation mechanism to create dynamic error messages. When validation fails with an error, the system automatically substitutes annotation attribute values into the message template.
The interpolation process works as follows:
- Finds the message template based on the key (e.g.,
{jakarta.validation.constraints.Size.message}) - Replaces parameters in curly braces with values from corresponding annotation attributes
- Supports international localization through MessageSource
For the @Size annotation, the system automatically substitutes the min and max values into the message template. You can use the same principles for your own annotations.
Creating a Custom Annotation
Let’s create a @CustomSize annotation that will work similarly to @Size, but with the ability to use custom messages.
import jakarta.validation.Constraint;
import jakarta.validation.Payload;
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
@Target({ElementType.FIELD, ElementType.PARAMETER})
@Retention(RetentionPolicy.RUNTIME)
@Constraint(validatedBy = CustomSizeValidator.class)
public @interface CustomSize {
String message() default "{com.example.validation.CustomSize.message}";
Class<?>[] groups() default {};
Class<? extends Payload>[] payload() default {};
int min() default 0;
int max() default Integer.MAX_VALUE;
}
Key points:
@Constraintspecifies the validator classmessage()uses a template with a key for interpolationmin()andmax()define the value range- The annotation can be applied to fields and method parameters
Implementing ConstraintValidator
Now let’s create a validator that will check values and pass parameters to the interpolation system:
import jakarta.validation.ConstraintValidator;
import jakarta.validation.ConstraintValidatorContext;
import org.springframework.beans.BeanWrapperImpl;
public class CustomSizeValidator implements ConstraintValidator<CustomSize, CharSequence> {
private int min;
private int max;
@Override
public void initialize(CustomSize constraintAnnotation) {
this.min = constraintAnnotation.min();
this.max = constraintAnnotation.max();
}
@Override
public boolean isValid(CharSequence value, ConstraintValidatorContext context) {
if (value == null) {
return true; // null values are validated by @NotNull annotation
}
int length = value.length();
if (length < min || length > max) {
// Add parameters for message interpolation
context.disableDefaultConstraintViolation();
context.buildConstraintViolationWithTemplate(
context.getDefaultConstraintMessageTemplate()
)
.addConstraintViolation()
.addPropertyNode("value")
.addDynamicValue("min", min)
.addDynamicValue("max", max)
.addConstraintViolation();
return false;
}
return true;
}
}
Important: The addDynamicValue() method adds values for interpolation parameters.
Message Configuration in Spring Boot
For message interpolation to work, you need to configure MessageSource. Create a message file:
messages.properties
com.example.validation.CustomSize.message=Size must be between {min} and {max} characters
messages_ru_RU.properties
com.example.validation.CustomSize.message=Размер должен быть между {min} и {max} символами
Add the configuration to Spring Boot:
import org.springframework.context.MessageSource;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.support.ReloadableResourceBundleMessageSource;
import org.springframework.validation.beanvalidation.LocalValidatorFactoryBean;
@Configuration
public class ValidationConfig {
@Bean
public MessageSource messageSource() {
ReloadableResourceBundleMessageSource messageSource =
new ReloadableResourceBundleMessageSource();
messageSource.setBasename("classpath:messages");
messageSource.setDefaultEncoding("UTF-8");
messageSource.setCacheSeconds(10);
return messageSource;
}
@Bean
public LocalValidatorFactoryBean validator() {
LocalValidatorFactoryBean bean = new LocalValidatorFactoryBean();
bean.setValidationMessageSource(messageSource());
return bean;
}
}
Complete Implementation Example
Let’s create a complete example of using our annotation:
import jakarta.validation.Valid;
import org.springframework.http.ResponseEntity;
import org.springframework.validation.BindingResult;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RestController;
@RestController
public class UserController {
@PostMapping("/users")
public ResponseEntity<String> createUser(@Valid @RequestBody UserDto user, BindingResult result) {
if (result.hasErrors()) {
// Handle validation errors
return ResponseEntity.badRequest().body(result.getAllErrors().toString());
}
return ResponseEntity.ok("User created successfully");
}
}
// DTO with our annotation
public class UserDto {
@CustomSize(min=3, max=20)
private String username;
@CustomSize(min=6, max=30)
private String password;
// getters and setters
}
For testing, you can use the following service:
import org.springframework.stereotype.Service;
import org.springframework.validation.annotation.Validated;
@Service
@Validated
public class UserService {
public void validateUsername(@CustomSize(min=3, max=20) String username) {
// Method with parameter validation
System.out.println("Username is valid: " + username);
}
}
Advanced Features
Conditional Validation
You can add additional attributes for conditional validation:
@Constraint(validatedBy = CustomSizeValidator.class)
public @interface CustomSize {
// ... existing attributes
String message() default "{com.example.validation.CustomSize.message}";
boolean includeMin() default true;
boolean includeMax() default true;
}
Expression Language Interpolation
For complex scenarios, you can use Expression Language:
@Constraint(validatedBy = CustomSizeValidator.class)
public @interface CustomSize {
String message() default "{com.example.validation.CustomSize.expression}";
String expression() default "min == max ? 'must be equal to {min}' : 'must be between {min} and {max}'";
}
Custom Interpolation Logic
For complex scenarios, you can create a custom interpolator:
import jakarta.validation.MessageInterpolator;
import org.springframework.context.MessageSource;
public class CustomMessageInterpolator implements MessageInterpolator {
private final MessageSource messageSource;
public CustomMessageInterpolator(MessageSource messageSource) {
this.messageSource = messageSource;
}
@Override
public String interpolate(String messageTemplate, Context context) {
// Your custom interpolation logic
return messageSource.getMessage(messageTemplate, null, Locale.getDefault());
}
// ... other interface methods
}
Troubleshooting Common Problems
Problem: Parameters are not substituted in the message
Solution: Ensure that:
- The validator adds parameters via
addDynamicValue() - The message key matches the one specified in the annotation
- MessageSource is properly configured
Problem: Messages are not localized
Solution: Check:
- Message file names (messages_ru_RU.properties)
- UTF-8 encoding
- Correct locales in the application
Problem: Annotation doesn’t work in Spring Boot
Solution: Add the dependency to pom.xml:
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-validation</artifactId>
</dependency>
And ensure the validator is registered as a bean.
Problem: Dynamic values are not passed
Solution: Use ConstraintValidatorContext to add dynamic values:
context.disableDefaultConstraintViolation();
context.buildConstraintViolationWithTemplate(
"{com.example.validation.CustomSize.message}"
)
.addConstraintViolation()
.addDynamicValue("min", min)
.addDynamicValue("max", max);
Sources
- Configurable Model Validations with Jakarta Bean Validation, Spring Boot and Hibernate — Estafet
- Spring Boot Custom Bean Validations with Jakarta ConstraintValidator, Grouping Validation Constraints, GroupSequence and i18n messages · GitHub
- Java Bean Validation :: Spring Framework
- Spring Validation Message Interpolation | Baeldung
- Custom Validation in Spring Boot best explained – Part 1 – DevXperiences
- Hibernate Validator 9.0.1.Final - Jakarta Validation Reference
- Java Bean Validation with Javax/Jakarta Validation | Medium
- @Valid + Jakarta with DTO in Spring Boot 3 - DEV Community
- Custom Validation Messages in Spring Boot REST APIs | Medium
- Guide to Field Validation with Jakarta Validation in Spring | Medium
Conclusion
- To create dynamic validation messages, use Jakarta Validation’s interpolation mechanism with parameters in curly braces
- A custom annotation requires three components: the annotation itself, the validator, and the message configuration
- Spring Boot simplifies configuration through automatic validation configuration and MessageSource
- To pass dynamic values, use the
addDynamicValue()method in ConstraintValidator - The system supports international localization through different property files
I recommend starting with the basic implementation as shown in the example and gradually adding more complex features as needed.