Bird
Raised Fist0
Spring Bootframework~7 mins

Custom validator annotation in Spring Boot

Choose your learning style10 modes available

Start learning this pattern below

Jump into concepts and practice - no test required

or
Recommended
Test this pattern10 questions across easy, medium, and hard to know if this pattern is strong
Introduction

Custom validator annotations let you check if data meets your own rules. This helps keep your app data clean and correct.

You want to check a user input field with rules not covered by built-in validators.
You need to validate a complex object property with special logic.
You want to reuse the same validation rule on many fields or classes.
You want to keep validation logic separate from your main code for clarity.
Syntax
Spring Boot
import jakarta.validation.Constraint;
import jakarta.validation.Payload;
import java.lang.annotation.*;

@Documented
@Constraint(validatedBy = YourValidatorClass.class)
@Target({ ElementType.METHOD, ElementType.FIELD })
@Retention(RetentionPolicy.RUNTIME)
public @interface YourAnnotation {
    String message() default "Validation failed message";
    Class<?>[] groups() default {};
    Class<? extends Payload>[] payload() default {};
}

The @Constraint annotation links your custom annotation to its validator class.

The message is the error shown if validation fails.

Examples
This defines a custom annotation @NotEmptyString to check if a string is not empty.
Spring Boot
import jakarta.validation.Constraint;
import jakarta.validation.Payload;
import java.lang.annotation.*;

@Documented
@Constraint(validatedBy = NotEmptyStringValidator.class)
@Target({ ElementType.FIELD })
@Retention(RetentionPolicy.RUNTIME)
public @interface NotEmptyString {
    String message() default "String must not be empty";
    Class<?>[] groups() default {};
    Class<? extends Payload>[] payload() default {};
}
This is the validator class that checks if the string is not null or empty.
Spring Boot
import jakarta.validation.ConstraintValidator;
import jakarta.validation.ConstraintValidatorContext;

public class NotEmptyStringValidator implements ConstraintValidator<NotEmptyString, String> {
    @Override
    public boolean isValid(String value, ConstraintValidatorContext context) {
        return value != null && !value.trim().isEmpty();
    }
}
Sample Program

This example creates a custom annotation @StartsWithA that checks if a string starts with 'A'. The Person class uses it on the name field. The main method validates two persons and prints violations.

Spring Boot
import jakarta.validation.Constraint;
import jakarta.validation.Payload;
import jakarta.validation.ConstraintValidator;
import jakarta.validation.ConstraintValidatorContext;
import jakarta.validation.Validation;
import jakarta.validation.Validator;
import jakarta.validation.ValidatorFactory;
import jakarta.validation.constraints.NotNull;
import java.lang.annotation.*;
import java.util.Set;
import jakarta.validation.ConstraintViolation;

@Documented
@Constraint(validatedBy = StartsWithAValidator.class)
@Target({ ElementType.FIELD })
@Retention(RetentionPolicy.RUNTIME)
@interface StartsWithA {
    String message() default "Must start with letter A";
    Class<?>[] groups() default {};
    Class<? extends Payload>[] payload() default {};
}

class StartsWithAValidator implements ConstraintValidator<StartsWithA, String> {
    @Override
    public boolean isValid(String value, ConstraintValidatorContext context) {
        return value != null && value.startsWith("A");
    }
}

class Person {
    @NotNull
    @StartsWithA
    String name;

    Person(String name) {
        this.name = name;
    }
}

public class CustomValidatorDemo {
    public static void main(String[] args) {
        ValidatorFactory factory = Validation.buildDefaultValidatorFactory();
        Validator validator = factory.getValidator();

        Person p1 = new Person("Alice");
        Person p2 = new Person("Bob");

        Set<ConstraintViolation<Person>> violations1 = validator.validate(p1);
        Set<ConstraintViolation<Person>> violations2 = validator.validate(p2);

        System.out.println("Violations for p1: " + violations1.size());
        System.out.println("Violations for p2: " + violations2.size());
        for (var v : violations2) {
            System.out.println(v.getPropertyPath() + ": " + v.getMessage());
        }
    }
}
OutputSuccess
Important Notes

Always implement ConstraintValidator with your annotation and the type you want to validate.

Use @Target to specify where your annotation can be used (fields, methods, etc.).

Remember to add @Retention(RetentionPolicy.RUNTIME) so the annotation is available at runtime.

Summary

Custom validator annotations let you create your own rules for data checks.

You link the annotation to a validator class that contains the logic.

Use them to keep validation clean and reusable across your app.

Practice

(1/5)
1. What is the main purpose of creating a custom validator annotation in Spring Boot?
easy
A. To define your own validation rules reusable across your application
B. To replace built-in annotations like @NotNull completely
C. To automatically generate database tables
D. To handle HTTP requests in controllers

Solution

  1. Step 1: Understand the role of custom validator annotations

    They allow you to create your own rules for validating data beyond built-in checks.
  2. Step 2: Identify the main benefit

    These annotations keep validation logic clean and reusable across different parts of your app.
  3. Final Answer:

    To define your own validation rules reusable across your application -> Option A
  4. Quick Check:

    Custom validator = reusable validation rules [OK]
Hint: Custom validators create reusable rules, not replace built-ins [OK]
Common Mistakes:
  • Thinking custom validators replace all built-in annotations
  • Confusing validation with database or HTTP handling
  • Assuming custom validators auto-generate code
2. Which of the following is the correct way to declare a custom validator annotation interface in Spring Boot?
easy
A. @Validator class MyValidator { String message() default "Invalid"; }
B. class MyValidator { String message() default "Invalid"; }
C. interface MyValidator { void validate(); }
D. @interface MyValidator { String message() default "Invalid"; Class[] groups() default {}; Class[] payload() default {}; }

Solution

  1. Step 1: Recall the syntax for custom annotation interfaces

    They use @interface keyword and define methods like message(), groups(), and payload().
  2. Step 2: Check each option

    @interface MyValidator { String message() default "Invalid"; Class[] groups() default {}; Class[] payload() default {}; } correctly uses @interface and includes required methods. Others either use wrong keywords or miss required parts.
  3. Final Answer:

    @interface MyValidator { String message() default "Invalid"; Class[] groups() default {}; Class[] payload() default {}; } -> Option D
  4. Quick Check:

    Custom annotation = @interface + standard methods [OK]
Hint: Custom annotations use @interface with message, groups, payload [OK]
Common Mistakes:
  • Using class or interface instead of @interface
  • Omitting groups() or payload() methods
  • Adding methods unrelated to validation
3. Given this validator class snippet, what will happen when validating a string with value "abc123"?
public class AlphaValidator implements ConstraintValidator<Alpha, String> {
  public boolean isValid(String value, ConstraintValidatorContext context) {
    return value != null && value.matches("^[a-zA-Z]+$");
  }
}
medium
A. Validation throws a NullPointerException
B. Validation passes because the string contains letters
C. Validation fails because the string contains digits
D. Validation always returns true regardless of input

Solution

  1. Step 1: Analyze the validation logic

    The method checks if the string is not null and matches the regex "^[a-zA-Z]+$", which means only letters allowed.
  2. Step 2: Test the input "abc123" against the regex

    Since "abc123" contains digits, it does not match the regex, so the method returns false.
  3. Final Answer:

    Validation fails because the string contains digits -> Option C
  4. Quick Check:

    Regex allows only letters, digits cause failure [OK]
Hint: Check regex carefully; digits break letter-only pattern [OK]
Common Mistakes:
  • Assuming partial match passes validation
  • Ignoring null check in code
  • Thinking digits are allowed by regex
4. You wrote a custom validator but it always passes validation even for invalid data. Which of these is the most likely cause?
medium
A. The annotation interface is missing the @Target annotation
B. The isValid method always returns true regardless of input
C. The validator class does not implement ConstraintValidator
D. The message() method in the annotation returns an empty string

Solution

  1. Step 1: Understand the role of isValid method

    This method contains the validation logic and must return true only for valid inputs.
  2. Step 2: Identify why validation always passes

    If isValid always returns true, invalid data will pass unchecked.
  3. Final Answer:

    The isValid method always returns true regardless of input -> Option B
  4. Quick Check:

    isValid controls validation result; always true means always pass [OK]
Hint: Check isValid method return values first when validation fails [OK]
Common Mistakes:
  • Forgetting to implement ConstraintValidator interface
  • Missing @Target causes compile warnings but not always validation failure
  • Empty message() affects error text, not validation logic
5. You want to create a custom validator annotation @StartsWith that checks if a string starts with a given prefix. Which combination of elements is required to implement this correctly?
hard
A. An annotation interface with a String prefix() method, a validator class implementing ConstraintValidator<StartsWith, String>, and overriding isValid to check the prefix
B. An annotation interface with int length(), a validator class implementing Validator, and overriding validate to check length
C. A class annotated with @Component that implements ConstraintValidator without an annotation interface
D. An annotation interface with String suffix(), a validator class implementing ConstraintValidator<EndsWith, String>, and overriding isValid to check suffix

Solution

  1. Step 1: Define the annotation interface with a prefix parameter

    The annotation must declare a method String prefix() to accept the prefix value.
  2. Step 2: Implement the validator class correctly

    The validator class must implement ConstraintValidator<StartsWith, String> and override isValid to check if the string starts with the given prefix.
  3. Final Answer:

    An annotation interface with a String prefix() method, a validator class implementing ConstraintValidator<StartsWith, String>, and overriding isValid to check the prefix -> Option A
  4. Quick Check:

    Annotation + validator class + isValid checking prefix = correct [OK]
Hint: Match annotation method and validator generic types carefully [OK]
Common Mistakes:
  • Using wrong method names like suffix() for prefix check
  • Implementing wrong interfaces or missing annotation interface
  • Confusing validate() with isValid() method names