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 — `@SubclassMapping` not working with `@Mapping` nested properties and target all · Issue #3126 · mapstruct/mapstruct

@SubclassMapping not working with @Mapping nested properties and target all #3126

New issue
Copy link
New issue
Copy link
Closed
#3281
Closed
@SubclassMapping not working with @Mapping nested properties and target all#3126
#3281
Copy link
Assignees
filiphr
Labels
bug
Milestone
1.6.0.Beta1

Description

@bwgjoseph
bwgjoseph
opened on Dec 29, 2022
Issue body actions

Expected behavior

It should be able to map the nested properties from source to target subclass.

Usage

@SubclassMapping(source = HomeAddressDO.class, target = HomeAddressResponseDto.class)
@SubclassMapping(source = OfficeAddressDO.class, target = OfficeAddressResponseDto.class)
@Mapping(source = "auditable", target = ".")
AddressResponseDto toDto(AddressDO addressDo);

Actual behavior

MapStruct complains the following:

No property named "addressDo.auditable.createdBy" exists in source parameter(s). Did you mean "auditable"?

image

Steps to reproduce the problem

I have created a reproduce at here

In summary, this is the setup

public abstract class AddressDO {
    public abstract AddressDOBuilder<?, ?> toBuilder();

    private String id;
    private Auditable auditable;
}

public class Auditable {
    private String createdBy;
    private LocalDateTime createdAt;
}

public class HomeAddressDO extends AddressDO {
  // some fields
}

public class OfficeAddressDO extends AddressDO {
  // some fields
}

public interface AddressResponseDto {}

public record HomeAddressResponseDto(String id, String street, String postalCode, String unit, String createdBy, String createdAt) implements AddressResponseDto {}

public record OfficeAddressResponseDto(String id, String building, String street, String postalCode, String unit, String createdBy, String createdAt) implements AddressResponseDto {}

@Mapper(
    componentModel = MappingConstants.ComponentModel.SPRING,
    subclassExhaustiveStrategy = SubclassExhaustiveStrategy.RUNTIME_EXCEPTION
)
public interface AddressMapper {
    @SubclassMapping(source = HomeAddressDO.class, target = HomeAddressResponseDto.class)
    @SubclassMapping(source = OfficeAddressDO.class, target = OfficeAddressResponseDto.class)
    @Mapping(source = "auditable", target = ".") // < offending line
    AddressResponseDto toDto(AddressDO addressDo);
}

MapStruct Version

1.5.3.Final

