mapstruct / mapstruct/mapstruct

using @MappingTarget with @Default constructor annotated member class

Open
#2,178 3 comments 0 reactions 1 assignee View on GitHub

@sjaakd is already working on this.

Since Aug 21, 2020.

feature for:team-discussion
Dominant language
Java
Stars
7.7k
Forks
1.1k
PR merge metrics
No merged PRs in 30d

Description

Mapping method with @MappingTarget generates code that tries to alter immutable member instead of setting a new instance.

Example:
If I have a class with a constructor injected member:

public class DemonstratorTarget {
    private DemonstratorTargetEmbedded embedded;
    @Default
    public DemonstratorTarget(DemonstratorTargetEmbedded embedded) {        this.embedded = embedded;    }
    public DemonstratorTargetEmbedded getEmbedded() {        return embedded;    }
    public void setEmbedded(DemonstratorTargetEmbedded embedded) {        this.embedded = embedded;    }
}

with a supposedly immutable member class:

public class DemonstratorTargetEmbedded {
    private String version;
    private Map<String, String> metadata = new HashMap<>();
    public DemonstratorTargetEmbedded() {    }
    @Default
    public DemonstratorTargetEmbedded(String version, Map<String, String> metadata) {
        this.version = version;
        this.metadata = metadata;
    }
    public String getVersion() {        return version;    }
    public Map<String, String> getMetadata() {        return metadata;    }
}

and use it in a mapper as @MappingTarget:

@Mapper
public interface DemonstratorMapperBroken {
    void updateTarget(@MappingTarget DemonstratorTarget target, DemonstratorSource source);
}

The resulting mapper will not call target.setEmbedded([newly created instance]) but tries to alter the member:

            mappingTarget.getMetadata().clear();
            Map<String, String> map = demonstratorSourceEmbedded.getMetadata();
            if ( map != null ) {
                mappingTarget.getMetadata().putAll( map );
            }

This of course fails in the example above, because there is no setter for version.

Now if I change the mapper to look like this (added a mapping method, did not change the original one):

@Mapper
public interface DemonstratorMapperWorking {
    void updateTarget(@MappingTarget DemonstratorTarget target, DemonstratorSource source);
    DemonstratorTargetEmbedded toTargetEmbedded(DemonstratorSourceEmbedded gender);
}

...the mapper works as expected:

target.setEmbedded( toTargetEmbedded( source.getEmbedded() ) );
[...]
DemonstratorTargetEmbedded demonstratorTargetEmbedded = new DemonstratorTargetEmbedded( version, metadata );

I am unsure if this is expected behaviour. Is there a way to circumvent this without adding a method like toTargetEmbedded?

I created a demonstrator:

  1. git clone -b mapstruct-constructor-mapping https://github.com/mickroll/demonstrator.git
  2. cd demonstrator
  3. mvn clean install

Contributor guide

Open the contributing guide

First steps

  1. Read the whole issue, then the project's contributing guide.
  2. Comment on the issue to say you are picking it up — it saves two people doing the same work.
  3. Fork the repository and make your change on a branch.
  4. Open a pull request that references the issue number.

Assessment

This issue has not been assessed yet.

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.