Skip to content

Exceptions declared to be thrown by a mapping method, are not declared in generated mapping methods for nested types. #3142

@asjp1970

Description

@asjp1970

Expected behavior

This is the context of what I am implementing:

  • Mapping 2 beans with attributes of types for which a mapping method is generated (SubscriptionEntityMapper)
  • The mapper above is configured to use another mapper holding @BeforeMapping and @AfterMapping callback methods. This is implemented like this because several other mappers will use the same implementation (MappingValidator).
  • The callback methods throw 2 domain specific checked exceptions.

With the simple example that I enclose in section Steps to reproduce the problem, this is what I would have expected to see in the implementation:

@Generated(
    value = "org.mapstruct.ap.MappingProcessor",
    date = "2023-01-23T22:57:02+0000",
    comments = "version: 1.5.3.Final, compiler: javac, environment: Java 13.0.12 (Azul Systems, Inc.)"
)
public class SubscriptionMapperImpl implements SubscriptionMapper {

    private final MappingValidator mappingValidator = Mappers.getMapper( MappingValidator.class );

    @Override
    public SubscriptionModel fromTrafficToModel(SubscriptionTraffic trafficEntity, String mscId) throws IllegalArgumentException {
        if ( trafficEntity == null && mscId == null ) {
            return null;
        }

        SubscriptionModel.SubscriptionModelBuilder subscriptionModel = SubscriptionModel.builder();

        if ( trafficEntity != null ) {
            subscriptionModel.contextInfo( contextInfoTrafficToContextInfoModel( trafficEntity.getContextInfo() ) );
        }
        subscriptionModel.mscid( mscId );

        return subscriptionModel.build();
    }

    @Override
    public SubscriptionTraffic fromModelToTraffic(SubscriptionModel modelEntity) throws IllegalArgumentException {
        mappingValidator.doPreMappingValidations( modelEntity );

        if ( modelEntity == null ) {
            return null;
        }

        SubscriptionTraffic.SubscriptionTrafficBuilder subscriptionTraffic = SubscriptionTraffic.builder();

        subscriptionTraffic.contextInfo( contextInfoModelToContextInfoTraffic( modelEntity.getContextInfo() ) );

        mappingValidator.doAfterMappingValidations( modelEntity );

        return subscriptionTraffic.build();
    }

    protected ContextInfoModel contextInfoTrafficToContextInfoModel(ContextInfoTraffic contextInfoTraffic) throws IllegalArgumentException {
        mappingValidator.doPreMappingValidations( contextInfoTraffic );

        if ( contextInfoTraffic == null ) {
            return null;
        }

        ContextInfoModel.ContextInfoModelBuilder contextInfoModel = ContextInfoModel.builder();

        contextInfoModel.id( contextInfoTraffic.getId() );
        Map<String, String> map = contextInfoTraffic.getCtxt();
        if ( map != null ) {
            contextInfoModel.ctxt( new LinkedHashMap<String, String>( map ) );
        }

        mappingValidator.doAfterMappingValidations( contextInfoTraffic );

        return contextInfoModel.build();
    }

    protected ContextInfoTraffic contextInfoModelToContextInfoTraffic(ContextInfoModel contextInfoModel) throws IllegalArgumentException {
        mappingValidator.doPreMappingValidations( contextInfoModel );

        if ( contextInfoModel == null ) {
            return null;
        }

        ContextInfoTraffic.ContextInfoTrafficBuilder contextInfoTraffic = ContextInfoTraffic.builder();

        contextInfoTraffic.id( contextInfoModel.getId() );
        Map<String, String> map = contextInfoModel.getCtxt();
        if ( map != null ) {
            contextInfoTraffic.ctxt( new LinkedHashMap<String, String>( map ) );
        }

        mappingValidator.doAfterMappingValidations( contextInfoModel );

        return contextInfoTraffic.build();
    }
}

Actual behavior

The actual behavior, though is that MapStruct generates the protected mapping methods to map the ContextInfo attribute with the following signature:

 protected ContextInfoModel contextInfoTrafficToContextInfoModel(ContextInfoTraffic contextInfoTraffic) {
        mappingValidator.doPreMappingValidations( contextInfoTraffic );

        // mapping

        mappingValidator.doAfterMappingValidations( contextInfoTraffic );

        return contextInfoModel.build();
    }

Resulting in the compilation problem: java: unreported exception ...; must be caught or declared to be thrown in case the exceptions thrown by the callback methods are checked, which is the case in our application. This is because MapStruct included the call to the @BeforeMapping method, but as this method throws an exception, it needs to be handled, or thrown by the method stringMonitoringConfigurationMapToStringMonitoringConfigurationMap1.

In our application, we need the exception to be propagated and not caught/processed in any mapping method, so I did not try what is documented in section Exceptions.

So I see two possibilities, unless I am misconfiguring o misunderstanding something:

  1. A way to instruct MapStruct to ONLY inlcude the lifecycle methods in the main mapping methods defined by the user in the mapper interface.
  2. internal mapping methods in the generated implementation inherit the signature, as is, of the mapping methods declared in the mapper interface, to avoid compilation from failing when lifecycle methods invoked throw checked exceptions.

Steps to reproduce the problem

I attach a simplified example.

First the beans to be mapped, including an attribute to force MapStruct to create a mapping method in the implementation; quite silly but fitting the purpose:

import lombok.Builder;
import lombok.Getter;

@Builder
@Getter
public class SubscriptionModel {

    private String mscid;

    private ContextInfoModel contextInfo;

}
import lombok.Builder;
import lombok.Getter;

import java.util.Map;

@Getter
@Builder
public class ContextInfoModel {
    final String id;
    final Map<String, String> ctxt;
}
import lombok.Builder;
import lombok.Getter;

import java.util.Map;

@Getter
@Builder
public class ContextInfoTraffic {
    final String id;
    final Map<String, String> ctxt;
}
import lombok.Builder;
import lombok.Getter;

@Builder
@Getter
public class SubscriptionTraffic {

    private ContextInfoTraffic contextInfo;

}

The mapper and the additional mapper with lifecycle methods:

import org.mapstruct.Mapper;
import org.mapstruct.Mapping;

@Mapper (uses = MappingValidator.class)
public interface SubscriptionMapper {
    @Mapping(target = "mscid", source = "mscId")
    SubscriptionModel fromTrafficToModel(SubscriptionTraffic trafficEntity, String mscId) throws IllegalArgumentException;
    SubscriptionTraffic fromModelToTraffic(SubscriptionModel modelEntity) throws IllegalArgumentException;
}
import org.mapstruct.AfterMapping;
import org.mapstruct.BeforeMapping;
import org.mapstruct.Mapper;

@Mapper
public interface MappingValidator {

  @BeforeMapping
  default void doPreMappingValidations(Object source)
      throws IllegalArgumentException {
    throw new IllegalArgumentException();
  }

  @AfterMapping
  default void doAfterMappingValidations(Object source)
      throws IllegalArgumentException {
    throw new IllegalArgumentException();
  }
}

MapStruct Version

MapStruct 1.5.3

Metadata

Metadata

Assignees

Labels

Type

No type

Projects

No projects

Milestone

Relationships

None yet

Development

No branches or pull requests

Issue actions