Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Mapping from "."(this) to target child object with additional custom mapping failed. #3482

Open
AndrisAncans opened this issue Dec 25, 2023 · 1 comment
Labels

Comments

@AndrisAncans
Copy link

AndrisAncans commented Dec 25, 2023

Expected behavior

Mapping from source "." to to target object fails for case with additional defined mapping.

     @Mapping(target = "inner1", source = ".")
     @Mapping(target = "inner1.innerName", source = "sourceName")
    TopLevel mapSourceToTop(SourceLevel source);

Without missing mapping @Mapping(target = "inner1.innerName", source = "sourceName") just copy same named properties. With @Mapping(target = "inner1.innerName", source = "sourceName") - only "inner1.innerName" copied. It is no any MapStruct warring in such case, but only some fields copied.

It is valid workaround by adding mapping method from SourceLevel to InnerLevel.
Possible related to #3475.

Actual behavior

To compare different source behaviour, same type objects copied from source different sources

@Mapper(componentModel = MappingConstants.ComponentModel.SPRING, mappingControl = DeepClone.class)
public interface LevelMapper {
     @Mapping(target = "inner1", source = ".")
     @Mapping(target = "inner1.innerName", source = "sourceName")
     @Mapping(target = "inner2", source = "inner")
     @Mapping(target = "inner2.innerName", source = "inner.sourceInnerName")
    TopLevel mapSourceToTop(SourceLevel source);
}

In result inner1 object filled only innerName value.

@Component
public class LevelMapperImpl implements LevelMapper {

    @Override
    public TopLevel mapSourceToTop(SourceLevel source) {
        if ( source == null ) {
            return null;
        }

        TopLevel.TopLevelBuilder topLevel = TopLevel.builder();

        topLevel.inner1( sourceLevelToInnerLevel( source ) );
        topLevel.inner2( sourceInnerLevelToInnerLevel( source.getInner() ) );

        return topLevel.build();
    }

    protected InnerLevel sourceLevelToInnerLevel(SourceLevel sourceLevel) {
        if ( sourceLevel == null ) {
            return null;
        }

        InnerLevel.InnerLevelBuilder innerLevel = InnerLevel.builder();

        innerLevel.innerName( sourceLevel.getSourceName() );

        return innerLevel.build();
    }

    protected InnerLevel sourceInnerLevelToInnerLevel(SourceInnerLevel sourceInnerLevel) {
        if ( sourceInnerLevel == null ) {
            return null;
        }

        InnerLevel.InnerLevelBuilder innerLevel = InnerLevel.builder();

        innerLevel.innerName( sourceInnerLevel.getSourceInnerName() );
        innerLevel.id( sourceInnerLevel.getId() );
        innerLevel.name( sourceInnerLevel.getName() );

        return innerLevel.build();
    }
}

Before additional mapping @Mapping(target = "inner1.innerName", source = "sourceName") it was same name copy.

    protected InnerLevel sourceLevelToInnerLevel(SourceLevel sourceLevel) {
        if ( sourceLevel == null ) {
            return null;
        }

        InnerLevel.InnerLevelBuilder innerLevel = InnerLevel.builder();

        innerLevel.id( sourceLevel.getId() );
        innerLevel.name( sourceLevel.getName() );

        return innerLevel.build();
    }

Steps to reproduce the problem

Source objects:

import lombok.Builder;
import lombok.Data;

@Data
@Builder
public class SourceLevel {
    Integer id;
    String name;
    String sourceName;
    boolean isTrue;
    SourceInnerLevel inner;
}
import lombok.Builder;
import lombok.Data;

@Data
@Builder
public class SourceInnerLevel {
    Integer id;
    String name;
    boolean isTrue;
    String sourceInnerName;
}

Target objects:

import lombok.Builder;
import lombok.Data;

@Data
@Builder
public class TopLevel {
    InnerLevel inner1;
    InnerLevel inner2;
}
import lombok.Builder;
import lombok.Data;

@Data
@Builder
public class InnerLevel {
    Integer id;
    String name;
    boolean isTrue;
    String innerName;
}

Mapper:

import org.mapstruct.Mapper;
import org.mapstruct.Mapping;
import org.mapstruct.MappingConstants;
import org.mapstruct.control.DeepClone;

@Mapper(componentModel = MappingConstants.ComponentModel.SPRING, mappingControl = DeepClone.class)
public interface LevelMapper {
     @Mapping(target = "inner1", source = ".")
     @Mapping(target = "inner1.innerName", source = "sourceName")
     @Mapping(target = "inner2", source = "inner")
     @Mapping(target = "inner2.innerName", source = "inner.sourceInnerName")
    TopLevel mapSourceToTop(SourceLevel source);
}

Result build with incorrect sourceLevelToInnerLevel() method:

@Component
public class LevelMapperImpl implements LevelMapper {

    @Override
    public TopLevel mapSourceToTop(SourceLevel source) {
        if ( source == null ) {
            return null;
        }

        TopLevel.TopLevelBuilder topLevel = TopLevel.builder();

        topLevel.inner1( sourceLevelToInnerLevel( source ) );
        topLevel.inner2( sourceInnerLevelToInnerLevel( source.getInner() ) );

        return topLevel.build();
    }

    protected InnerLevel sourceLevelToInnerLevel(SourceLevel sourceLevel) {
        if ( sourceLevel == null ) {
            return null;
        }

        InnerLevel.InnerLevelBuilder innerLevel = InnerLevel.builder();

        innerLevel.innerName( sourceLevel.getSourceName() );

        return innerLevel.build();
    }

    protected InnerLevel sourceInnerLevelToInnerLevel(SourceInnerLevel sourceInnerLevel) {
        if ( sourceInnerLevel == null ) {
            return null;
        }

        InnerLevel.InnerLevelBuilder innerLevel = InnerLevel.builder();

        innerLevel.innerName( sourceInnerLevel.getSourceInnerName() );
        innerLevel.id( sourceInnerLevel.getId() );
        innerLevel.name( sourceInnerLevel.getName() );

        return innerLevel.build();
    }
}

MapStruct Version

1.5.5.Final

@AndrisAncans AndrisAncans changed the title Mapping from "."(target this) to target child object with additional custom mapping failed. Mapping from "."(this) to target child object with additional custom mapping failed. Dec 25, 2023
@filiphr
Copy link
Member

filiphr commented Dec 27, 2023

This is not related to #3475.

Nested target mappings are tricky. The use of source = "." is not documented and works by accident. In theory something like:

    @Mapping(target = "inner1", source = "source")
    @Mapping(target = "inner1.innerName", source = "source.sourceName")

Should correctly, work, but it doesn't. We can look into it. However, what you could do is the following:

@Mapper(mappingControl = DeepClone.class)
public interface LevelMapper {

    @Mapping(target = "inner1", source = "source")
    @Mapping(target = "inner2", source = "inner")
    @Mapping(target = "inner2.innerName", source = "inner.sourceInnerName")
    TopLevel mapSourceToTop(SourceLevel source);

    @Mapping(target = "innerName", source = "sourceName")
    InnerLevel sourceToInner(SourceLevel sourceLevel);
}

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
Projects
None yet
Development

No branches or pull requests

2 participants