Activity

  1. bwgjoseph
    added
    bug
    on Dec 29, 2022
  2. Zegveld commented on Dec 29, 2022

    @Zegveld
    Zegveld
    on Dec 29, 2022
    Contributor
    More actions

    Interesting situation, the cause is that the Mapping annotations are first analysed and the result of this is passed on to the forged SubclassMapping methods. These new methods get different source field names assigned, for example the HomeAddressDO source becomes homeAddressDO instead of addressDo, meanwhile the mapping definition contains the term addressDo in the tree for lookup actions. The result is that addressDo.auditable.createdBy could not be found, but homeAddressDO.auditable.createdBy could be found instead.

    The easiest way to fix this is probably to retain the name of the source parameter and not redetermine this for forged methods based on SubclassMapping annotations. My preferred solution would be to do the analysis for each forged SubclassMapping instead, but this would mean quite a bit of redesign for forged methods.

  3. bwgjoseph commented on Dec 30, 2022

    @bwgjoseph
    bwgjoseph
    on Dec 30, 2022
    Author
    More actions

    Interesting, didn't realize this would be a unique use-case.

    The easiest way to fix this is probably to retain the name of the source parameter

    When you say this, it means to not create a different field in the target class?

    So my target would look like

    public record HomeAddressResponseDto(String id, String street, String postalCode, String unit, Auditable auditable) implements AddressResponseDto {}
    
    public record OfficeAddressResponseDto(String id, String building, String street, String postalCode, String unit, Auditable auditable) implements AddressResponseDto {}

    I tested it, and it does seem to work, but my ideal scenario is to flatten the ResponseDTO, so rather than

    {
        "id": "12345",
        "street": null,
        "postalCode": null,
        "unit": null,
        "auditable": {
            "createdBy": "Joseph",
            "createdAt": "2022-12-30T14:20:38.86937"
        }
    }

    I would like it to be

    {
        "id": "12345",
        "street": null,
        "postalCode": null,
        "unit": null,
        "createdBy": "Joseph",
        "createdAt": "2022-12-30T14:20:38.86937"
    }

    Is there any other way around this?

    I also tried to be explicit where I did something like

    @Mapping(source = "homeAddressDO.auditable.createdBy", target = "createdBy")

    But it will complain the same error

    No property named "homeAddressDO.auditable.createdBy" exists in source parameter(s). Did you mean "auditable"?
    

    image

    Reacted by Eduardo Eljaiek
  4. Zegveld commented on Dec 30, 2022

    @Zegveld
    Zegveld
    on Dec 30, 2022
    Contributor
    More actions

    You misunderstood me. When I mentioned The easiest way to fix this is probably to retain the name of the source parameter, I was talking about the parameter names that MapStruct assigns to the generated methods. In the case of subclassmappings something goes wrong here because it inherits the mappings but does not adjust the tree it needs to walk through.

    The workaround is the following:

    @Mapper(
        componentModel = MappingConstants.ComponentModel.SPRING,
        subclassExhaustiveStrategy = SubclassExhaustiveStrategy.RUNTIME_EXCEPTION
    )
    public interface AddressMapper {
        @SubclassMapping(source = HomeAddressDO.class, target = HomeAddressResponseDto.class)
        @SubclassMapping(source = OfficeAddressDO.class, target = OfficeAddressResponseDto.class)
        @Mapping(source = "auditable", target = ".")
        AddressResponseDto toDto(AddressDO addressDo);
    
        @InheritConfiguration( "toDto" ) // or @Mapping(source = "auditable", target = ".")
        HomeAddressResponseDto toHomeDto(HomeAddressDO addressDo);
    
        @InheritConfiguration( "toDto" ) // or @Mapping(source = "auditable", target = ".")
        OfficeAddressResponseDto toOfficeDto(OfficeAddressDO addressDo);
    }
    

    Now MapStruct does not generate the subclassing methods, instead it will use the user defined ones.


    What mapstruct wanted to do was the following:

        @SubclassMapping(source = HomeAddressDO.class, target = HomeAddressResponseDto.class)
        @SubclassMapping(source = OfficeAddressDO.class, target = OfficeAddressResponseDto.class)
        @Mapping(source = "auditable", target = ".")
        AddressResponseDto toDto(AddressDO addressDo);
    

    Would become something like the following after resolving the mappings:

        @SubclassMapping(source = HomeAddressDO.class, target = HomeAddressResponseDto.class)
        @SubclassMapping(source = OfficeAddressDO.class, target = OfficeAddressResponseDto.class)
        @Mapping(source = "addressDo.auditable.createdBy", target = "createdBy")
        @Mapping(source = "addressDo.auditable.createdAt", target = "createdAt")
        AddressResponseDto toDto(AddressDO addressDo);
    
        // so far it is still good, but here comes the inheriting mappings issue I was talking about:
        @Mapping(source = "addressDo.auditable.createdBy", target = "createdBy")
        @Mapping(source = "addressDo.auditable.createdAt", target = "createdAt")
        HomeAddressResponseDto HomeAddressDOtoHomeAddressResponseDto(HomeAddressDO homeAddressDo);
    

    you can see in the above method that the parameter name mismatches with what is listed in the source from the @Mapping statement. The fix for this inside of the MapStruct code would be to retain the name of the source parameter. In which case the following would be generated instead:

        @Mapping(source = "addressDo.auditable.createdBy", target = "createdBy")
        @Mapping(source = "addressDo.auditable.createdAt", target = "createdAt")
        HomeAddressResponseDto HomeAddressDOtoHomeAddressResponseDto(HomeAddressDO addressDo);
    

    Where this parameter now matches the source fields and thus works correctly.

    Hope this helps in understanding what goes wrong and why. I'll see if I can get a PR ready for this soon.

    Reacted by Joseph Gan
  5. Zegveld
    added a commit that references this issue on Dec 30, 2022

    mapstruct#3126: keep parameter name intact for subclassmappings to en…

    001e36e
  6. bwgjoseph commented on Dec 30, 2022

    @bwgjoseph
    bwgjoseph
    on Dec 30, 2022
    Author
    More actions

    Thanks for the detailed explanation! I get what you are saying now. I see that you have submitted the PR, and I'll use the workaround for now, until the new version is released.

    If you don't mind me asking this, and please direct me to the relevant docs if there is.

    I did a simple test on

    public interface AddressMapper {
        @SubclassMapping(source = HomeAddressDO.class, target = HomeAddressResponseDto.class)
        @SubclassMapping(source = OfficeAddressDO.class, target = OfficeAddressResponseDto.class)
        @Mapping(source = "auditable", target = ".")
        AddressResponseDto toDto(AddressDO addressDo);
    
        HomeAddressResponseDto toHomeDto(HomeAddressDO addressDo);
    
        OfficeAddressResponseDto toOfficeDto(OfficeAddressDO addressDo);
    }

    Notice I did not use @InheritConfiguration but MapStruct still uses the custom method I declared, even though it did not map for auditable but it did not use the generated methods. How does MapStruct knows to use those custom methods? Is it based on the same explanation in mapping-configuration-inheritance. However, do note that I did not define @InheritConfiguration

    Reacted by Eduardo Eljaiek
  7. Zegveld commented on Dec 30, 2022

    @Zegveld
    Zegveld
    on Dec 30, 2022
    Contributor
    More actions

    If a manually defined method matches the wanted signature that one is used instead of generating a new method.

    If you take @SubclassMapping(source = HomeAddressDO.class, target = HomeAddressResponseDto.class) for example, the source (parameter) is HomeAddressDO and the target (return type) is HomeAddressResponseDto.
    The method HomeAddressResponseDto toHomeDto(HomeAddressDO addressDo) matches these criteria so that method is used.
    Note that methods annotated by @org.mapstruct.Named will not be considered valid matching targets.

    Documentation: https://mapstruct.org/documentation/stable/reference/html/#adding-custom-methods

    Reacted by Joseph Gan
  8. bwgjoseph commented on Dec 31, 2022

    @bwgjoseph
    bwgjoseph
    on Dec 31, 2022
    Author
    More actions

    ah! Thanks for the explanation.

    Appreciate it.

  9. bwgjoseph commented on Dec 31, 2022

    @bwgjoseph
    bwgjoseph
    on Dec 31, 2022
    Author
    More actions

    hey @Zegveld,

    I'm not sure if this falls into the same category of issue. I'm happy to create a separate ticket if you think it is a different issue.

    So similar to what I'm trying to do before, but this time, I want to update the object, hence using @MappingTarget (using builder)

    So I tried to apply the same concept as before

    @SubclassMapping(source = HomeAddressRequestDto.class, target = HomeAddressDO.class)
    @SubclassMapping(source = OfficeAddressRequestDto.class, target = OfficeAddressDO.class)
    AddressDO toUpdateDomainObject(AddressRequestDto addressRequestDto, @MappingTarget AddressDO.AddressDOBuilder<?, ?> addressDOBuilder);
    
    HomeAddressDO toUpdateHomeDomainObject(HomeAddressRequestDto homeAddressRequestDto, @MappingTarget HomeAddressDO.HomeAddressDOBuilder<?, ?> homeAddressDOBuilder);
    
    OfficeAddressDO toUpdateOfficeDomainObject(OfficeAddressRequestDto officeAddressRequestDto, @MappingTarget OfficeAddressDO.OfficeAddressDOBuilder<?, ?> officeAddressDOBuilder);

    It throws The result type is not assignable to the return type error which I couldn't find anything in the issue/SO close to this particular use-case

    image

    In another mapper that I have, it works, but it doesn't have the inheritance classes, which makes it pretty straight-forward where I just need to declare something like this

    // using Address as the sample
    AddressDO toUpdateDomainObject(AddressRequestDto addressRequestDto, @MappingTarget AddressDO.AddressDOBuilder<?, ?> addressDOBuilder);

    I have updated my repo with the updated code, you can refer to my latest commit at repo

    Thanks!

  10. Zegveld commented on Dec 31, 2022

    @Zegveld
    Zegveld
    on Dec 31, 2022
    Contributor
    More actions

    that is probably because update mappings are not yet supported for subclassmappings. see #2946

  11. bwgjoseph commented on Dec 31, 2022

    @bwgjoseph
    bwgjoseph
    on Dec 31, 2022
    Author
    More actions

    I did come across that issue, may I check if there is any workaround for this?

  12. sranka
    added a commit that references this issue on Jan 2, 2023

    fix: detect collection adders using Iterable setter/getter mapstruct#…

    14e31b9
  13. bwgjoseph commented on Jan 7, 2023

    @bwgjoseph
    bwgjoseph
    on Jan 7, 2023
    Author
    More actions

    I did a workaround, and it seems to be working for my case. Not sure if I missed anything. Essentially is to construct my own default method, and cast it then let MapStruct to generate the subclass methods

    default AddressDO toUpdateDomainObject(AddressRequestDto addressRequestDto, @MappingTarget AddressDO addressDO) {
        if ( addressDO == null ) {
            return null;
        }
    
        if (addressDO instanceof HomeAddressDO) {
            return toUpdateHomeDomainObject((HomeAddressRequestDto) addressRequestDto, ((HomeAddressDO) addressDO).builder() );
        }
        else if (addressDO instanceof OfficeAddressDO) {
            return toUpdateOfficeDomainObject((OfficeAddressRequestDto) addressRequestDto, ((OfficeAddressDO) addressDO).builder() );
        }
        else {
            throw new IllegalArgumentException("Not all subclasses are supported for this mapping. Missing for " + addressDO.getClass());
        }
    }
    
    HomeAddressDO toUpdateHomeDomainObject(HomeAddressRequestDto homeAddressRequestDto, @MappingTarget HomeAddressDO.HomeAddressDOBuilder<?, ?> homeAddressDOBuilder);
    
    OfficeAddressDO toUpdateOfficeDomainObject(OfficeAddressRequestDto officeAddressRequestDto, @MappingTarget OfficeAddressDO.OfficeAddressDOBuilder<?, ?> officeAddressDOBuilder);
  14. filiphr
    added this to the 1.6.0 milestone on Mar 17, 2023
  15. filiphr
    changed the title [-]@SubclassMapping not working with @Mapping nested properties[/-] [+]`@SubclassMapping` not working with `@Mapping` nested properties and target all[/+] on Mar 17, 2023
  16. filiphr
    added 2 commits that reference this issue on May 21, 2023

    mapstruct#3126 Apply target this references in the BeanMappingMethod

    b3912f7

    mapstruct#3126 Apply target this references in the BeanMappingMethod

    e8e04e2
  17. filiphr
    self-assigned this
    on May 21, 2023
  18. filiphr
    closed this as completedin #3281on May 27, 2023
  19. filiphr
    added a commit that references this issue on May 27, 2023

    #3126 Apply target this references in the BeanMappingMethod

    c2eed45
  20. bwgjoseph commented on May 27, 2023

    @bwgjoseph
    bwgjoseph
    on May 27, 2023
    Author
    More actions

    that is probably because update mappings are not yet supported for subclassmappings. see #2946

    Thanks for the fix @filiphr. May I check if the fix includes the fix for update subclass mapping as well?

  21. filiphr commented on May 27, 2023

    @filiphr
    filiphr
    on May 27, 2023
    Member
    More actions

    @bwgjoseph you should follow #2946 fir the status if the update subclass mappings. If and when we add subclass mappings for update methods I believe that it will work as expected.

    Reacted by Joseph Gan
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

  • @filiphr
    filiphr

Labels

bug

Type

No type

Projects

No projects

    Milestone

    • 1.6.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.