mapstruct icon indicating copy to clipboard operation
mapstruct copied to clipboard

Lombok @Value/@Builder mapping fails when source/target has nested properties

Open dan-lind opened this issue 5 years ago • 15 comments

This issue is somewhat involved to explain, so I took the lombok example from the mapstruct-examples repo and adapted a bit to create a repro which can be found here: https://github.com/dan-lind/mapstruct-lombok-example

What I have observed is that when annotating the target with @Value and @Builder, mapstruct is unable create a mappping when the source has properties on multiple levels, and the target has nested properties.

When building the above repro code with gradle clean build, I get these error messages

SourceTargetMapper.java:23: error: Property "testingToo" has no write accessor in NestedTarget for target name SourceTargetMapper.java:23: error: Property "testing" has no write accessor in NestedTarget for target name 

The source looks like this

@Data
public class Source {
    private String test;
    private AnotherSource testToo;
}

@Data
public class AnotherSource {
    private String test;
}

and target

@Value
@Builder
public class Target {
    NestedTarget nestedTarget;
}

@Value
@Builder
public class NestedTarget {

    Long testing;
    Long testingToo;
}

Some observations: If I remove either of the source properties, i.e.

    //private String test;
    private AnotherSource testToo;

or

    private String test;
    //private AnotherSource testToo;

and make the appropriate adjustments to the mapper, then the problem goes away.

The problem also goes away if I move the properties of the NestedTarget down to Target, i.e.

@Value
@Builder
public class Target {
    Long testing;
    Long testingToo;
}}

Please let me know if you need more information.

dan-lind avatar Jan 30 '21 09:01 dan-lind

I am fairly certain that this is linked to https://github.com/rzwitserloot/lombok/issues/1538. There is an interface that's implemented in MapStruct that it implemented in Lombok that should make all of this work. There are some known issues with the builder. Please vote on the Lombok issue

filiphr avatar Jan 31 '21 07:01 filiphr

@filiphr This one is not lombok related, I attached a delomboked example that fails too. I noticed that it works if there is only one @Mapping in SourceTargetMapper. It seems like it only fails if one source is not nested and the other is and both targets are nested. Every other combination I tried seems to be okay.

Rawi01 avatar Feb 02 '21 16:02 Rawi01

Thanks for looking into this @Rawi01. Looking at the zip you shared it seems like the problem is the same as in #2322. I'll reopen this and have a better look soon

filiphr avatar Feb 03 '21 08:02 filiphr

OK I had a better look at this and I know why this is happening. It is due to the nested target mapping.

When we have

    @Mapping(target = "nestedTarget.testing", source = "test")
    @Mapping(target = "nestedTarget.testingToo", source = "testToo.test")
    Target toTarget(Source source);

MapStruct will create 2 update methods for mapping the NestedTarget as there are 2 different sources for that. However, NestedTarget has no setters and thus we can't update it.

We now have good support for constructor mapping, perhaps We can reuse that instead and create one method where we will get the target data and not create update methods.

I'll need to think a bit more about how to fix this. It will take some time.

@dan-lind if you can I will advise you to try for a solution that does not use nested target mapping. e.g.

    @Mapping(target = "nestedTarget", source = "source")
    Target toTarget(Source source);

    @Mapping(target = "testing", source = "test")
    @Mapping(target = "testingToo", source = "testToo.test")
    NestedTarget toNestedTarget(Source source);

Having now written this example, perhaps we can generate such a method (a potential fix for this)

filiphr avatar Feb 06 '21 08:02 filiphr

Thanks for looking in to this @filiphr. For now, I got it working by just using regular get/set methods (@ Data annotation). I'll probably leave like that for now and wait for a potential fix. I'll be happy to help you test when you think you have a solution!

dan-lind avatar Feb 06 '21 14:02 dan-lind

Thanks for looking in to this @filiphr. For now, I got it working by just using regular get/set methods (@ Data annotation). I'll probably leave like that for now and wait for a potential fix. I'll be happy to help you test when you think you have a solution!

Your Lombok jar package (or Maven dependency) should be placed in front of mapstruct, so that Lombok can be executed first at compile time

yangyik avatar Mar 11 '21 01:03 yangyik

Do you have some updates? When it will be possible to use this feature?

fragaLY avatar Mar 24 '21 12:03 fragaLY

@fragaLY which feature are you talking about? This is a corner case bug which we will look into fixing for 1.5 (as shown with the linked milestone). There really is no need to ask for updates when all the available information is already in the issue

filiphr avatar Mar 27 '21 15:03 filiphr

Thanks for looking in to this @filiphr. For now, I got it working by just using regular get/set methods (@ Data annotation). I'll probably leave like that for now and wait for a potential fix. I'll be happy to help you test when you think you have a solution!

Your Lombok jar package (or Maven dependency) should be placed in front of mapstruct, so that Lombok can be executed first at compile time

I tried your method,it worked.

moguxiaoxiang01 avatar Jul 06 '21 09:07 moguxiaoxiang01

Still reproduces :(

npwork avatar Nov 17 '21 21:11 npwork

Still reproduces :(

@npwork, your comment does not bring a lot to the current discussion. Yes the original reported issue still reproduces, the fact that this issue is not closed should make it clear that the problem has not been solved.

filiphr avatar Nov 20 '21 07:11 filiphr

Hey, @filiphr , I found a variation of the same issue,

https://bitbucket.org/rdllopes/mapstructnestederror/

If the target class contains a builder inner class, mapstruct will target that inner class instead of the outer class.

random-goodguy avatar Nov 24 '22 02:11 random-goodguy

Thanks @random-goodguy, we'll have a look at your project in more details. What you are explaining is right though. Have a look at our Using builders documentation, when MapStruct finds builders in your class then it will use the builder to create the object and not the target class. You need to disable builders if you don't want to use them.

You can disable them by using

@BeanMapping(builder = @Builder(disableBuilder = true))

or

@Mapper(builder = @Builder(disableBuilder = true))

filiphr avatar Nov 26 '22 08:11 filiphr

@filiphr Thank you. But I didn't expect that the inner builder would interfere on MapStruct behavior by default. Well, anyway your solution works. Thanks again.

random-goodguy avatar Nov 30 '22 20:11 random-goodguy

This might also be tackled with #2941

thunderhook avatar Sep 21 '23 19:09 thunderhook