Skip to main content
Version: 0.0.7

Parameter Injection

Easygram automatically resolves and injects parameters into your handler methods via a set of built-in argument resolvers. You can declare any combination of supported types in any order — the framework matches each parameter by type (and annotation, where required).

Every supported type can also be wrapped in Optional<T>. The framework resolves T normally and wraps the result in Optional.ofNullable(). If no value is available (e.g. the update has no contact when Optional<Contact> is declared), Optional.empty() is injected. See Optional Parameters for details.

Complete Reference

Resolved type / annotationModuleNotes
UpdatecoreRaw Telegram update object
UsercoreMessage sender; set by BotContextSetterFilter
ChatcoreChat context; set by BotContextSetterFilter
BotRequestcoreCurrent request object
BotResponsecoreMutable response accumulator
TelegramClientcoreTelegram API client
BotMetadatacoreBot username and ID
Throwable (or any subclass)coreException — only populated in @BotExceptionHandler methods
@BotCommandValue StringcoreThe matched command (e.g. "/start")
@BotTextValue StringcoreFull message text
@BotCallbackQueryData StringcoreCallback query data string
@BotCommandQueryParam TcoreTyped positional argument after the command (Jackson-converted)
ContactcoreContact object — only in @BotContact handlers
LocationcoreLocation object — only in @BotLocation handlers
InlineQuerycoreInline query — only in @BotInlineQuery handlers
ChosenInlineResultcoreChosen inline result — only in @BotChosenInlineResult handlers
ShippingQuerycoreShipping query — only in @BotShippingQuery handlers
PreCheckoutQuerycorePre-checkout query — only in @BotPreCheckoutQuery handlers
PollcorePoll object — only in @BotPoll handlers
PollAnswercorePoll answer — only in @BotPollAnswer handlers
ChatMemberUpdatedcoreChat member update — only in @BotMyChatMember / @BotChatMemberUpdate handlers
ChatJoinRequestcoreChat join request — only in @BotChatJoinRequest handlers
BusinessConnectioncoreBusiness connection — only in @BotBusinessConnection handlers
BusinessMessagesDeletedcoreDeleted business messages — only in @BotDeletedBusinessMessages handlers
BotMarkupContextcoreDynamic markup params — only in @BotMarkup factory methods
Localecore-i18nUser's locale; requires core-i18n on the classpath
@BotInlineQueryValue StringcoreInline query text string — only in @BotInlineQuery handlers
@BotChosenInlineResultId StringcoreChosen result id — only in @BotChosenInlineResult handlers
@BotShippingPayload StringcoreInvoice payload string — only in @BotShippingQuery handlers
@BotPreCheckoutPayload StringcoreInvoice payload string — only in @BotPreCheckoutQuery handlers
Optional wrapping

Every type in this table can be wrapped in Optional<T>. The resolved value is wrapped in Optional.ofNullable()Optional.empty() is injected when no value is available.


Core Types

User & Chat

User and Chat are extracted from the incoming Update by BotContextSetterFilter before your handler runs.

@BotCommand("/whoami")
public String whoAmI(User user, Chat chat) {
return "You are " + user.getFirstName() + " in chat " + chat.getId();
}

Update

Inject the raw Telegram Update when you need access to data not exposed by the higher-level parameters.

@BotCommand("/debug")
public String debug(Update update) {
return "Update ID: " + update.getUpdateId();
}

BotRequest & BotResponse

BotRequest is the internal request wrapper for the current update. BotResponse is a mutable accumulator — methods enqueued on it are sent by BotApiSenderFilter after the handler chain completes.

@BotCommand("/status")
public void status(BotRequest request, BotResponse response) {
long chatId = request.getChat().getId();
response.addApiMethod(SendMessage.builder()
.chatId(chatId)
.text("Status: OK")
.build());
}

TelegramClient

Use TelegramClient to execute API calls directly and inspect the result synchronously.

@BotCommand("/pin")
public void pin(Chat chat, TelegramClient client) throws TelegramApiException {
client.execute(PinChatMessage.builder()
.chatId(chat.getId())
.messageId(someMessageId)
.build());
}

BotMetadata

Provides the bot's own username and ID, as registered with Telegram.

@BotCommand("/me")
public String botInfo(BotMetadata meta) {
return "I am @" + meta.getUsername() + " (ID " + meta.getBotId() + ")";
}

