Skip to content

Jsr310TimestampBasedConverters cannot be overriden via user converters in JdbcCustomConversions #2292

Description

@ThomasVitale

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 InstantString 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).

Metadata

Metadata

Labels

Type

No type

Projects

No projects

Milestone

No milestone

Relationships

None yet

Development

No branches or pull requests

Issue actions