Skip to main content

Jakarta Bean Validation

Easygram integrates with Jakarta Bean Validation (formerly Java EE Validation / Hibernate Validator) to let you declare constraints directly on handler method parameters. When a constraint fails, the framework throws a ConstraintViolationException before the handler body runs — no validation boilerplate inside your methods.

How it works

After argument resolvers populate the handler method parameters, MethodInvocationFilter calls ExecutableValidator.validateParameters() on the resolved argument array:

@BotCommand / @BotTextDefault / … matched

Argument resolvers — @BotTextValue, @BotCommandQueryParam, User, Chat, …

MethodInvocationFilter:
ExecutableValidator.validateParameters(bean, method, args)
↓ violation found?
│ YES → throw ConstraintViolationException (handler body never runs)
│ NO ↓
Handler method body executes

Validation is conditional: the Validator bean must be present in the application context. If no Validator is configured, the step is silently skipped.

Enabling validation

spring-boot-starter-validation is a transitive dependency of most Easygram transport modules (longpolling, webhook, messaging-kafka-consumer, etc.). In practice you rarely need to declare it explicitly. If your project only uses core-api you can add it manually:

<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-validation</artifactId>
</dependency>

Spring Boot autoconfigures a Validator bean automatically when this dependency is on the classpath.

Supported parameter types

Any handler parameter resolved by a built-in or custom argument resolver can carry Jakarta constraint annotations:

ResolverAnnotationExample
Text message body@BotTextValue@NotBlank @Size(max=200) String text
Command query param@BotCommandQueryParam@Min(1) @Max(100) int page
Callback query data@BotCallbackQueryData@NotBlank String data
Full command string@BotCommandValue@Pattern(regexp="^/\\w+") String cmd

Telegram-native objects injected by type (User, Chat, Update, BotRequest) are not typically constrained — use @Valid on your own POJO parameters instead.

Basic example

@BotController
public class SearchController {

@BotCommand("/search")
public String search(
@BotCommandQueryParam("q") @NotBlank @Size(min = 2, max = 100) String query,
@BotCommandQueryParam("page") @Min(1) int page) {
// Only reached when query is non-blank, between 2-100 chars, and page >= 1
return "Searching for: " + query + " (page " + page + ")";
}

@BotExceptionHandler(ConstraintViolationException.class)
public String onValidationError(ConstraintViolationException ex) {
String messages = ex.getConstraintViolations().stream()
.map(v -> "• " + v.getMessage())
.collect(Collectors.joining("\n"));
return "⚠️ Invalid input:\n" + messages;
}
}
Exception handler scope

@BotExceptionHandler methods are scoped per controller — the handler above only catches violations thrown by methods in SearchController. Place a shared handler in a dedicated @BotController class if you want to cover all controllers from one place.

Common constraints

AnnotationApplies toExample
@NotNullAny@NotNull String text
@NotBlankString@NotBlank String name
@NotEmptyString, Collection@NotEmpty List<String> ids
@Size(min, max)String, Collection@Size(min=2, max=50) String city
@Min(value)Numeric@Min(1) int quantity
@Max(value)Numeric@Max(120) int age
@Pattern(regexp)String@Pattern(regexp="^\\+\\d{7,15}$") String phone
@EmailString@Email String address
@PositiveNumeric@Positive long chatId
@PositiveOrZeroNumeric@PositiveOrZero int offset

All Jakarta Validation 3.x constraints are supported. Third-party constraints (Hibernate Validator extras, custom annotations) also work as long as they implement ConstraintValidator.

Custom constraint messages

Override the default message in the annotation:

@NotBlank(message = "Name must not be empty")
@Size(min = 2, max = 50, message = "Name must be between 2 and 50 characters")
String name

Or use ValidationMessages.properties (standard Jakarta Validation resource bundle):

# src/main/resources/ValidationMessages.properties
uz.example.constraint.name.notblank=Name must not be empty
uz.example.constraint.name.size=Name must be between {min} and {max} characters
@NotBlank(message = "{uz.example.constraint.name.notblank}")
@Size(min = 2, max = 50, message = "{uz.example.constraint.name.size}")
String name

Custom validators

Implement ConstraintValidator<A, T> to create reusable annotations:

// 1. The annotation
@Documented
@Constraint(validatedBy = TelegramUsernameValidator.class)
@Target({ElementType.PARAMETER})
@Retention(RetentionPolicy.RUNTIME)
public @interface TelegramUsername {
String message() default "Invalid Telegram username";
Class<?>[] groups() default {};
Class<? extends Payload>[] payload() default {};
}

// 2. The validator
public class TelegramUsernameValidator
implements ConstraintValidator<TelegramUsername, String> {

@Override
public boolean isValid(String value, ConstraintValidatorContext ctx) {
if (value == null) return true; // let @NotNull handle null
return value.startsWith("@") && value.length() >= 6 && value.length() <= 33;
}
}

// 3. Usage on a handler parameter
@BotCommand("/lookup")
public String lookup(@BotCommandQueryParam("username") @TelegramUsername String username) {
return "Looking up: " + username;
}

Exception handling patterns

Minimal — plain text reply

@BotExceptionHandler(ConstraintViolationException.class)
public String onValidationError(ConstraintViolationException ex) {
String messages = ex.getConstraintViolations().stream()
.map(v -> "• " + v.getMessage())
.collect(Collectors.joining("\n"));
return "⚠️ Invalid input:\n" + messages;
}

With i18n (core-i18n)

When using core-i18n, return a LocalizedReply with a format argument derived from the violations:

@BotExceptionHandler(ConstraintViolationException.class)
public LocalizedReply onValidationError(ConstraintViolationException ex) {
String detail = ex.getConstraintViolations().stream()
.map(v -> "• " + v.getMessage())
.collect(Collectors.joining("\n"));
// "error.validation=⚠️ Invalid input:\n{0}" in messages/bot.properties
return LocalizedReply.of("error.validation", detail);
}

Structured — field-level feedback

@BotExceptionHandler(ConstraintViolationException.class)
public String onValidationError(ConstraintViolationException ex) {
return ex.getConstraintViolations().stream()
.map(v -> {
// extract the parameter name from the path
String field = StreamSupport
.stream(v.getPropertyPath().spliterator(), false)
.reduce((a, b) -> b)
.map(Path.Node::getName)
.orElse("input");
return String.format("• %s: %s", field, v.getMessage());
})
.collect(Collectors.joining("\n", "⚠️ Validation errors:\n", ""));
}

Disabling validation for a specific handler

If you need to bypass validation for one handler while keeping it everywhere else, declare a no-op override by registering a custom Validator implementation just for that case — or simply omit constraint annotations on that handler's parameters.

To globally disable validation, exclude the Validator bean:

@SpringBootApplication(exclude = ValidationAutoConfiguration.class)
public class MyBotApplication { ... }

Constraint groups

Use groups to run different validation rules depending on context:

public interface OnRegistration {}
public interface OnUpdate {}

@BotCommand("/register")
public String register(
@BotCommandQueryParam("email")
@NotBlank(groups = OnRegistration.class)
@Email(groups = {OnRegistration.class, OnUpdate.class})
String email) { ... }
Groups and sequential validation

The default group (Default) is always validated. Custom groups require explicit wiring via a @GroupSequence or a custom Validator configuration — standard Jakarta Validation behaviour.


See also: