modelmapper

Mapping nested object with Optional into flat structure


Is there a strategy in ModelMapper to map nested object with Optional into a flat structure

Without Optional this works e.g.

source.getRemainingCapacity().getValue()
destination.setRemainingCapacityValue()

but when source.getRemainingCapacity() is a Optional. The mapping is not working

source.getRemainingCapacity().get().getValue()
destination.setRemainingCapacityValue()

Is there a way todo that?


Solution

  • Yes, you can achieve this using a custom property mapping or a converter in ModelMapper to properly handle the Optional wrapper.

    Problem

    Let’s say you have the following classes:

    import java.util.Optional;
    
    public class Source {
        private Optional<RemainingCapacity> remainingCapacity;
    
        public Optional<RemainingCapacity> getRemainingCapacity() {
            return remainingCapacity;
        }
    
        public void setRemainingCapacity(Optional<RemainingCapacity> remainingCapacity) {
            this.remainingCapacity = remainingCapacity;
        }
    
        public static class RemainingCapacity {
            private Integer value;
    
            public Integer getValue() {
                return value;
            }
    
            public void setValue(Integer value) {
                this.value = value;
            }
        }
    }
    
    public class Destination {
        private Integer remainingCapacityValue;
    
        public Integer getRemainingCapacityValue() {
            return remainingCapacityValue;
        }
    
        public void setRemainingCapacityValue(Integer remainingCapacityValue) {
            this.remainingCapacityValue = remainingCapacityValue;
        }
    }
    

    Trying to access the value directly like this:

    source.getRemainingCapacity().get().getValue();
    

    ...won’t work with ModelMapper, since it doesn't automatically unwrap Optional.

    Solution 1: Use PropertyMap for custom mapping

    You can configure a custom mapping that extracts the value from the Optional:

    import org.modelmapper.ModelMapper;
    import org.modelmapper.PropertyMap;
    
    public class MappingExample {
        public static void main(String[] args) {
            ModelMapper modelMapper = new ModelMapper();
    
            modelMapper.addMappings(new PropertyMap<Source, Destination>() {
                @Override
                protected void configure() {
                    map().setRemainingCapacityValue(
                        source.getRemainingCapacity()
                              .orElse(new Source.RemainingCapacity())
                              .getValue()
                    );
                }
            });
    
            Source source = new Source();
            Source.RemainingCapacity capacity = new Source.RemainingCapacity();
            capacity.setValue(10);
            source.setRemainingCapacity(Optional.of(capacity));
    
            Destination destination = new Destination();
            modelMapper.map(source, destination);
    
            System.out.println("Mapped value: " + destination.getRemainingCapacityValue());
        }
    }
    

    Solution 2: Use a Converter (more reusable)

    Using a converter is cleaner, especially if you need to reuse the logic:

    import org.modelmapper.Converter;
    import org.modelmapper.spi.MappingContext;
    
    Converter<Source, Integer> remainingCapacityConverter = ctx ->
        ctx.getSource()
           .getRemainingCapacity()
           .map(Source.RemainingCapacity::getValue)
           .orElse(null);
    
    modelMapper.typeMap(Source.class, Destination.class).addMappings(mapper ->
        mapper.using(remainingCapacityConverter)
              .map(src -> src, Destination::setRemainingCapacityValue)
    );
    

    Notes