Skip to content

Jsr310TimestampBasedConverters cannot be overriden via user converters in JdbcCustomConversions #2292

@ThomasVitale

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
No fields configured for issues without a type.

Projects

No projects

Milestone

No milestone

Relationships

None yet

Development

No branches or pull requests

Issue actions