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
DescriptionExpected 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"?

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 ActivityZegveld commented on Dec 29, 2022 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. bwgjoseph commented on Dec 30, 2022 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"?

Reacted by Eduardo Eljaiek Zegveld commented on Dec 30, 2022 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. bwgjoseph commented on Dec 30, 2022 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 Zegveld commented on Dec 30, 2022 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 bwgjoseph commented on Dec 31, 2022 ah! Thanks for the explanation.
Appreciate it. bwgjoseph commented on Dec 31, 2022 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

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! Zegveld commented on Dec 31, 2022 that is probably because update mappings are not yet supported for subclassmappings. see #2946 bwgjoseph commented on Dec 31, 2022 I did come across that issue, may I check if there is any workaround for this? bwgjoseph commented on Jan 7, 2023 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);changed the title [-]@SubclassMapping not working with @Mapping nested properties[/-] [+]`@SubclassMapping` not working with `@Mapping` nested properties and target all[/+] on Mar 17, 2023added a commit that references this issue on May 27, 2023#3126 Apply target this references in the BeanMappingMethod bwgjoseph commented on May 27, 2023
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? filiphr commented on May 27, 2023 @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.
MetadataMetadataDevelopmentNo branches or pull requestsIssue actions
|