Skip to content

Instantly share code, notes, and snippets.

@matchilling
Created October 14, 2025 09:43
Show Gist options
  • Select an option

  • Save matchilling/8503466814cc9980a9bb8b8dc215d433 to your computer and use it in GitHub Desktop.

Select an option

Save matchilling/8503466814cc9980a9bb8b8dc215d433 to your computer and use it in GitHub Desktop.
Converter that transforms string values into Java Instant} objects

This converter is useful in Spring Boot applications for several common scenarios:

Web Form Handling: When processing HTML forms with datetime-local inputs or date pickers, this converter automatically transforms user-submitted date strings into Instant objects for your controller methods.

REST API Parameter Binding: For REST endpoints that accept date parameters as query strings or path variables, it enables automatic conversion from various string formats to Instant without manual parsing.

Configuration Properties: When binding application properties from YAML/properties files that contain date values, Spring can use this converter to map them directly to Instant fields in @ConfigurationProperties classes.

Data Transfer Objects: In DTOs where you need flexible date format support, this converter allows clients to send dates in multiple formats while your application consistently works with Instant objects.

Database Integration: When working with JPA entities or database queries that need to convert string-based date filters into proper temporal types for database operations.

The converter's multi-format support makes it particularly valuable in applications that need to handle dates from different sources (web forms, APIs, configuration files) or support international date formats while maintaining a consistent internal representation using Instant.

package com.matchilling.websupport.converter;
import java.time.Instant;
import java.time.LocalDate;
import java.time.LocalDateTime;
import java.time.ZoneId;
import java.time.format.DateTimeFormatter;
import java.util.List;
import java.util.Optional;
import java.util.function.Function;
import lombok.RequiredArgsConstructor;
import org.springframework.core.convert.converter.Converter;
/**
* Converter that transforms string values into {@link Instant} objects.
*
* <p>This converter supports multiple date and time formats, attempting to parse
* the input string with each format in order until finding a compatible one:</p>
*
* <ul>
* <li>{@code ISO_INSTANT} - ISO-8601 format with UTC timezone (e.g., "2023-12-25T10:30:00Z")</li>
* <li>{@code ISO_DATE_TIME} - ISO-8601 format without timezone (e.g., "2023-12-25T10:30:00")</li>
* <li>{@code ISO_DATE} - ISO-8601 date only (e.g., "2023-12-25")</li>
* <li>{@code yyyy-MM-dd'T'HH:mm} - HTML datetime-local format (e.g., "2023-12-25T10:30")</li>
* </ul>
*
* <p>For formats that don't include an explicit timezone, the timezone configured
* in the constructor is used. Dates without a time component are converted to
* the start of day (00:00:00) in the specified timezone.</p>
*
* <p>If no format is compatible with the input string, the converter returns
* {@code null}.</p>
*
* @author matchilling
* @see Converter
* @see Instant
* @see ZoneId
* @since 1.0
*/
@RequiredArgsConstructor
public class InstantConverter implements Converter<String, Instant> {
private final ZoneId zoneId;
@Override
public <U> Converter<String, U> andThen(Converter<? super Instant, ? extends U> after) {
return Converter.super.andThen(after);
}
/**
* Convert the source String to an Instant.
*
* @param source
* @return the converted Instant, or null if conversion fails
*/
@Override
public Instant convert(String source) {
for (var fn : functions()) {
try {
var result = fn.apply(source);
if (result.isPresent()) {
return result.get();
}
} catch (Exception _) {
}
}
return null;
}
/**
*
* @return a list of functions that attempt to parse a String into an Instant
*/
private List<Function<String, Optional<Instant>>> functions() {
return List.of(
source -> Optional.of(source).map(Instant::parse),
source -> Optional.of(source).map(DateTimeFormatter.ISO_DATE_TIME::parse).map(it -> LocalDate.from(it).atStartOfDay().atZone(zoneId).toInstant()),
source -> Optional.of(source).map(DateTimeFormatter.ISO_DATE::parse).map(it -> LocalDate.from(it).atStartOfDay().atZone(zoneId).toInstant()),
source -> Optional.of(source).map(it -> DateTimeFormatter.ofPattern("yyyy-MM-dd'T'HH:mm").parse(it)).map(it -> LocalDateTime.from(it).atZone(zoneId).toInstant()));
}
}
package com.matchilling.websupport.converter;
import java.time.Instant;
import java.time.ZoneId;
import org.junit.jupiter.api.Assertions;
import org.junit.jupiter.api.Nested;
import org.junit.jupiter.params.ParameterizedTest;
import org.junit.jupiter.params.provider.CsvSource;
class InstantConverterTest {
private final static ZoneId ZONE_ID = ZoneId.of("UTC");
private final InstantConverter subject = new InstantConverter(ZONE_ID);
@Nested
class ConvertTest {
@ParameterizedTest(name = "{0} should be converted to {1}")
@CsvSource({
// ISO_DATE
"1970-01-01, 1970-01-01T00:00:00Z",
"1970-01-01+01:00, 1970-01-01T00:00:00Z",
// ISO_DATE_TIME
"1970-01-01T01:00:00, 1970-01-01T00:00:00Z",
// web datetime-local
"1970-01-01T00:00, 1970-01-01T00:00:00Z",
// Default ISO_INSTANT
"1970-01-01T00:00:00Z, 1970-01-01T00:00:00Z",
})
void shouldConvert(String source, String expected) {
// when:
var actual = subject.convert(source);
// then:
Assertions.assertEquals(Instant.parse(expected), actual);
}
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment