ModelMapper: Choose mapping based on Child class
Asked Answered
A

4

12

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.

Aesir answered 24/9, 2018 at 8:32 Comment(0)
C
6

I would use ObjectMapper instead of ModelMapper.

In Parent class add the possibility to get the discriminator value.

//..
public class Parent {

    @Column(name = "type", insertable = false, updatable = false)
    private String type;
    //getters and setters
}

Your ParentDTO should be mapped to Child(*)DTO

@JsonTypeInfo(
        use = JsonTypeInfo.Id.NAME,
        include = JsonTypeInfo.As.PROPERTY,
        property = "type")
@JsonSubTypes({
        @JsonSubTypes.Type(value = ChildADTO.class, name = "child_a"),
        @JsonSubTypes.Type(value = ChildBDTO.class, name = "child_b")
})
 public abstract class ParentDTO {
   // ..
 }

in the conversion service/method add an object mapper with ignore unknown (to ignore what you did not declare in your DTO class)

    ObjectMapper objectMapper = new ObjectMapper();
    objectMapper.configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false);

just simply call :

Parent parent = // get from repository
ParentDTO parentDTO = objectMapper.readValue(objectMapper.writeValueAsBytes(parent), ParentDTO.class);

In this way, your ParentDTO is always instantiated with the right type.

Cantone answered 26/9, 2018 at 18:0 Comment(1)
I can't seem to be able to get this to function. I had to remove the insertable = false, because otherwise I couldn't map it again. I also had some odd issues with the type value not actually mapping itself, which was odd, because it obviously could. Then it mapped to a Parent instead of a ChildA, and I was done with getting this to work... The only real difference I can see is that my Parent is not abstract. You can get exact implementations at your request. Any help would be much appreciated.Omaomaha
B
10

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));
....
Bumf answered 26/9, 2018 at 17:11 Comment(0)
C
6

I would use ObjectMapper instead of ModelMapper.

In Parent class add the possibility to get the discriminator value.

//..
public class Parent {

    @Column(name = "type", insertable = false, updatable = false)
    private String type;
    //getters and setters
}

Your ParentDTO should be mapped to Child(*)DTO

@JsonTypeInfo(
        use = JsonTypeInfo.Id.NAME,
        include = JsonTypeInfo.As.PROPERTY,
        property = "type")
@JsonSubTypes({
        @JsonSubTypes.Type(value = ChildADTO.class, name = "child_a"),
        @JsonSubTypes.Type(value = ChildBDTO.class, name = "child_b")
})
 public abstract class ParentDTO {
   // ..
 }

in the conversion service/method add an object mapper with ignore unknown (to ignore what you did not declare in your DTO class)

    ObjectMapper objectMapper = new ObjectMapper();
    objectMapper.configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false);

just simply call :

Parent parent = // get from repository
ParentDTO parentDTO = objectMapper.readValue(objectMapper.writeValueAsBytes(parent), ParentDTO.class);

In this way, your ParentDTO is always instantiated with the right type.

Cantone answered 26/9, 2018 at 18:0 Comment(1)
I can't seem to be able to get this to function. I had to remove the insertable = false, because otherwise I couldn't map it again. I also had some odd issues with the type value not actually mapping itself, which was odd, because it obviously could. Then it mapped to a Parent instead of a ChildA, and I was done with getting this to work... The only real difference I can see is that my Parent is not abstract. You can get exact implementations at your request. Any help would be much appreciated.Omaomaha
R
0

How about

    TypeMap<Parent.class, ParentDTO.class> typeMap = modelMapper.createTypeMap(Parent.class, ParentDTO.class);

    typeMap
     .include(ChildA .class, ClassADTO .class)
    .include(ChildB.class, ClassbDTO.class);

reference :http://modelmapper.org/user-manual/type-map-inheritance

Rossie answered 28/9, 2018 at 17:41 Comment(1)
I tried it and unfortunately this does not do what original question wants. It includes the Parent->ParentDTO mapping information when mapping ChildA->ClassADTO, but trying to map parent (which is instance of some child) to ParentDTO.class will not try to map it to ClassADTO, but to ParentDTO. Answer from Axel P works though (except for some corner cases).Alban
C
0

I've created a converter that uses Jackson annotations for inheritance so if you are using Jackson and have a lot of these, you wouldn't need to do create mapping twice (once for Jackson and once for ModelMapper).

public class JacksonInheritanceConverter<S, D> implements Converter<S, D> {

  @SneakyThrows({IntrospectionException.class, IllegalAccessException.class, InvocationTargetException.class})
  public D convert(MappingContext<S, D> context) {
    var destinationType = context.getDestinationType();
    var jsonTypeInfo = destinationType.getAnnotation(JsonTypeInfo.class);
    var discriminator = new PropertyDescriptor(jsonTypeInfo.property(), context.getSourceType()).getReadMethod().invoke(context.getSource());
    var subclassType = Arrays.stream(destinationType.getAnnotation(JsonSubTypes.class).value())
        .filter(type -> type.name().equals(discriminator.toString()))
        .findFirst()
        .orElseThrow(() -> new IllegalArgumentException("Source: " + context.getSourceType().getSimpleName() + " Discriminator: " + discriminator))
        .value();

    var finalMappingContext = context.create(context.getSource(), subclassType);
    var result = context.getMappingEngine().map(finalMappingContext);
    return context.getDestinationType().cast(result);
  }
}

But I couldn't find a way to assign this to all mapping where it would trigger automatically as soon as it sees @JsonTypeInfo. So still need to go through the list of inheritance based classes manually.

modelMapper.createTypeMap(ChildA.class, ParentDTO.class)
            .setConverter(new JacksonInheritanceConverter<>());
Conversazione answered 5/12, 2023 at 8:36 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.