2

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?

3
  • 1
    I don't see why you would need a converter in the first place. Use the the #temporals expression to format java.time classes. Not the spring conversion service. Commented Aug 25 at 15:09
  • After reading the documention [1], #temporals is a good solution. But there is the (constructed) edge case where there are many dates in a template. There a ConversionService may be another option. [1] thymeleaf.org/doc/tutorials/3.1/… Commented Aug 25 at 17:46
  • Your template shouild do the formatting not a converter. Also your premise "it should not be so difficult to parse a datetime" is so wrong, if there is something difficult it is parsing dates, times and combinations of those. Add timezones and you are in for a treat. So add the formatting to the template not in a conversion service. Commented Aug 26 at 5:45

0

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.