Repository navigation
Description
It seems that custom "Converter<S, T>"-Beans are not registered correctly when running a SpringBoot-Application and SpringShell. I have attached a full working project with test-cases demonstrating the problem: shell.zip
In the previous 3.x-line of SpringShell this was not a issue.
In a nutshell:
We have commands in the form:
@Command(name = "custom")
public void custom(
@Option(longName = "msg", required = true) Message msg) {
System.out.println(msg.toString());
}In a Component we define a new Bean implementiung the Converter-Interface. This is initialized from SpringBoot before running the actual command - and yet the Converter cannot be found from the CommandExecutor:
org.springframework.shell.core.command.CommandExecutionException: Unable to execute command custom
at org.springframework.shell.core.command.CommandExecutor.execute(CommandExecutor.java:74) ~[spring-shell-core-4.0.2.jar:4.0.2]
at org.springframework.shell.core.NonInteractiveShellRunner.executeCommand(NonInteractiveShellRunner.java:158) ~[spring-shell-core-4.0.2.jar:4.0.2]
at org.springframework.shell.core.NonInteractiveShellRunner.run(NonInteractiveShellRunner.java:95) ~[spring-shell-core-4.0.2.jar:4.0.2]
at org.springframework.shell.core.autoconfigure.ShellRunnerAutoConfiguration.lambda$springShellApplicationRunner$0(ShellRunnerAutoConfiguration.java:39) ~[spring-shell-core-autoconfigure-4.0.2.jar:4.0.2]
at org.springframework.boot.SpringApplication.lambda$callRunner$0(SpringApplication.java:788) ~[spring-boot-4.0.6.jar:4.0.6]
at org.springframework.util.function.ThrowingConsumer$1.acceptWithException(ThrowingConsumer.java:82) ~[spring-core-7.0.7.jar:7.0.7]
at org.springframework.util.function.ThrowingConsumer.accept(ThrowingConsumer.java:60) ~[spring-core-7.0.7.jar:7.0.7]
at org.springframework.util.function.ThrowingConsumer$1.accept(ThrowingConsumer.java:86) ~[spring-core-7.0.7.jar:7.0.7]
at org.springframework.boot.SpringApplication.callRunner(SpringApplication.java:800) ~[spring-boot-4.0.6.jar:4.0.6]
at org.springframework.boot.SpringApplication.callRunner(SpringApplication.java:788) ~[spring-boot-4.0.6.jar:4.0.6]
at org.springframework.boot.SpringApplication.lambda$callRunners$0(SpringApplication.java:776) ~[spring-boot-4.0.6.jar:4.0.6]
at java.base/java.util.stream.ForEachOps$ForEachOp$OfRef.accept(ForEachOps.java:184) ~[na:na]
at java.base/java.util.stream.SortedOps$SizedRefSortingSink.end(SortedOps.java:357) ~[na:na]
at java.base/java.util.stream.AbstractPipeline.copyInto(AbstractPipeline.java:510) ~[na:na]
at java.base/java.util.stream.AbstractPipeline.wrapAndCopyInto(AbstractPipeline.java:499) ~[na:na]
at java.base/java.util.stream.ForEachOps$ForEachOp.evaluateSequential(ForEachOps.java:151) ~[na:na]
at java.base/java.util.stream.ForEachOps$ForEachOp$OfRef.evaluateSequential(ForEachOps.java:174) ~[na:na]
at java.base/java.util.stream.AbstractPipeline.evaluate(AbstractPipeline.java:234) ~[na:na]
at java.base/java.util.stream.ReferencePipeline.forEach(ReferencePipeline.java:596) ~[na:na]
at org.springframework.boot.SpringApplication.callRunners(SpringApplication.java:776) ~[spring-boot-4.0.6.jar:4.0.6]
at org.springframework.boot.SpringApplication.run(SpringApplication.java:328) ~[spring-boot-4.0.6.jar:4.0.6]
at org.springframework.boot.SpringApplication.run(SpringApplication.java:1365) ~[spring-boot-4.0.6.jar:4.0.6]
at org.springframework.boot.SpringApplication.run(SpringApplication.java:1354) ~[spring-boot-4.0.6.jar:4.0.6]
at shell.SpringShellApplication.main(SpringShellApplication.java:14) ~[classes/:na]
Caused by: org.springframework.core.convert.ConverterNotFoundException: No converter found capable of converting from type [java.lang.String] to type [shell.SpringShellApplication$Message]
at org.springframework.core.convert.support.GenericConversionService.handleConverterNotFound(GenericConversionService.java:289) ~[spring-core-7.0.7.jar:7.0.7]
at org.springframework.core.convert.support.GenericConversionService.convert(GenericConversionService.java:184) ~[spring-core-7.0.7.jar:7.0.7]
at org.springframework.core.convert.support.GenericConversionService.convert(GenericConversionService.java:165) ~[spring-core-7.0.7.jar:7.0.7]
at org.springframework.shell.core.command.adapter.MethodInvokerCommandAdapter.prepareArguments(MethodInvokerCommandAdapter.java:153) ~[spring-shell-core-4.0.2.jar:4.0.2]
at org.springframework.shell.core.command.adapter.MethodInvokerCommandAdapter.doExecute(MethodInvokerCommandAdapter.java:93) ~[spring-shell-core-4.0.2.jar:4.0.2]
at org.springframework.shell.core.command.AbstractCommand.execute(AbstractCommand.java:166) ~[spring-shell-core-4.0.2.jar:4.0.2]
at org.springframework.shell.core.command.CommandExecutor.execute(CommandExecutor.java:71) ~[spring-shell-core-4.0.2.jar:4.0.2]
... 23 common frames omitted
From my point of view this should be possible as it is standard Spring-Behaviour to support "Converters".
Additional note: When registering the Command programmitically the Converters are respected. It seems to me, that the Building-process of the Commands via Annotations is the problem.
@Bean
org.springframework.shell.core.command.Command programmatic() {
Consumer<CommandContext> action = ctx ->
System.out.println(ctx.getOptionByName("msg"));
return org.springframework.shell.core.command.Command
.builder()
.name("programmatic")
.options(
CommandOption.with()
.longName("msg")
.type(Message.class).build())
.execute(action);
}
``
Activity
[-]Custom "Converters" not working at all[/-][+]Custom "Converters" not working with @Command-Annotations[/+]czpilar commentedon May 19, 2026
Fixed in PR #1353
When no
ConfigurableConversionServicebean is definedCommandFactoryBeanfalls back to a freshDefaultConversionService. That fallback now also harvestsConverter,GenericConverterandConverterFactorybeans from the application context and registers them on the new service. ForConverter<S, T>declared as a@Beanlambda, the runtime class loses its generics, so source/target are resolved from the merged bean definition'sResolvableTypeand registered via the typedaddConverter(source, target, converter)overload.tschoellhorn commentedon May 19, 2026
Thanks that you could look into the issue. Is there a workaround I could use in the meantime (before the PR is merged into the main line) ?
czpilar commentedon May 19, 2026
Here's a workaround you can add into your application until the fix lands - it replicates the same auto-harvesting logic from the PR as a user-defined
ConfigurableConversionServicebean, so your existing@Bean Converter<String, Message>lambda from the reproducer keeps working unchanged:With registered bean:
Without registered bean:

tschoellhorn commentedon May 19, 2026
Thanks for the workaround. It works great in the testases I provided.
Yet, in the larger project I try to upgrade, the Converter-Beans are more complicated and have dependencies to the database. Thus, they are initialized after these ConversionService and thus have not been published to the ApplicationContext. I tried to remedy that with @dependsOn - but in that case I receive a circular definition:
Now, this seems a general issue which might affect also your fix. I did not find an easy way to include that case in the tests. Any ideas how to proceed?
tschoellhorn commentedon May 19, 2026
I could provide a more complex and realistic use case including tests. The main difference is that the Converter-Bean itself depends on another bean (in that case JdbcTemplate). Now it does not get registered with this solution. I guess the real fix will have the same problems. Please find attached the updated test-cases. It has nothing to do with lambda-expressions.
shell.zip
czpilar commentedon May 19, 2026
I tried the reproducer against the PR (4.0.3-SNAPSHOT), removed the workaround first - all three tests pass, even with
MessageConverteras a@Componentthat@AutowiredsJdbcTemplate.About the cycle: it isn't caused by the harvesting logic itself, it's caused by the bean's name and type. A bean named
conversionServiceof typeConfigurableConversionServiceis also picked up by Spring as the conversion service for@ConfigurationPropertiesbinding. SoSqlInitializationPropertiesneedsconversionService,conversionServiceneedsMessageConverter,MessageConverterneedsJdbcTemplate,JdbcTemplateneedsDataSource,DataSourceneeds the SQL initializer, the SQL initializer needsSqlInitializationProperties-> loop closed.The fix doesn't trigger this because its
DefaultConversionServiceis a plain local variable insideCommandFactoryBean#getConfigurableConversionService, never registered as a bean. Spring's property binding keeps using its own default. By the timeCommandRegistryis being created and our harvesting runs,DataSourceandJdbcTemplatealready exist, soMessageConverterinstantiates without trouble.If you want to keep using a workaround until the PR is merged to the main and released, just rename the bean - e.g.
@Bean("shellConversionService") ConfigurableConversionService β¦.CommandFactoryBeanlooks it up by type so it still finds it, but Spring's property binder only looks for the literal nameconversionService, so the cycle breaks.tschoellhorn commentedon May 20, 2026
Thanks for the "fix" of the fix. I did not catch the loop myself - sorry. Now I am confident that your fix also will actually close the issue. Hope your fix will be integrated soon.
czpilar commentedon May 20, 2026
@tschoellhorn No problem. Iβm glad to help this project with my contributions, as we use it in our company projects and I also use it in my personal projects.
Fix user-defined Converter beans were ignored by @Command-annotated mβ¦