Annotated Parameters

@BotCommandValue

Injects the matched command string exactly as received.

@BotCommand("/start")
public String onStart(@BotCommandValue String command) {
// command = "/start"
return "Hello from " + command;
}

@BotTextValue

Injects the full text of the incoming message.

@BotText("hello")
public String onHello(@BotTextValue String text) {
// text = "hello"
return "You said: " + text;
}

@BotCallbackQueryData

Injects the data string from an inline keyboard callback query.

@BotCallbackQuery("action:")
public String onAction(@BotCallbackQueryData String data) {
// data = "action:something"
return "Received: " + data;
}

@BotCommandQueryParam

Extracts and type-converts the single positional argument that follows the command (the second space-separated token). Conversion is performed via Jackson's ObjectMapper, so any type Jackson can deserialize from a plain string is supported.

// User sends: /start 42
@BotCommand("/start")
public String onDeepLink(@BotCommandQueryParam Integer referralCode) {
return "Referred by: " + referralCode;
}
// User sends: /item widget
@BotCommand("/item")
public String onItem(@BotCommandQueryParam String itemName) {
return "You selected: " + itemName;
}

Note: Only one positional argument is supported — the token at parts[1] (index 1 after splitting on whitespace). There is no named-parameter syntax.


Handler-Specific Parameters

Contact (@BotContact)

Contact is only resolved when the handler is annotated with @BotContact.

@BotContact
public String onContact(User user, Contact contact) {
return "Received contact: " + contact.getPhoneNumber();
}

Location (@BotLocation)

Location is only resolved when the handler is annotated with @BotLocation.

@BotLocation
public String onLocation(Location location) {
return "Lat: " + location.getLatitude() + ", Lon: " + location.getLongitude();
}

Throwable (@BotExceptionHandler)

Throwable (or any subclass) is only resolved inside @BotExceptionHandler methods. Declare the most specific exception type you need — the resolver matches any assignable subclass.

@BotExceptionHandler
public String onError(Throwable ex, User user) {
return "Sorry " + user.getFirstName() + ", something went wrong: " + ex.getMessage();
}

// Or target a specific exception type:
@BotExceptionHandler
public String onIllegalArg(IllegalArgumentException ex) {
return "Bad input: " + ex.getMessage();
}

BotMarkupContext (@BotMarkup factory methods only)

BotMarkupContext carries runtime parameters passed when a handler returns a PlainReply (or similar) with a parameterised markup ID. It is only available in @BotMarkup-annotated factory methods inside a @BotConfiguration class — not in regular handler methods.

@BotConfiguration
public class Keyboards {

@BotMarkup("confirm")
public ReplyKeyboard confirmKeyboard(BotMarkupContext ctx) {
String action = (String) ctx.getParam("action");
return InlineKeyboardMarkup.builder()
.keyboardRow(List.of(
InlineKeyboardButton.builder().text("Yes").callbackData("yes:" + action).build(),
InlineKeyboardButton.builder().text("No").callbackData("no:" + action).build()
))
.build();
}
}

The markup is requested from a handler via PlainReply.withMarkup:

@BotCommand("/delete")
public PlainReply onDelete() {
return PlainReply.of("Are you sure?")
.withMarkup("confirm", Map.of("action", "delete"));
}

New Parameter Types (0.0.2)

The following type-based and annotation-based parameter resolvers were added in 0.0.2 to support the new update-type handler annotations.

InlineQuery (@BotInlineQuery)

Inject the raw InlineQuery object in handlers annotated with @BotInlineQuery.

@BotInlineQuery
public void onInline(InlineQuery query) {
log.info("Inline query from user {}: {}", query.getFrom().getId(), query.getQuery());
}

ChosenInlineResult (@BotChosenInlineResult)

Inject the ChosenInlineResult object in handlers annotated with @BotChosenInlineResult.

@BotChosenInlineResult
public void onChosen(ChosenInlineResult result) {
log.info("Result '{}' chosen by user {}", result.getResultId(), result.getFrom().getId());
}

ShippingQuery (@BotShippingQuery)

Inject the ShippingQuery object in handlers annotated with @BotShippingQuery.

@BotShippingQuery
public void onShipping(ShippingQuery query) {
log.info("Shipping address: {}", query.getShippingAddress());
}

