I have a spring boot webapp, which uses thymeleaf for html templating and as templating engine for emails sent by this webapp, too. In my email I want to use a LocalDateTime, which does not work.
Stacktrace
Caused by: org.springframework.expression.spel.SpelEvaluationException: EL1001E: Type conversion problem, cannot convert from java.time.LocalDateTime to java.lang.String
at org.springframework.expression.spel.support.StandardTypeConverter.convertValue(StandardTypeConverter.java:87)
at org.springframework.expression.common.ExpressionUtils.convertTypedValue(ExpressionUtils.java:57)
at org.springframework.expression.spel.standard.SpelExpression.getValue(SpelExpression.java:369)
at org.thymeleaf.spring6.expression.SPELVariableExpressionEvaluator.evaluate(SPELVariableExpressionEvaluator.java:279)
... 49 common frames omitted
Caused by: org.springframework.core.convert.ConverterNotFoundException: No converter found capable of converting from type [java.time.LocalDateTime] to type [java.lang.String]
at org.springframework.core.convert.support.GenericConversionService.handleConverterNotFound(GenericConversionService.java:288)
at org.springframework.core.convert.support.GenericConversionService.convert(GenericConversionService.java:184)
at org.springframework.expression.spel.support.StandardTypeConverter.convertValue(StandardTypeConverter.java:82)
I already tried using a @Bean with a @Configuration to register the following converter:
public class LocalDateTimeConverter implements Converter<LocalDateTime, String>, ConditionalConverter {
@Bean
public LocalDateTimeConverter ldtConverter() {
return new LocalDateTimeConverter();
}
@Override
public String convert(LocalDateTime source) {
return source.format(DateTimeFormatter.ofPattern("yyy-MM-dd HH:mm:ss"));
}
@Override
public boolean matches(TypeDescriptor sourceType, TypeDescriptor targetType) {
if (sourceType.getType() == LocalDateTime.class && targetType.getType() == String.class ) {
return true;
} else {
return false;
}
}
}
I also tried to register (another) LocalDateTimeConverter2 in the thymeleafTemplateEngine which is directly referenced when sending an email.
public class LocalDateTimeConverter2 extends GenericConversionService
implements ConditionalConverter, ConfigurableConversionService, ConversionService {
LocalDateTimeConverter2() {
}
@Override
public Object convert(Object source, TypeDescriptor sourceType, TypeDescriptor targetType) {
return (Object) (((LocalDateTime) source).format(DateTimeFormatter.ofPattern("yyy-MM-dd HH:mm:ss")));
}
@Override
public boolean matches(TypeDescriptor sourceType, TypeDescriptor targetType) {
if (sourceType.getType() == LocalDateTime.class && targetType.getType() == String.class) {
return true;
} else {
return false;
}
}
public static <t> boolean matches(Class<? extends Object> class1, Class<t> targetClass) {
if (class1.equals(LocalDateTime.class) && targetClass.equals(String.class)) {
return true;
} else {
return false;
}
}
}
Therefore a ConversionService is used (according to the documentation), which falls back to the default ConfigurationService, when it does not match LocalDateTime & String.
@Configuration
public class MyLocalDateTimeConversionService implements IStandardConversionService {
GenericConversionService myConverter = LocalDateTimeConverter2();
StandardConversionService standardConversionService = new StandardConversionService();
@Override
public <T> T convert(IExpressionContext context, Object object, Class<T> targetClass) {
if (LocalDateTimeConverter2.matches(object.getClass(), targetClass)) {
return myConverter.convert(object.getClass(), targetClass);
} else {
return standardConversionService.convert(context, object, targetClass);
}
}
private GenericConversionService LocalDateTimeConverter2() {
return new LocalDateTimeConverter2();
}
}
The previously defined TemplateEngine is used like this. When I replace the LocalDateTime with a String, the following code works and sends emails:
public class TemplateResolver {
@Bean
public ITemplateResolver thymeleafTemplateResolver() {
ClassLoaderTemplateResolver templateResolver = new ClassLoaderTemplateResolver();
templateResolver.setResolvablePatterns(Collections.singleton("html/*"));
templateResolver.setSuffix(".html");
templateResolver.setTemplateMode("HTML");
templateResolver.setCharacterEncoding("UTF-8");
templateResolver.setCacheable(false);
return templateResolver;
}
@Bean
public SpringTemplateEngine thymeleafTemplateEngine(ITemplateResolver templateResolver) {
SpringTemplateEngine templateEngine = new SpringTemplateEngine();
Set<IDialect> dialects = templateEngine.getDialects();
StandardDialect standardDialect = (StandardDialect) dialects.iterator().next();
IStandardConversionService conversionService = new MyLocalDateTimeConversionService();
standardDialect.setConversionService(conversionService);
templateEngine.setTemplateResolver(templateResolver);
return templateEngine;
}
}
The actual email sending process is implemented like this
@Service
public class EmailService {
@Autowired
private JavaMailSender mailSender;
@Autowired
private EmailServiceConfig emailServiceConfig;
@Autowired
TemplateEngine templateEngine;
public void sendHtmlEmail(String subject, LocalDateTime date) {
Context ctx = new Context();
ctx.setVariable("date", date);
MimeMessage mimeMessage = mailSender.createMimeMessage();
MimeMessageHelper message = new MimeMessageHelper(mimeMessage, "UTF-8");
try {
message.setFrom(emailServiceConfig.getFrom());
message.setTo(emailServiceConfig.getTo());
message.setSubject(subject);
String htmlContent = this.templateEngine.process("mail/default.html", ctx);
message.setText(htmlContent, true);
mailSender.send(mimeMessage);
} catch (MessagingException e) {
e.printStackTrace();
}
}
}
Any ideas what I am missing, did wrong or how it is done properly? After all, conceptionally, it should not be so difficult to parse a datetime.
For sake of completeness of a minimum reproducable sample:
public class Helper {
@Autowired
private EmailService mailer;
@Scheduled(fixedRate = 15 * 1000)
public void schedule() {
sendAMail("Test", LocalDateTime.now());
}
private void sendAMail(String subject, LocalDateTime date) {
mailer.sendHtmlEmail(subject, date);
}
}
And finally the "main" method
@SpringBootApplication
@Configuration
@EnableScheduling
public class Webapp {
public static void main(String[] args) {
SpringApplication.run(SosApplication.class, args);
}
}
Using the information of another question did not really help. However the answer is in fact ten years old.
The @EnableWebMvc annontation, which is refered to being a possible solution, is roughly seven years old (SpringFramework Web Servlet's most recent release was 2018) and not supported by Spring Boot.
EDIT: I now found out, that I made a mistake in my template. Instead of ${date} I used ${{date}} (which only works for strings). With that fixed, it kind of works. However I may still want to change the format of the date from 2025-08-25T19:42:58.836682488 to something different. Therefore a ConversionService may be helpful or is there some other way?
#temporalsexpression to format java.time classes. Not the spring conversion service.