spring-projects/spring-ai · warning
Method is annotated with @Tool but returns a functional…
Error message
Method ${toolMethod.getName()} is annotated with @Tool but returns a functional type. This is not supported and the method will be ignored. What it means
MethodToolCallbackProvider skips @Tool-annotated methods whose return type is a functional type (Function, Supplier, Consumer), logging this warning. The Spring AI tool API expects tool methods to return plain data; returning a lambda/functional interface is unsupported, so the method is ignored and will not become a ToolCallback.
Solutions
- Change the @Tool method to return a concrete data type and execute the function inside the method body.
- If you need function-style tools, keep using the FunctionCallback/ToolCallback API instead of @Tool.
- Split the code: keep the lambda private, expose a @Tool method that calls it and returns the result.
Example fix
// before
@Tool(description="Get weather")
Function<String, String> getWeather() {
return city -> weatherService.fetch(city);
}
// after
@Tool(description="Get weather")
String getWeather(String city) {
return weatherService.fetch(city);
} Defensive patterns
Strategy: validation
Validate before calling
for (Method m : clazz.getDeclaredMethods()) {
if (m.isAnnotationPresent(Tool.class) &&
(Function.class.isAssignableFrom(m.getReturnType()) ||
Supplier.class.isAssignableFrom(m.getReturnType()) ||
Consumer.class.isAssignableFrom(m.getReturnType()))) {
throw new IllegalStateException("@Tool method returns functional type: " + m.getName());
}
} Prevention
- @Tool methods must return plain data types, never Function/Supplier/Consumer.
- When migrating legacy FunctionCallback tools, inline the function logic into the method body.
- Scan your tool classes in CI to catch functional return types early.
- Call toolCallbacks on the provider in a startup test so skipped methods surface as count mismatches.
When it happens
Trigger: Building tool callbacks from a class (e.g. ToolCallbacks.from(obj) or bean-based scanning) where a @Tool method returns Function/Supplier/Consumer; detected in isFunctionalType, consulted by toolMethods and toolCallbacks.
Common situations: Refactoring a Function-based (legacy FunctionCallback) tool into a @Tool method without changing the return type; wrapping logic in a Supplier for laziness; migration from old tool API styles.
Understand the failure class
Background: UnsupportedOperationException and "is not supported" errors: when a library deliberately refuses a call — this error's family across 30 libraries.
Related errors
- Conversion from JSON failed
- Currently only methods with a single CreateMessageRequest…
- Currently only one tool call is supported per message!
- Currently only one tool call is supported per message!
- Gemini doesn't support message type:
AI-assisted analysis of spring-projects/spring-ai@98a7beda4f (2026-09-11).
Data as JSON: /api/errors/0c20fbb3c65ac962.
Report an issue: GitHub.
Appendix: source
Thrown at spring-ai-model/src/main/java/org/springframework/ai/tool/method/MethodToolCallbackProvider.java:117
.toolCallResultConverter(ToolUtils.getToolCallResultConverter(toolMethod))
.build())
.toArray(ToolCallback[]::new))
.flatMap(Stream::of)
.toArray(ToolCallback[]::new);
validateToolCallbacks(toolCallbacks);
return toolCallbacks;
}
private boolean isFunctionalType(Method toolMethod) {
var isFunction = ClassUtils.isAssignable(Function.class, toolMethod.getReturnType())
|| ClassUtils.isAssignable(Supplier.class, toolMethod.getReturnType())
|| ClassUtils.isAssignable(Consumer.class, toolMethod.getReturnType());
if (isFunction) {
if (logger.isWarnEnabled()) {
logger.warn("Method " + toolMethod.getName() + "is annotated with @Tool but returns a functional type. "
+ "This is not supported and the method will be ignored.");
}
}
return isFunction;
}
private boolean isToolAnnotatedMethod(Method method) {
Tool annotation = AnnotationUtils.findAnnotation(method, Tool.class);
return Objects.nonNull(annotation);
}
private void validateToolCallbacks(ToolCallback[] toolCallbacks) {
List<String> duplicateToolNames = ToolUtils.getDuplicateToolNames(toolCallbacks);
if (!duplicateToolNames.isEmpty()) {
throw new IllegalArgumentException("Multiple tools with the same name (%s) found in sources: %s".formatted(
String.join(", ", duplicateToolNames),
this.toolObjects.stream().map(o -> o.getClass().getName()).collect(Collectors.joining(", "))));View on GitHub (pinned to 98a7beda4f)