When building Java applications, especially with frameworks like Spring or Jakarta EE, validation plays a critical role. You might be familiar with annotations like @NotNull
, @Size
, or @Email
. But what if you need something more custom? Maybe you want a special kind of "NotNull" check — one that's tailored for your domain or offers better error messages.
In this post, we’ll walk through creating a custom constraint annotation, similar to @NotNull
.
Why Create a Custom Constraint?
- Specific validation logic: Maybe you want to check not only for null but also for empty strings, or to validate conditionally.
- Custom error messages: Tailor the messages for your application’s tone.
- Reusable across the codebase: Keep code clean and consistent.
- Domain-specific validations: "Name must not be blank", "User ID must not be empty", etc.
Step 1: Create the Custom Annotation
Let’s create a simple @ConditionalNotNull annotation.
import jakarta.validation.Constraint;
import jakarta.validation.Payload;
import java.lang.annotation.Documented;
import java.lang.annotation.Retention;
import java.lang.annotation.Target;
import static java.lang.annotation.ElementType.*;
import static java.lang.annotation.RetentionPolicy.RUNTIME;
@Documented
@Constraint(validatedBy = ConditionalNotNullValidator.class)
@Target({ METHOD, FIELD, ANNOTATION_TYPE, PARAMETER })
@Retention(RUNTIME)
public @interface ConditionalNotNull {
String message() default "This field cannot be null";
Class>[] groups() default {};
Class extends Payload>[] payload() default {};
}
Explanation:
-
@Constraint
points to the Validator class that will contain the actual validation logic. -
@Target
specifies where the annotation can be used (fields, parameters, etc.). -
@Retention(RUNTIME)
means the annotation is available at runtime for validation frameworks.
Step 2: Implement the Validator
Now, create a validator class ConditionalNotNullValidator
.
import jakarta.validation.ConstraintValidator;
import jakarta.validation.ConstraintValidatorContext;
public class ConditionalNotNullValidator implements ConstraintValidator {
@Override
public void initialize(ConditionalNotNull constraintAnnotation) {
// You can access annotation attributes here if needed
}
@Override
public boolean isValid(Object value, ConstraintValidatorContext context) {
return value != null;
}
}
Explanation:
- The
isValid
method returnstrue
if the value passes validation. - Here, we simply check if the value is not
null
.
Step 3: Use the Custom Annotation
Now you can use @ConditionalNotNull just like @NotNull
!
public class UserDTO {
@ConditionalNotNull(message = "Username must not be null!")
private String username;
@ConditionalNotNull
private String email;
// getters and setters
}
When you validate UserDTO, the @CondtionalNotNull
constraints will automatically kick in.
Bonus: Extend It Further
You can enhance @ConditionalNotNull
to:
- Check for non-empty strings or collections
- Add conditional validation (validate only if another field has a certain value)
- Support customized error codes for better API responses
Example to check non-empty Strings:
@Override
public boolean isValid(Object value, ConstraintValidatorContext context) {
if (value == null) {
return false;
}
if (value instanceof String) {
return !((String) value).trim().isEmpty();
}
return true;
}
Now @ConditionalNotNull
not only checks for null but also ensures strings aren't just blank spaces!
Conclusion
Creating a custom constraint like @NotNull
is straightforward but very powerful. It keeps your code cleaner, validations reusable, and applications more maintainable.
By combining annotations and validator classes, you can create any kind of validation logic tailored specifically to your project’s needs.