ModelMapper: Choose mapping based on Child class

Simon picture Simon · Sep 24, 2018 · Viewed 7.4k times · Source

TL;DR

I want to use modelMapper in a way that I map from AbstractParent to AbstractParentDTO and later in the ModelMapper-Config call the specific mappers for each Sub-class and then skip the rest of the (abstrac-class) mappings.

How is that Possible? Is this the right approach? Is there a design flaw?


What I have:

The parent entity:

@Inheritance(strategy = InheritanceType.JOINED)
@DiscriminatorColumn(name = "type")
public abstract class Parent {
//some more fields
}

One child entity:

//Basic Lombok Annotations
@DiscriminatorValue("child_a")
public class ChildA extends Parent {
//some more fields
}

Another child entity:

@DiscriminatorValue("child_b")
public class ChildB extends Parent {
//some more fields   
}

Then I have the parent DTO class:

@JsonTypeInfo(use = JsonTypeInfo.Id.NAME)
@JsonSubTypes({
@JsonSubTypes.Type(value = ChildA.class, name = "child_a"),
@JsonSubTypes.Type(value = ChildB.class, name = "child_b"),
public abstract class ParentDTO {
//some more fields
}

One Child DTO:

public class ClassADTO extends ParentDTO {
//some more fields
}

and another DTO:

public class ClassBDTO extends ParentDTO {
//some more fields
}

In my case I'll get DTO's from the controller and map them to Entities when giving them to the Service. I'll have to do the same thing in 5-6 Endpoints.

The Endpoints look roughly like this:

@PreAuthorize(CAN_WRITE)
@PutMapping("/{id}")
public ResponseEntity<ParentDTO> update(
        @PathVariable("id") UUID id,
        @RequestBody @Valid ParentDTO parentDTO) {

    Parent parent = parentService.update(id, parentDTO);

    if (parentDTO instanceof ChildADTO) {
        return ResponseEntity.ok(modelMapper.map(parent, ChildADTO.class));
    } else if (parentDTO instanceof ChildBDTO) {
        return ResponseEntity.ok(modelMapper.map(parent, ChildBDTO.class));
    }
    throw new BadRequestException("The Parent is not Valid");
}

Only that I have a few more Childs that make things even bulkier.


What I want:

Instead of checking a bunch of times what instance the DTO (or Entity) is, I simply want to write for example:

modelmapper.map(parent, ParentDTO.class)

and do the "instance of..." check ONCE in my ModelMapper Configuration.


What I've tried:

I already have different Converters for every possible direction and mapping-case defined in my ModelMapper Configuration (since they require more complex mapping anyways).

I've tried to solve my problem by writing one more Converter for the Parent Classes and setting it as a ModelMapper PreConverter:

    //from Entity to DTO
    Converter<Parent, ParentDTO> parentParentDTOConverter = mappingContext -> {
        Parent source = mappingContext.getSource();
        ParentDTO dest = mappingContext.getDestination();

        if (source instanceof CHildA) {
            return modelMapper.map(dest, ChildADTO.class);
        } else if (source instanceof ChildB) {
            return modelMapper.map(dest, ChildBDTO.class);
        } 
        return null;
    };

and:

modelMapper.createTypeMap(Parent.class, ParentDTO.class)
                .setPreConverter(parentParentDTOConverter);

But I'm always getting the same MappingError:

1) Failed to instantiate instance of destination com.myexample.data.dto.ParentDTO. Ensure that com.myexample.data.dto.ParentDTOO has a non-private no-argument constructor.

which I get (I guess), I cannot construct an Object of an abstract class. But thats not what I'm trying, am I? I guess that modelMapper is still doing the rest of the Mapping after finishing with my PreConverter. I've also tried to set it with .setConverter but always with the same result.


  • Does anyone knows how to 'disable' the custom mappings? I don't really want to write "pseudo-mappers" that act like mappers and just call the specific mappers for each scenario.

  • Is my design just bad? How would you improve it?

  • Is this just not implemented into ModelMapper yet?

Any help and hint is appreciated.

Answer

Feedforward picture Feedforward · Sep 26, 2018

Well, the solution I found uses converters. In this case modelMapper doesn't try to create a new instance of abstract class, but uses the converter directly.

You can put all the converters in same place

modelMapper.createTypeMap(ChildA.class, ParentDTO.class)
            .setConverter(mappingContext -> modelMapper.map(mappingContext.getSource(), ClassADTO.class));

modelMapper.createTypeMap(ChildB.class, ParentDTO.class)
            .setConverter(mappingContext -> modelMapper.map(mappingContext.getSource(), ClassBDTO.class));
....