javamodelmapper

ModelMapper incorrectly maps to nested object property


I am using ModelMapper to map DTOs to Entities and vice versa. I have a problem when a I am mapping a ChildDTO to a Child where both the Child class and the Parent class have a property with the same name.

These are the objects:

public class Child {

    @Id
    @EqualsAndHashCode.Include
    @GeneratedValue(strategy= GenerationType.IDENTITY)
    private Long id;

    private String name; // <---- property called 'name'
    // other properties

    @ManyToOne(fetch = FetchType.LAZY)
    @JoinColumn(name = "parent_id")
    private Parent parent;
}

public class Parent {

    @Id
    @EqualsAndHashCode.Include
    @GeneratedValue(strategy= GenerationType.IDENTITY)
    private Long id;

    private String name; // <---- property called 'name'
    // other properties

    @OneToMany(mappedBy = "parent", cascade = CascadeType.ALL, orphanRemoval = true)
    private List<Child> items = new ArrayList<>();
}

public class ChildDTO {

    @JsonProperty(access = JsonProperty.Access.READ_ONLY)
    private Long id;

    private String name;
    // other properties

    @JsonProperty(access = JsonProperty.Access.READ_ONLY)
    private Long parentId;
}

And this is the code that does the mapping:

ModelMapper modelMapper = new ModelMapper();
modelMapper.getConfiguration().setPropertyCondition(Conditions.isNotNull());
modelMapper.getConfiguration().setAmbiguityIgnored(true);
modelMapper.getConfiguration().setMatchingStrategy(MatchingStrategies.STRICT);

Child childItem = repository.findById(id);

ChildDTO childItemDTO = new ChildDTO();
childItemDTO.setName("new name");

modelMapper.map(childItemDTO, childItem);

This results in both Child::name and Parent::name to change to "new name". Notice I'm already using the STRICT mapping strategy.

I could also not find way of skipping calling Parent::setName() when mapping from ChildDTO to Child

ModelMapper version: 2.3.2


Solution

  • I encountered a challenge where Model Mapper attempted to map nested fields with the same abstract class as the parent, resulting in the engine mapping values to the child properties. To address this issue, I devised a solution by extending the matching strategy to avoid mapping unexpected nested fields. example :

    abstract class AbstractClass{
        String ref;
    }
    
    class Parent extends AbstractClass{
        ...
        
    }
    
    class Child extends AbstractClass{
        Parent parent;
        ...
    }
    

    when i try to map a Child object whith this data

    { ref: "CHILD-REF"; parent: null } to another Child object { ref: null, parent: { ref: "PARENT-REF" } }

    the beahvior by default will be a Child object : { ref: "CHILD-REF", parent: { ref: "CHILD-REF" } }

    and using my new condition the result will be { ref: "CHILD-REF", parent: { ref: "PARENT-REF" } }

    here the details

    ...
    final ModelMapper mapper = new ModelMapper();
    ...
    mapper.getConfiguration().setMatchingStrategy(new CustomModelMapperMapping());
    

    extending the MatchingStrategies.STANDARD

    public class CustomModelMapperMapping implements MatchingStrategy {
    
        static final MatchingStrategy standardMatchingStrategy = MatchingStrategies.STANDARD;
    
        NuagikeModelMapperMapping() {
        }
    
        public boolean matches(PropertyNameInfo propertyNameInfo) {
    
            //this is the strandard behavior
            final boolean standard_matching_result = standardMatchingStrategy.matches(propertyNameInfo);
            
            //and then i added new condition
            if (standard_matching_result) {
    
                final String sourceSubTokens = propertyNameInfo.getSourcePropertyTokens()
                        .stream()
                        .flatMap(s -> StreamSupport.stream(s.spliterator(), false))
                        .collect(Collectors.joining("-")).toLowerCase();
                final String destinationSubTokens = propertyNameInfo.getDestinationPropertyTokens()
                        .stream()
                        .flatMap(s -> StreamSupport.stream(s.spliterator(), false))
                        .collect(Collectors.joining("-")).toLowerCase();
    
                return sourceSubTokens.equals(destinationSubTokens);
            }
    
            return false;
        }
    
        public boolean isExact() {
            return false;
        }
    
        public String toString() {
            return "my custom strategy";
        }
    }