GV

GeezerVison

The World Wide Web, restored to 800 pixels of glory.

Home|About|Gaming cabinet|Archives|How it works|Starter sites

On this site

  • Sign in
  • ActionsCI/CD and automation
  • CodespacesCloud development environments
  • All Features
  • Documentation
  • Training
  • Certifications
  • Build with Copilot
  • Customize Copilot
  • Integrations
  • Trending repos
  • GitHub Sponsors
  • Developer articles
  • Enterprises

This view

  • Original page
  • New address

GeezerVison is showing github.com — Kotlin: problems with data classes · Issue #2281 · mapstruct/mapstruct

Kotlin: problems with data classes #2281

New issue
Copy link
New issue
Copy link
Closed
#3978
Closed
Kotlin: problems with data classes#2281
#3978
Copy link
Labels
kotlin
Milestone
1.7.0.Beta1

Description

@jakoss
jakoss
opened on Nov 24, 2020
Issue body actions
  • Is this an issue (and hence not a question)?

I'm trying to use MapStruct with kotlin codebase. For the most part i'm pretty successfull, but i have 2 issues that are weird and require workarounds that shouldn't be necessary.

First issue that i have is that if i try to map data class with single field i have to explicitly ignore "copy" target in mapper annotation because i'm getting warning "Unmapped target property: "copy"". When i add second field - it's no longer needed. Example:

data class SinglePropertyTest(val property: String)
data class SinglePropertyTestDto(val property: String)

data class DoublePropertyTest(val property1: String, val property2: Int)
data class DoublePropertyTestDto(val property1: String, val property2: Int)

@Mapper
interface SinglePropertyMapper {
    // this gives me warning - Unmapped target property: "copy"
    fun toDto(singlePropertyTest: SinglePropertyTest): SinglePropertyTestDto

    // this works just fine
    fun toDto(doublePropertyTest: DoublePropertyTest): DoublePropertyTestDto
}

Seconds issue is more dangerous. If class have all fields nullable with default value set to null - mapper is silently failing to map any properties (all properties in target class are null). Example:

fun main(args: Array<String>) {
    val allNullClass = AllNullClass("test", null)
    val mapper = Mappers.getMapper(AllNullMapper::class.java)
    val mappedDto = mapper.toDto(allNullClass)

    // property1 here is null !
    assert(mappedDto.property1 != null)
    assert(mappedDto.property2 == null)
}

data class AllNullClass(
    val property1: String? = null,
    val property2: Int? = null
)

data class AllNullClassDto(
    val property1: String? = null,
    val property2: Int? = null
)

@Mapper
interface AllNullMapper {
    fun toDto(allNullClass: AllNullClass): AllNullClassDto
}

Generated code looks like that:

public class AllNullMapperImpl implements AllNullMapper {

    @Override
    public AllNullClassDto toDto(AllNullClass allNullClass) {
        if ( allNullClass == null ) {
            return null;
        }

        AllNullClassDto allNullClassDto = new AllNullClassDto();

        return allNullClassDto;
    }
}

It seems like MapStruct is using the empty constructor in this case.

All in all - great library, almost perfect for my current usage!

Reacted by Arne Seime, Morten Løcka, Nikolay Metchev, Olivier Voortman, Sergey Shumov, Julian Jarecki, edee111, Pasha Dymytriiev, Anderson Xavier, Jens and 36 more
Reacted by Nikhil Sidhaye

Activity

