1
main() {
StudentDataDto source= new StudentDataDto();
    studentDataDto.setCreatedAt("2022-01-20T11:12:46");
    StudentMetaDataEntity destination= modelMapper.map(studentDataDto,
        StudentMetaDataEntity.class);
}

StudentDataDto {
private String createdAt;
}

StudentMetaDataEntity {
private Timestamp createdAt; (java.sql.Timestamp)
}

Exception message:

org.modelmapper.MappingException: ModelMapper mapping errors:

1) Converter org.modelmapper.internal.converter.DateConverter@2b08772d failed to convert java.lang.String to java.sql.Timestamp.
Caused by: org.modelmapper.MappingException: ModelMapper mapping errors:

1) String must be in JDBC format [yyyy-MM-dd HH:mm:ss.fffffffff] to create a java.sql.Timestamp

1 error
    at org.modelmapper.internal.Errors.toMappingException(Errors.java:258)
    at org.modelmapper.internal.converter.DateConverter.dateFor(DateConverter.java:125)
    at org.modelmapper.internal.converter.DateConverter.convert(DateConverter.java:70)
    at org.modelmapper.internal.converter.DateConverter.convert(DateConverter.java:53)
    at org.modelmapper.internal.MappingEngineImpl.convert(MappingEngineImpl.java:306)
    at org.modelmapper.internal.MappingEngineImpl.map(MappingEngineImpl.java:109)
    at org.modelmapper.internal.MappingEngineImpl.setDestinationValue(MappingEngineImpl.java:245)
    at org.modelmapper.internal.MappingEngineImpl.propertyMap(MappingEngineImpl.java:187)
    at org.modelmapper.internal.MappingEngineImpl.typeMap(MappingEngineImpl.java:151)
    at org.modelmapper.internal.MappingEngineImpl.map(MappingEngineImpl.java:114)
    at org.modelmapper.internal.MappingEngineImpl.map(MappingEngineImpl.java:71)
    at org.modelmapper.ModelMapper.mapInternal(ModelMapper.java:573)
    at org.modelmapper.ModelMapper.map(ModelMapper.java:406)
...

By referring to this similar question's answers, I understand that the source code of model mapper restrict the timestamp string formats.

My question: Instead of changing my StudentDataDto property type to java.sql.Timestamp, is it possible that I keep my desired timestamp format in yyyy-MM-dd'T'HH:mm:ss and customize my modelmapper converter to solve the exception?

Yu Tian Toby
  • 209
  • 1
  • 4
  • 11
  • (A) The `Timestamp` class is one of the terrible date-time classes that were years ago supplanted by the modern *java.time* classes. Replaced by `Instant` generally, except for database exchange use `OffsetDateTime`. (B) You are ignoring the crucial issue of time zone or offset-from-UTC. Your input lacks an indicator or zone/offset. So it does not fit `OffsetDateTime`/`Instant`, nor does it fit `Timestamp`. If that input string is meant to represent a moment as seen in UTC, append a `Z`, per the [ISO 8601](https://en.m.wikipedia.org/wiki/ISO_8601) standard: `"2022-01-20T11:12:46Z"`. – Basil Bourque Feb 25 '22 at 17:00
  • @BasilBourque `java.sql.Timestamp` was replaced by `java.time.LocalDateTime`, not `Instant`. JDBC does not define support for `Instant` (only for `LocalDate`, `LocalTime`, `LocalDateTime`, `OffsetTime` and `OffsetDateTime`. – Mark Rotteveel Feb 25 '22 at 17:57
  • @MarkRotteveel Incorrect. The [Javadoc for `Timestamp`](https://docs.oracle.com/en/java/javase/17/docs/api/java.sql/java/sql/Timestamp.html) says that class is “a composite of a `java.util.Date` and a separate nanoseconds value”. And `java.util.Date` represents a moment as seen in UTC, with its modern replacement being `java.time.Instant`. So `LocalDateTime` has no part in this. As for JDBC, as I noted, the appropriate class to represent a moment when exchanging with a database is `OffsetDateTime`. – Basil Bourque Feb 25 '22 at 19:59
  • @BasilBourque Although `java.sql.Timestamp` is based on `java.util.Date`, it doesn't represent a UTC value. It represents a SQL `TIMESTAMP` which is a data type without time zone information, and when converting to a SQL `TIMESTAMP`, the default JVM time zone is used to determine the timestamp value (the same value as produced by its `toString()`). This is also the reason that `Timestamp` has methods `toLocalDateTime()` and `valueOf(LocalDateTime)`. – Mark Rotteveel Feb 25 '22 at 20:08

1 Answers1

1

You just need to write you own converter and register it with ModelMapper instance.

  1. Option 1 - more general. If your date string will always be in this format, you can write a converter from String to java.sql.Timestamp, so it will always be applied even when using other dtos.
public class StringToTimestampConverter implements Converter<String, Timestamp> {

        private final DateTimeFormatter formatter = DateTimeFormatter.ofPattern("yyyy-MM-dd'T'HH:mm:ss");

        @Override
        public Timestamp convert(MappingContext<String, Timestamp> mappingContext) {
            String source = mappingContext.getSource();
            LocalDateTime dateTime = LocalDateTime.parse(source, this.formatter);
            return Timestamp.valueOf(dateTime);
        }
    }

Basically convert string to LocaDateTime using DateTimeFormatter, then convert using Timestamp.valueOf(LocalDateTime).

  1. Option 2 - more specific. If you are using different formats in your app you can make StudentDataDto to StudentMetaDataEntity converter
public class DtoToMetaConverter implements Converter<StudentDataDto, StudentMetaDataEntity> {

        @Override
        public StudentMetaDataEntity convert(MappingContext<StudentDataDto, StudentMetaDataEntity> mappingContext) {
            StudentDataDto source = mappingContext.getSource();
            StudentMetaDataEntity dest = new StudentMetaDataEntity();
            //Convert string to timestamp like in other example
            dest.setCreatedAt(...);
            return dest;
        }
    }

Then register the converter and test it.

public class TimestampMain {

    public static void main(String[] args) {
        ModelMapper modelMapper = new ModelMapper();
        modelMapper.addConverter(new StringToTimestampConverter());

        StudentDataDto source = new StudentDataDto();
        source.setCreatedAt("2022-01-20T11:12:46");
        StudentMetaDataEntity destination = modelMapper.map(source, StudentMetaDataEntity.class);
        System.out.println(destination.getCreatedAt());
    }
}

This example uses the more general option 1, but if you need option 2, just register the other converter in similar fashion.

Chaosfire
  • 4,818
  • 4
  • 8
  • 23