Saturday, November 4, 2023

MapStruct library: setting a field with an expression to a ZonedDateTime

Introduction

Setup:

- Mapstruct 1.5.5
- Kotlin 1.8
- IntelliJ 2023.2.3
 

 

For a mapping between two classes, the source class did not have the mandatory (non-null, Kotlin!) target class field created. And I wanted to fill it with the current ZonedDateTime class, with the timezone of Amsterdam/Europe. And that specific ZoneId is a constant in my Application.kt class named MY_DEFAULT_TIME_ZONE.

Solution

So I looked at the expression field in @Mapping found here.
I got this solution working quite fast with: 

@Mapping(target = "created", expression = "java(ZonedDateTime.now())"). 

But as you see, the ZoneId constant is still missing.

Potential other solutions

I had to try quite a few things to get that working, because the class ZoneId was not getting recognized in the generated implementation MapStruct mapper.
In the end this worked:

@Mapper(componentModel = MappingConstants.ComponentModel.SPRING, imports = [ZoneId::class, Application::class])
interface MyMapper {

  @Mapping(target = "created", expression = "java(ZonedDateTime.now(my.package.Application.MY_DEFAULT_TIME_ZONE))")
  fun myDtoToResponseDto(myDto: MyDto): ResponseDto
  ...
}
Note the imports field to have the class ZoneId and the constant available (imported) in the implementation class, generated by MapStruct.

In the Application.kt you then have to make the MY_DEFAULT_TIME_ZONE constant available to Java, since that's what MapStruct uses as language:

Application.kt
{
  companion object {

    @JvmField
    val MY_DEFAULT_TIME_ZONE: ZoneId = ZoneId.of("Europe/Amsterdam")
    ...
}

I also tried this: 

@Mapping(target = "created", source = ".", qualifiedByName = ["getValue"]) 

with a function:

@Named(value = "getValue")
fun getValue(myDto: MyDto): ZonedDateTime {
  return ZonedDateTime.now(MY_DEFAULT_TIME_ZONE)
}

The advantage of this solution is that you can use Kotlin code and you don't have to wait and see if your expression has the correct syntax and will compile.
But then I got this error: ZonedDateTime does not have an accessible constructor. I also tried to wrap the field created in a small class, but that didn't work either (could be me :)
See this and this for more details on how that should work.

I also tried with the @JvmDefault annotation, but that is deprecated + it requires you to use the -Xjvm-default property, which I couldn't get to work in IntelliJ with Gradle.
And it is not always guaranteed to work, see here and here and here:

I'm definitely still a beginner in using MapStruct. So probably one of the other methods could work too... Any tips are welcome :)