Next
  1. filiphr commented on Nov 25, 2020

    @filiphr
    filiphr
    on Nov 25, 2020
    Member
    More actions

    Both problems seem related to the way the Kotlin byte code (AST) looks like when we try to create the mappers.

    I guess that the first issue is due to the fact that Kotlin creates some method that looks like:

    SinglePropertyTest#copy(String)
    

    and since this is a fluent property we treat it as property. Seems like this property is not there when having multiple properties.

    The second problem is the fact that Kotlin creates multiple constructors (one of them being the empty constructor) and thus MapStruct picking that one.

    For both cases I have really limited knowledge of Kotlin and would appreciate any help. It would be good to know if there is some way to determine that a class is a kotlin data class during our compilation. If we can detect that then we can do something here.

    Reacted by Szukács Marcell and Martijn van der Blom
  2. jakoss commented on Nov 25, 2020

    @jakoss
    jakoss
    on Nov 25, 2020
    Author
    More actions

    There are ways to detect the data class type in JVM, but they rely on internal implementation that might change (https://stackoverflow.com/a/29077162/939133). Nontheless i believe that's still better than nothing. data classes are really one of the best kotlin features and one that's almost always used for models and dtos. So this issues might be quite often

  3. hahmjt commented on Dec 8, 2020

    @hahmjt
    hahmjt
    on Dec 8, 2020
    More actions

    I am having similar issue where Impl function does not have any sort of value assignment and does not use the Constructor to initialize the object. In Kotlin case, it would create an exception.

    Here is the Impl method generated

        public Dto entityToDto(Entity entity) {
            if ( entity == null ) {
                return null;
            }
    
            long arg0 = 0L;
            String arg1 = null;
            long arg2 = 0L;
            int arg3 = 0;
    
            Dto dto = new Dto( arg0, arg1, arg2, arg3 );
    
            return dto;
        }
    
    Reacted by Svyatoslav Lebetsky, Łukasz Kokot and Danielle
  4. mortenlocka commented on Feb 13, 2021

    @mortenlocka
    mortenlocka
    on Feb 13, 2021
    More actions

    I'm experiencing the same issue. Hoping for a fix!

    Reacted by Honza
  5. filiphr
    added
    kotlin
    on Mar 14, 2021
  6. Iamdreamlover commented on May 6, 2021

    @Iamdreamlover
    Iamdreamlover
    on May 6, 2021
    More actions

    I have same problem. Do someone has a solution? thank you

  7. a-ivanov commented on Jul 23, 2021

    @a-ivanov
    a-ivanov
    on Jul 23, 2021
    More actions

    Easy fix is to ignore copy "property":

    @Mapper
    interface SomeMapper {
    
        @Mapping(target = "copy", ignore = true)
        fun toDto(src: Src): Dto
    }

    This is nevertheless misleading.

  8. jakoss commented on Jul 23, 2021

    @jakoss
    jakoss
    on Jul 23, 2021
    Author
    More actions

    @a-ivanov It's a workaround we have to live with, but i wouldn't call it a fix :)

    Reacted by Steve Hoang, Vladimir Kim and David Kovář
    Reacted by Anton Ivanov, d844022f2a734789aa637500a757671e, Ki and loalexzzzz
  9. sskrla commented on Jan 12, 2022

    @sskrla
    sskrla
    on Jan 12, 2022
    More actions

    I also commented on #2577 which is a duplicate of one of the stated problems. Specifically Kotlin will generate an implicit constructor when all properties have a default. This causes model mapper to "fall out of" constructor mapping mode.

    A relatively simple workaround is to annotate the constructor with @Default and introduce an explicit no-args constructor without this annotation. Simply supplying the annotation will copy it into the implicit no-arg constructor as well, causing Mapstruct to (I think I observed) non-deterministically select a mapping mode.

    data class AllNullClassDto @Default constructor(
        val property1: String? = null,
        val property2: Int? = null
    ) {
        constructor() : this(null, null)
    }
    

    It isn't pretty, and would be nice if the default behavior was the correct behavior, but it works.

    Reacted by Anton Koscejev and Sergey Kolychev
    Reacted by Nikhil Sidhaye and Vladimir Kim
  10. acoconut commented on Jan 27, 2022

    @acoconut
    acoconut
    on Jan 27, 2022
    More actions

    @sskrla which package is this @Default you're using?

  11. filiphr commented on Jan 29, 2022

    @filiphr
    filiphr
    on Jan 29, 2022
    Member
    More actions

    @acoconut have a look in the Non-shipped annotations section of our documentation. The @Default annotation can be in any package, it can be your own annotation as well. MapStruct only looks in the simple name of the annotation

    Reacted by Ana Cutillas and Kirill Gavrilov
  12. haghani-mehran commented on Feb 25, 2022

    @haghani-mehran
    haghani-mehran
    on Feb 25, 2022
    More actions

    @sskrla which package is this @Default you're using?

    Alternatively you could only make the no-args constructor private then you don't need to add the @Default annotation.

    Reacted by Anton Koscejev and Kirill Gavrilov
  13. koscejev commented on Mar 10, 2022

    @koscejev
    koscejev
    on Mar 10, 2022
    More actions

    Here's another small improvement to the workaround. It's not necessary to call the constructor with values for all the fields that have defaults, but at least 1 field is required to distinguish the constructors:

    data class AllNullClassDto(
        val property1: String? = null,
        val property2: Int? = null,
        ...
        val property28: Long? = null,
    ) {
        private constructor() : this(null)
    }

    Unfortunately I haven't figured out how to apply this workaround to generated DTOs. :(

    Reacted by Steve Skrla, minyu-luo, dloukusa-bc and dirkbolte-otto
  14. grzegorz-grabarz commented on Apr 11, 2022

    @grzegorz-grabarz
    grzegorz-grabarz
    on Apr 11, 2022
    More actions

    Hi, we also had problems with Kotlin's copy method. Similar error occurs when we define an instance method that takes one parameter as input and returns the containing class as a return type. (so any signature similar to the copy method)

    The root cause of this behaviour is DefaultAccessorNamingStrategy's isSetterMethod.
    It recognizes such functions as fluent setters.

    Our solution was defining a CustomAccessorNamingStrategy and forgetting that fluent setters ever existed :)

    class CustomAccessorNamingStrategy : DefaultAccessorNamingStrategy() {
    
        override fun isSetterMethod(method: ExecutableElement): Boolean {
            val methodName = method.simpleName.toString()
            return methodName.startsWith("set") && methodName.length > 3
        }
    }
    Reacted by Lucas Laurindo dos Santos
  15. stravag commented on May 18, 2022

    @stravag
    stravag
    on May 18, 2022
    More actions

    Easy fix is to ignore copy "property":

    @Mapper
    interface SomeMapper {
    
        @Mapping(target = "copy", ignore = true)
        fun toDto(src: Src): Dto
    }

    This is nevertheless misleading.

    This workaround only works if the data class has a single property, it fails when there is more than one:

    error: Unknown property "copy" in result type

  16. 7 remaining items

  17. wirekang commented on Apr 22, 2023

    @wirekang
    wirekang
    on Apr 22, 2023
    More actions

    Our solution was defining a CustomAccessorNamingStrategy and forgetting that fluent setters ever existed :)

    We already using a spi-impl, so I can't implement a custom Strategy. It seems there is no way to use multiple Strategies. Do you have an idea?

  18. NikolayMetchev commented on Apr 22, 2023

    @NikolayMetchev
    NikolayMetchev
    on Apr 22, 2023
    More actions

    Our solution was defining a CustomAccessorNamingStrategy and forgetting that fluent setters ever existed :)

    We already using a spi-impl, so I can't implement a custom Strategy. It seems there is no way to use multiple Strategies. Do you have an idea?

    We use a hack to incorporate both. Gradle can ransform the protobuf spi jar to remove one of the files in there and then we subclass the protobuf version

  19. wirekang commented on Apr 22, 2023

    @wirekang
    wirekang
    on Apr 22, 2023
    More actions

    @NikolayMetchev
    Wow Can you give more information about the gradle hack?

  20. NikolayMetchev commented on Apr 22, 2023

    @NikolayMetchev
    NikolayMetchev
    on Apr 22, 2023
    More actions

    @NikolayMetchev Wow Can you give more information about the gradle hack?

    The idea is to delete the /META-INF/services/org.mapstruct.ap.spi.AccessorNamingStrategy file from protobuf-spi-impl.jar

    build.gradle.kts

    val artifactType = Attribute.of("artifactType", String::class.java)
    val processed = Attribute.of("processed", Boolean::class.javaObjectType)
    
    dependencies {
        attributesSchema {
            attribute(processed)
        }
        artifactTypes.getByName("jar") {
            attributes.attribute(processed, false)
        }
    
        registerTransform(DeleteJarEntry::class) {
            from.attribute(artifactType, "jar").attribute(processed, false)
            to.attribute(artifactType, "jar").attribute(processed, true)
    
            parameters {
                fileNamePrefix = "protobuf-spi-impl"
                fileToDelete = "/META-INF/services/org.mapstruct.ap.spi.AccessorNamingStrategy"
            }
        }
    
        implementation(libs.mapstruct.protobuf)
    }
    
    configurations.all {
        afterEvaluate {
            if (isCanBeResolved) {
                attributes.attribute(processed, true)
            }
        }
    }
    

    buildSrc/src/main/kotlin/DeleteJarEntry.kt

    abstract class DeleteJarEntry : TransformAction<Parameters> {
        interface Parameters : TransformParameters {
            @get:Input
            var fileToDelete: String
    
            @get:Input
            var fileNamePrefix: String
        }
    
        @InputArtifact
        abstract fun getInputArtifact(): Provider<FileSystemLocation>
    
        override fun transform(outputs: TransformOutputs) {
            val input = getInputArtifact().get().asFile
            if (input.name.startsWith(parameters.fileNamePrefix)) {
                deleteJarEntry(input, parameters.fileToDelete)
            }
            outputs.file(getInputArtifact())
        }
    
        private fun deleteJarEntry(inputJar: File, fileToDelete: String) {
            FileSystems.newFileSystem(inputJar.toPath(), mapOf("create" to "false")).use {
                val pathToDelete = it.getPath(fileToDelete)
                if (Files.exists(pathToDelete)) {
                    Files.delete(pathToDelete)
                }
            }
        }
    }
    

    Then in your own gradle module have your own /META-INF/services/org.mapstruct.ap.spi.AccessorNamingStrategy file in module/src/main/resources/META-INF/services/org.mapstruct.ap.spi.AccessorNamingStrategy which will point to your class which will subclass no.entur.mapstruct.spi.protobuf.ProtobufAccessorNamingStrategy

    Reacted by wirekang
  21. wirekang commented on Apr 22, 2023

    @wirekang
    wirekang
    on Apr 22, 2023
    More actions

    @NikolayMetchev Thank you very much for your kind answer!! I'll try.

  22. birnbuazn commented on Jun 15, 2023

    @birnbuazn
    birnbuazn
    on Jun 15, 2023
    More actions

    I don't know why people are downvoting the reply from @jakoss. This thread here has multiple workarounds for the constructor and copy problems.

    I guess the reason is that the workarounds mentioned here are exactly what they are: workarounds. They work, but they are ugly.

    This has nothing to do that I want a solution served on a silver plate. I'm just wondering why I have to add boiler plate to my Kotlin data classes, just for MapStruct to pick up the correct constructor.

    Reacted by Leon-Manolo, Ana Cutillas, Danielle , Soraia Basso, rafal-danowski-allegro, Steve Hoang, Ondřej Sedlár, kiturutin, Jakub Łaba and Vladimir Kim
  23. jakoss commented on Jun 15, 2023

    @jakoss
    jakoss
    on Jun 15, 2023
    Author
    More actions

    I'm just wondering why I have to add boiler plate to my Kotlin data classes, just for MapStruct to pick up the correct constructor.

    You don't have to, you can use something else. Konvert is (in my opinion) already ahead in some features in addition to being first class kotlin citizen

    Reacted by Ana Cutillas, i-avalos, Raphael De Lio, Vladimir Kim and Martin Timko
  24. filiphr commented on Jun 16, 2023

    @filiphr
    filiphr
    on Jun 16, 2023
    Member
    More actions

    This has nothing to do that I want a solution served on a silver plate. I'm just wondering why I have to add boiler plate to my Kotlin data classes, just for MapStruct to pick up the correct constructor.

    We are all working on MapStruct in our free time, so things get prioritised based on what we can do. Currently we are focused on 1.6. I have seen that people are interested in Kotlin, so I am going to see if we can focus on that for 1.7.

    Reacted by Thom, GeoffreyStemys, Bernard Laveaux, evef and Natalia Goncharova
  25. lucasls commented on Aug 16, 2023

    @lucasls
    lucasls
    on Aug 16, 2023
    More actions

    Hi, we also had problems with Kotlin's copy method. Similar error occurs when we define an instance method that takes one parameter as input and returns the containing class as a return type. (so any signature similar to the copy method)

    The root cause of this behaviour is DefaultAccessorNamingStrategy's isSetterMethod. It recognizes such functions as fluent setters.

    Our solution was defining a CustomAccessorNamingStrategy and forgetting that fluent setters ever existed :)

    class CustomAccessorNamingStrategy : DefaultAccessorNamingStrategy() {
    
        override fun isSetterMethod(method: ExecutableElement): Boolean {
            val methodName = method.simpleName.toString()
            return methodName.startsWith("set") && methodName.length > 3
        }
    }

    I have a question about the SPI solution from @grzegorz-grabarz and the hack by @NikolayMetchev. Does the SPI solution require the META-INF to be placed in a separate JAR? Is this a limitation of Java's SPI mechanism or a decision from MapStruct?

    Defining a custom accessor strategy feels like the correct solution here, but I would prefer not to have to create a separate lib or module just for that. That's specially painful on Maven, given moving from single to submodule requires changing the whole structure of the project to have a parent project.

    Reacted by wirekang and Steve Hoang
  26. NikolayMetchev commented on Aug 16, 2023

    @NikolayMetchev
    NikolayMetchev
    on Aug 16, 2023
    More actions

    In theory the protobuf-spi could have allowed extension on their end by whatever means they think most appropriate. Might be worth filing a feature request with them. I don't think it is an inherent problem with Java SPI.

  27. stevezuju commented on Oct 10, 2023

    @stevezuju
    stevezuju
    on Oct 10, 2023
    More actions

    Any update on this guys ?

  28. filiphr
    marked kotlin kapt mapstruct report "Unmapped target property: "xxx" #3645 as a duplicate of this issue on May 31, 2025
  29. filiphr
    added this to the 1.7.0 milestone on Jan 29, 2026
  30. filiphr
    closed this as completedin d300664on Jan 29, 2026
  31. filiphr
    added a commit that references this issue on Jan 29, 2026

    Improve support for Kotlin Data Classes (mapstruct#3978)

    This issue will close once commit d300664 is merged into the 'main' branch.
    d300664
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    kotlin

    Type

    No type

    Projects

    No projects

      Milestone

      • 1.7.0.Beta1

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions

      You are visitor 01402503 — GeezerVison does not store the pages you look up.
      Best viewed with Netscape Navigator 4.7 or Internet Explorer 6 at 800×600. No cookies required.