Registering a user @WritingConverter from Instant (or LocalDate / LocalDateTime) to String has no effect at write time. The column is written as Timestamp despite the user converter being registered.
CustomConversions.collectPotentialConverterRegistrations adds user converters before store converters into the writingPairs LinkedHashSet, so insertion order correctly prioritises the user converter.
However, MappingRelationalConverter.determineCustomWriteTarget calls the two-arg getCustomWriteTarget(Instant.class, Timestamp.class) first (the Timestamp target comes from JdbcColumnTypes). That path short-circuits on a direct pairs.contains(new ConvertiblePair(Instant, Timestamp)) check, finding the store converter's pair and returning Timestamp without ever checking whether a user converter exists for the same source type.
Expected behaviour
A user-registered Instant → String converter takes priority. getCustomWriteTarget(Instant.class, Timestamp.class) should return empty when the user has already registered a converter for Instant to a different target type.
Actual behaviour
getCustomWriteTarget(Instant.class, Timestamp.class) returns Timestamp. The one-arg getCustomWriteTarget(Instant.class) is never reached, even though it would correctly return String.
The bug is reproducible directly:
JdbcCustomConversions conversions = new JdbcCustomConversions(List.of(InstantToStringConverter.INSTANCE));
assertThat(conversions.getCustomWriteTarget(Instant.class)).hasValue(String.class); // passes
assertThat(conversions.getCustomWriteTarget(Instant.class, Timestamp.class)).isEmpty(); // fails — returns Timestamp
where:
@WritingConverter
enum InstantToStringConverter implements Converter<Instant, String> {
INSTANCE;
@Override
public String convert(Instant source) {
return source.toString();
}
}
Same issue applies to LocalDate and LocalDateTime.
Workaround
Strip @WritingConverter entries from the store converters when constructing JdbcCustomConversions, so the conflicting ConvertiblePair(Instant, Timestamp) is never added to writingPairs:
Filter out @WritingConverter entries from storeConverters() before constructing StoreConversions, keeping only @ReadingConverter entries:
List<Object> readOnlyStoreConverters = JdbcCustomConversions.storeConverters().stream()
.filter(c -> c.getClass().isAnnotationPresent(ReadingConverter.class))
.toList();
JdbcCustomConversions conversions = new JdbcCustomConversions(
CustomConversions.StoreConversions.of(SimpleTypeHolder.DEFAULT, readOnlyStoreConverters),
List.of(InstantToStringConverter.INSTANCE));
assertThat(conversions.getCustomWriteTarget(Instant.class)).hasValue(String.class); // passes
assertThat(conversions.getCustomWriteTarget(Instant.class, Timestamp.class)).isEmpty(); // passes
This is a necessary escape hatch for any store that might not use Timestamp to represent java.time types (e.g. SQLite).
Registering a user
@WritingConverterfromInstant(orLocalDate/LocalDateTime) toStringhas no effect at write time. The column is written asTimestampdespite the user converter being registered.CustomConversions.collectPotentialConverterRegistrationsadds user converters before store converters into thewritingPairsLinkedHashSet, so insertion order correctly prioritises the user converter.However,
MappingRelationalConverter.determineCustomWriteTargetcalls the two-arggetCustomWriteTarget(Instant.class, Timestamp.class)first (theTimestamptarget comes fromJdbcColumnTypes). That path short-circuits on a directpairs.contains(new ConvertiblePair(Instant, Timestamp))check, finding the store converter's pair and returningTimestampwithout ever checking whether a user converter exists for the same source type.Expected behaviour
A user-registered
Instant→Stringconverter takes priority.getCustomWriteTarget(Instant.class, Timestamp.class)should return empty when the user has already registered a converter forInstantto a different target type.Actual behaviour
getCustomWriteTarget(Instant.class, Timestamp.class)returnsTimestamp. The one-arggetCustomWriteTarget(Instant.class)is never reached, even though it would correctly returnString.The bug is reproducible directly:
where:
Same issue applies to
LocalDateandLocalDateTime.Workaround
Strip
@WritingConverterentries from the store converters when constructingJdbcCustomConversions, so the conflictingConvertiblePair(Instant, Timestamp)is never added towritingPairs:Filter out
@WritingConverterentries fromstoreConverters()before constructingStoreConversions, keeping only@ReadingConverterentries:This is a necessary escape hatch for any store that might not use
Timestampto representjava.timetypes (e.g. SQLite).