PreCheckoutQuery (@BotPreCheckoutQuery)

Inject the PreCheckoutQuery object in handlers annotated with @BotPreCheckoutQuery.

@BotPreCheckoutQuery
public void onPreCheckout(PreCheckoutQuery query) {
log.info("Pre-checkout for {} {} from user {}", query.getTotalAmount(), query.getCurrency(), query.getFrom().getId());
}

Poll (@BotPoll)

Inject the Poll object in handlers annotated with @BotPoll.

@BotPoll
public void onPoll(Poll poll) {
log.info("Poll '{}' updated", poll.getQuestion());
}

PollAnswer (@BotPollAnswer)

Inject the PollAnswer object in handlers annotated with @BotPollAnswer.

@BotPollAnswer
public void onPollAnswer(PollAnswer answer) {
log.info("User {} answered poll {}", answer.getUser().getId(), answer.getPollId());
}

ChatMemberUpdated (@BotMyChatMember / @BotChatMemberUpdate)

Inject ChatMemberUpdated in handlers annotated with @BotMyChatMember or @BotChatMemberUpdate.

@BotMyChatMember
public void onBotStatus(ChatMemberUpdated update) {
log.info("Bot status changed: {} → {}", update.getOldChatMember().getStatus(), update.getNewChatMember().getStatus());
}

ChatJoinRequest (@BotChatJoinRequest)

Inject the ChatJoinRequest object in handlers annotated with @BotChatJoinRequest.

@BotChatJoinRequest
public void onJoinRequest(ChatJoinRequest request) {
log.info("Join request from user {}", request.getUser().getId());
}

BusinessConnection (@BotBusinessConnection)

Inject the BusinessConnection object in handlers annotated with @BotBusinessConnection.

@BotBusinessConnection
public void onConnection(BusinessConnection connection) {
log.info("Business connection {} enabled: {}", connection.getId(), connection.getIsEnabled());
}

BusinessMessagesDeleted (@BotDeletedBusinessMessages)

Inject BusinessMessagesDeleted in handlers annotated with @BotDeletedBusinessMessages.

@BotDeletedBusinessMessages
public void onDeleted(BusinessMessagesDeleted deleted) {
log.info("Deleted messages: {}", deleted.getMessageIds());
}

@BotInlineQueryValue

Injects the inline query text string. Only available in @BotInlineQuery handlers.

@BotInlineQuery
public void onInline(@BotInlineQueryValue String queryText, InlineQuery query) {
log.info("Query text: {}", queryText);
}

@BotChosenInlineResultId

Injects the chosen result ID string. Only available in @BotChosenInlineResult handlers.

@BotChosenInlineResult
public void onChosen(@BotChosenInlineResultId String resultId) {
log.info("Chosen result ID: {}", resultId);
}

@BotShippingPayload

Injects the invoice payload string. Only available in @BotShippingQuery handlers.

@BotShippingQuery
public void onShipping(@BotShippingPayload String payload, ShippingQuery query) {
log.info("Invoice payload: {}", payload);
}

@BotPreCheckoutPayload

Injects the invoice payload string. Only available in @BotPreCheckoutQuery handlers.

@BotPreCheckoutQuery
public void onPreCheckout(@BotPreCheckoutPayload String payload, PreCheckoutQuery query) {
log.info("Invoice payload: {}", payload);
}

Locale (core-i18n)

Locale is resolved by BotLocaleArgumentResolver, which is auto-configured only when core-i18n is on the classpath.

@BotCommand("/lang")
public String showLocale(Locale locale) {
return "Your language: " + locale.getDisplayLanguage();
}

Sharing Data Between Filters and Handlers

Both BotRequest and BotResponse carry a generic attribute map that is scoped to a single update processing cycle. This is the idiomatic way to pass computed data from a BotFilter to a handler without a shared Spring service.

// In a BotFilter (runs before handlers):
@Component
@Order(BotFilterOrder.CONTEXT_SETTER + 10)
public class UserEnrichmentFilter implements BotFilter {

private final UserRepository userRepository;

public UserEnrichmentFilter(UserRepository userRepository) {
this.userRepository = userRepository;
}

@Override
public void doFilter(BotRequest request, BotResponse response, BotFilterChain chain) {
User telegramUser = request.getUser();
AppUser appUser = userRepository.findByTelegramId(telegramUser.getId())
.orElseGet(() -> userRepository.save(new AppUser(telegramUser)));
request.setAttribute("appUser", appUser);
chain.doFilter(request, response);
}
}
// Custom resolver that reads the attribute:
@Component
public class AppUserArgumentResolver implements BotArgumentResolver {

@Override
public boolean supportsParameter(Parameter parameter) {
return parameter.getType() == AppUser.class;
}

@Override
public Object resolveArgument(Parameter parameter, BotRequest request, BotResponse response) {
return request.getAttribute("appUser");
}
}
// Handler receives the enriched object directly:
@BotCommand("/profile")
public String profile(AppUser appUser) {
return "Welcome back, " + appUser.getDisplayName();
}

To remove an attribute, set it to null:

request.setAttribute("appUser", null);

Custom Argument Resolvers

For types not covered by the built-in resolvers, implement BotArgumentResolver and register it as a Spring @Bean (or @Component). The framework collects all resolver beans automatically.

@Component
public class PaginationResolver implements BotArgumentResolver {

@Override
public boolean supportsParameter(Parameter parameter) {
// Use ParameterUtils.effectiveType so Optional<PageRequest> is also supported
return ParameterUtils.effectiveType(parameter) == PageRequest.class;
}

@Override
public Object resolveArgument(Parameter parameter, BotRequest request, BotResponse response) {
// Parse a page number stored in callback data, defaulting to page 0
String data = request.getUpdate().getCallbackQuery() != null
? request.getUpdate().getCallbackQuery().getData()
: "";
int page = data.startsWith("page:") ? Integer.parseInt(data.substring(5)) : 0;
return PageRequest.of(page, 10);
}
}

// Use in any handler:
@BotDefaultCallbackQuery
public String onPage(PageRequest page, User user) {
return "Showing page " + page.getPageNumber() + " for " + user.getFirstName();
}

Optional Parameters

Any supported parameter type can be wrapped in Optional<T>. The framework resolves T as normal and wraps the result:

  • Value presentOptional.of(value)
  • No value (resolver returns null) → Optional.empty()
  • No matching resolverOptional.empty()
// Optional.empty() when no user is attached (e.g. channel posts)
@BotCommand("/start")
public String start(Optional<User> user) {
return user.map(u -> "Hello, " + u.getFirstName()).orElse("Hello!");
}

// Optional.empty() when the message carries no text
@BotText("input")
public String onInput(@BotTextValue Optional<String> text) {
return text.orElse("(no text)");
}

// Optional.empty() when the update has no contact
@BotContact
public String savePhone(Optional<Contact> contact) {
return contact.map(c -> "Saved: " + c.getPhoneNumber())
.orElse("No contact shared.");
}

// Works with i18n types too
@BotCommand("/lang")
public String lang(Optional<Locale> locale) {
return locale.map(Locale::getDisplayLanguage).orElse("unknown");
}

Custom resolvers and Optional

If you write a custom BotArgumentResolver that matches by type, use ParameterUtils.effectiveType(parameter) instead of parameter.getType() so that Optional<YourType> parameters are also matched:

@Component
public class AppUserArgumentResolver implements BotArgumentResolver {

@Override
public boolean supportsParameter(Parameter parameter) {
// Handles both AppUser and Optional<AppUser>
return ParameterUtils.effectiveType(parameter) == AppUser.class;
}

@Override
public Object resolveArgument(Parameter parameter, BotRequest request, BotResponse response) {
return request.getAttribute("appUser"); // return null → Optional.empty() is injected automatically
}
}

Annotation-based resolvers (parameter.isAnnotationPresent(...)) require no changes — the annotation is present on the Optional<T> parameter itself and the factory handles the wrapping.


Parameter Order and Null Safety

  • Parameters may be declared in any order — the framework matches them by type and annotation, not position.
  • If a parameter cannot be resolved, null is injected (or Optional.empty() for Optional<T> parameters).
  • Wrap any parameter in Optional<T> to safely handle cases where the value may not be present for a given update type.
@BotCommand("/profile")
public String showProfile(
User user,
Chat chat,
@BotCommandValue String command,
BotRequest request,
TelegramClient client
) {
return user.getFirstName() + " in " + chat.getId() + " via " + command;
}

Next: Learn about chat state management for building multi-step flows.