mapstruct / mapstruct

An annotation processor for generating type-safe bean mappers

Home Page:https://mapstruct.org/

Geek Repo:Geek Repo

Github PK Tool:Github PK Tool

Kotlin Code Generation and Empty Constructor Preference

Fyzxs opened this issue · comments

commented
  • Is this an issue (and hence not a question)?

The key components of the issue I'm encountering

  • OpenApi DTO specification has non-required fields
  • Generation as immutable kotlin data classes
  • Being generated, cannot use @Default
  • MapStruct selecting empty constructor when present (w/o @default)

OpenApi will generate Kotlin code for a DTO with unrequired fields as nullable. The Kotlin code will look something like

data class Example{
    val requiredField: String,
    val notRequiredField: String? = null
}

The compiled code has a primary constructor with all fields and an empty constructor. The empty constructor is preferred by MapStruct. Since the fields are immutable, no data gets transferred between the source and target object.
The constructor that should be used exists, there's just preference for the empty constructor.

My thought on this is to have an option to tell MapStruct to use the constructor with the most arguments.

This one is a tricky one @Fyzxs. What should happen if there are constructors with the same number of arguments?

Since we are talking about Kotlin Data classes, I think that this issue is the same as #2281. I haven't used Kotlin that much. However, I think that a possible solution would be if we can somehow pick the constructor that matches the data class in its full from the AST generated by Kotlin

commented

At least part of #2281 is the same. The issue I'm encountering is that the empty constructor is being picked up and nothing mapped. I'm not encountering the issue with the #copy methods.

I think using the primary/default constructor out of the Kotlin class (independent of it being a data class) when available would be the best choice, or at least an option to configure it.

I was unable to see a way to get that constructor in the MapStruct code. I'm not familiar with the functionality used, so was fumbling around blind.

For a simple class

class Example(
    val imProp1: String,
    var mProp2: String? = null
)

there will be a constructor generated with this signature

public synthetic <init>(Ljava/lang/String;Ljava/lang/String;ILkotlin/jvm/internal/DefaultConstructorMarker;)V

(Note - Some of my actual code had an inserted int before the DefaultConstructorMarker, don't know why it's different here... It might not actually be different... when I decompile the bytecode to java, this is the signature that results
public Example(String var1, String var2, int var3, DefaultConstructorMarker var4))

The synthetic and last parameter being the DefaultConstructorMarker appear to be what identifies it. There's another constructor that is the same, w/o the DefaultConstructorMarker (and, in my actual code, w/o the int)

public <init>(Ljava/lang/String;Ljava/lang/String;)V

I have this idea working (needed it for work) and I have a commit with the functionality. Where I made the core change, BeanMappingMethod@getConstructorAccessor, the constructors provided do not include the synthetic constructor shown above.
This is the bit getting the constructors

 List<ExecutableElement> constructors = ElementFilter.constructorsIn( type.getTypeElement()
                .getEnclosedElements() );

If the default constructor can be retrieved; it'd be easy enough to find the constructor that matches all args except the int/default marker.


To address the actual question, I've let it fall through to existing behavior. It'd be reasonable to cause an exception as it does now when there were 2+ public constructors w/o one being empty.
I was trying to understand the code while working to get specific functionality in place; I don't think my current granularity is a great choice.
Having had a chance to step back from implementation, I think having it as an option on some combination of the @MapperConfig, @Mapper, @mapping would provide much better control than my current single setting that applies everywhere (which works for my case).

Thanks for your deep analysis @Fyzxs. I also had a brief look at how the Spring Framework handles this. I found this. Seems like Kotlin does expose certain annotations on the types to be able to detect such things.

What I would suggest is to do something in the BeanMappingMethod.Builder#hasDefaultAnnotationFromAnyPackage.

Or maybe even sooner and do something similar like we are doing for Java Records here.

I am OK to adding an information whether a MapStruct Type is a kotlin type. And doing special handling to get the constructor.

I have the following use case and it fails because the data class constructor specifies defaults:
This happens when you use immutable Pojos generated by Jooq

@Mapper
abstract class AMapper {
  @Mappings(
    Mapping(source = "a.id", target = "id2"),
    Mapping(source = "overrideId", target = "id"),
  )
  abstract fun map(a: A, overrideId: String? = null) : B
}

data class A(val id: String?, val id2: String?)

data class B
//@Default constructor
  (val id: String? = null, val id2: String? = null)

@Target(CONSTRUCTOR)
@Retention(CLASS)
annotation class Default

The error is :

error: Property "id2" has no write accessor in .
    @org.mapstruct.Mappings(value = {@org.mapstruct.Mapping(target = "id2", source = "a.id"), @org.mapstruct.Mapping(target = "id", source = "overrideId")})

Current workaround:
Modify Jooq to generate mutable Pojos.

It is not possible to modify Jooq currently to add that Default annotation which I have commented out in my example above and lets the use case succeed.
It is also not possible currently to remove the defaulting = null values from all the constructor paramters in the immutable Jooq pojos.

any update on this issue? Do we have a way to choose which constructor in kotlin?

Any news on this? Prior to Kotlin v1.5, adding the @Default annotation to the constructor allowed us to work around this issue. But after testing on Kotlin 1.5 & 1.6 mapstruct seems to generate implementation code that uses the empty constructor.

I'm experiencing exactly the same issue with openapi-generator and MapStruct 1.5.0.Beta2.
Waiting for Kotlin support or something... 😄

I found a workaround about this issue that completely fits me: generate builders for data classes, that are used as return types in mappers, at compile time. Since it's not required edit your classes, there are no limitations for data classes generated by third-party tools.
Created library for this, hope it will be helpful for you:
https://github.com/olxmute/mapstruct-kotlin-builder-generator

commented

Any news on this? Prior to Kotlin v1.5, adding the @Default annotation to the constructor allowed us to work around this issue. But after testing on Kotlin 1.5 & 1.6 mapstruct seems to generate implementation code that uses the empty constructor.

I am experiencing this exact same problem, after debugging I was able to determine the issue occurs because the annotation is added to all the constructors generated for the data class. An ugly workaround is to define the second constructor explicitally.