Search code examples
spring-mvcspring-bootjava-timedatetime-parsingzoneddatetime

Spring Boot parse form data to ZonedDateTime (dd/MM/yyyy HH:mm)


I have an entity which has java.time.ZonedDateTime property. I want to parse a string submitted from form in dd/MM/yyyy HH:mm format to java.time.ZonedDateTime.

I tried @DateTimeFormat(pattern = "dd/MM/yyyy HH:mm"). But it works only LocalDate with @DateTimeFormat(pattern = "dd/MM/yyyy") format.

I also created a Converter as below

public class ZonedDateTimeConverter implements Converter<String, ZonedDateTime> {

    private static final DateTimeFormatter DATE_TIME_FORMATTER = DateTimeFormatter.ofPattern("dd/MM/yyyy HH:mm");

    private final ZoneId zoneId;

    public ZonedDateTimeConverter(ZoneId zoneId) {
        this.zoneId = zoneId;
    }

    @Override
    public ZonedDateTime convert(String source) {
        LOG.info("Parsing string {} to ZonedDateTime: {}", source, ZonedDateTime.parse(source, DATE_TIME_FORMATTER));
        return ZonedDateTime.parse(source, DATE_TIME_FORMATTER);
    }
}

and overwriting webMvcConfiguration as below

@Configuration
public class WebMvcConfiguration extends WebMvcConfigurerAdapter {
    @Override
    public void addFormatters(FormatterRegistry registry) {
        registry.addConverter(new ZonedDateTimeConverter(ZoneId.systemDefault()));
    }
}

I'm not sure what is the right way to make it work. Using Spring Boot 1.5.6. Thymeleaf 3.0.7


Solution

  • Your DateTimeFormatter's pattern has only the date (dd/MM/yyyy) and time (HH:mm), but a ZonedDateTime also needs a timezone to be parsed directly.

    As the input doesn't have it, one alternative is to first parse the input to a java.time.LocalDateTime and then convert it to a ZoneDateTime, using the zoneId that you already have:

    public ZonedDateTime convert(String source) {
        // parse to LocalDateTime
        LocalDateTime dt = LocalDateTime.parse(source, DATE_TIME_FORMATTER);
        // convert to ZonedDateTime
        return dt.atZone(this.zoneId);
    }
    

    Another alternative is to set the timezone in the formatter, so you can parse directly to a ZonedDateTime. But in this case, I also suggest you a refactor: make the formatter a field of your converter class (instead of a static field), because it'll depend on the ZoneId you pass in the constructor.

    public class ZonedDateTimeConverter implements Converter<String, ZonedDateTime> {
    
        private final DateTimeFormatter formatter;
    
        public ZonedDateTimeConverter(ZoneId zoneId) {
            // set the zone in the formatter
            this.formatter = DateTimeFormatter.ofPattern("dd/MM/yyyy HH:mm").withZone(zoneId);
        }
    
        @Override
        public ZonedDateTime convert(String source) {
            // now the formatter has a zone set, so I can parse directly to ZonedDateTime
            return ZonedDateTime.parse(source, this.formatter);
        }
    }
    

    Just a note about using ZoneId.systemDefault(). This method gets the JVM's default timezone, but keep in mind that it can be changed without notice, even at runtime, so it's better to always specify which timezone you want to use.

    The API uses IANA timezones names (always in the format Continent/City, like Asia/Kolkata or Europe/Berlin). Then you create the timezone using of method, such as ZoneId.of("Asia/Kolkata").

    Avoid using the 3-letter abbreviations (like IST or PST) because they are ambiguous and not standard. Some of them might work (usually due to retro-compatibility reasons), but it's not guaranteed.

    You can get a list of all available timezones (and choose the best for your case) with ZoneId.getAvailableZoneIds().