Skip to content

Do not require subclassExhaustiveStrategy when source is a sealed class and all subtypes are specified #3054

@mjustin

Description

@mjustin

Use case

The @SubclassMapping and @BeanMapping(subclassExhaustiveStrategy = SubclassExhaustiveStrategy.RUNTIME_EXCEPTION) annotations can be combined to allow mapping to an abstract class or interface. If a given source input is not one of the specified types, a RuntimeException will be thrown.

@SubclassMapping(target = SubclassTarget.class, source = SubclassSource.class)
@BeanMapping(subclassExhaustiveStrategy = SubclassExhaustiveStrategy.RUNTIME_EXCEPTION)
AbstractTarget map(AbstractSource source);

=>

@Override
public AbstractTarget map(AbstractSource source) {
    if ( source == null ) {
        return null;
    }

    if (source instanceof SubclassSource) {
        return subclassSourceToSubclassTarget( (SubclassSource) source );
    }
    else {
        throw new IllegalArgumentException("Not all subclasses are supported for this mapping. Missing for " + source.getClass());
    }
}

However, in certain scenarios it is possible to guarantee at compile time that all possible source types are accounted for. In this case, subclassExhaustiveStrategy seems superfluous, and could arguably be dropped. One scenario where we know all source types are accounted for is if the source is a sealed class (or sealed interface), and all classes it permits are handled.

Example

Take the following type mapper & type hierarchy:

@Mapper
public interface SealedSourceMapper {
    @SubclassMapping(target = FinalSubclassTarget.class, source = FinalSubclassSource.class)
    @SubclassMapping(target = FinalGrandchildTarget.class, source = FinalGrandchildSource.class)
    @SubclassMapping(target = NonSealedSubclassTarget.class, source = NonSealedSubclassSource.class)
    Target mapToAbstractClass(SealedAbstractClassSource source);
public abstract sealed class SealedAbstractClassSource 
        permits FinalSubclassSource, SealedSubclassSource, NonSealedSubclassSource {
    private String property;
    public String getProperty() {return property;}
    public void setProperty(String property) {this.property = property;}
}

public final class FinalSubclassSource extends SealedAbstractClassSource {
}

public abstract sealed class SealedSubclassSource extends SealedAbstractClassSource 
        permits FinalGrandchildSource {
}

public final class FinalGrandchildSource extends SealedSubclassSource {
}

public non-sealed class NonSealedSubclassSource extends SealedAbstractClassSource {
}

public abstract class Target {
    private String property;
    public String getProperty() {return property;}
    public void setProperty(String property) {this.property = property;}
}

public class FinalSubclassTarget extends Target {
}

public class FinalGrandchildTarget extends Target {
}

public class NonSealedSubclassTarget extends Target {
}

In this case, at compile time, it can be determined that every possible type of the SealedAbstractClassSource source argument is accounted for. Therefore, specifying a subclassExhaustiveStrategy should not be required since it is known at compile time to be exhaustive.

Note: this is just as applicable if the source type is a sealed interface as if it were a sealed class.

Generated Code

public class SealedSourceMapperImpl implements SealedSourceMapper {
    @Override
    public Target mapToAbstractClass(SealedAbstractClassSource source) {
        if ( source == null ) {
            return null;
        }

        if (source instanceof FinalSubclassSource) {
            return finalSubclassSourceToFinalSubclassTarget( (FinalSubclassSource) source );
        }
        else if (source instanceof FinalGrandchildSource) {
            return finalGrandchildSourceToFinalGrandchildTarget( (FinalGrandchildSource) source );
        }
        else if (source instanceof NonSealedSubclassSource) {
            return nonSealedSubclassSourceToNonSealedSubclassTarget( (NonSealedSubclassSource) source );
        }
        else {
            // Same exception type as "Pattern Matching for switch" JEP if incompatible separate compilation
            throw new IncompatibleClassChangeError("Not all subclasses are supported for this mapping. Missing for " + source.getClass());
        }
    }
}

Possible workarounds

This is a minor quality of life enhancement that can easily be worked around by adding @BeanMapping(subclassExhaustiveStrategy = SubclassExhaustiveStrategy.RUNTIME_EXCEPTION) to the mapping method.

    @SubclassMapping(target = FinalSubclassTarget.class, source = FinalSubclassSource.class)
    @SubclassMapping(target = FinalGrandchildTarget.class, source = FinalGrandchildSource.class)
    @SubclassMapping(target = NonSealedSubclassTarget.class, source = NonSealedSubclassSource.class)
    @BeanMapping(subclassExhaustiveStrategy = SubclassExhaustiveStrategy.RUNTIME_EXCEPTION)
    Target mapToAbstractClass(SealedAbstractClassSource source);

MapStruct Version

org.mapstruct:mapstruct:1.5.3.Final

Metadata

Metadata

Assignees

Type

No type

Projects

No projects

Milestone

Relationships

None yet

Development

No branches or pull requests

Issue actions