ModelMapper: Choose mapping based on Child class - java

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.

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));
....

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.

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

Related

how to make child class deserialize without type property in Jackson while type specified in parent class

I have a service that receives my parent interface as input in order to receive different types as input.
interface is defined as below:
#JsonTypeInfo(use=JsonTypeInfo.Id.NAME, include=JsonTypeInfo.As.PROPERTY)
#JsonSubTypes({
#JsonSubTypes.Type(value = Person.class, name = "person"),
#JsonSubTypes.Type(value = Employee.class , name = "employee")
})
public interface TestInterface {
}
and i have a service that gets TestInterface as below:
#RequestMapping(value="/convert", method = RequestMethod.POST)
#ResponseBody
public Person test(#RequestBody TestInterface object){}
this service must get json like below and works just file with type property:
{"#type":"person","name":null,"family":"fami"}
the problem is when i have a service that receives the child class directly and i don't want to specify type property.
#RequestMapping(value="/convertPerson", method = RequestMethod.POST)
#ResponseBody
public Person test(#RequestBody Person object){}
in this example when i don't send type property i get (missing type id property) exception. is there any way to make it work without type id?
i already tried the below config but it didn't solve the problem:
spring.jackson.deserialization.fail-on-missing-external-type-id-property=false
solved the problem by putting #JsonTypeInfo(use = JsonTypeInfo.Id.NONE) on top of child class. with this annotation child class can work independently without type and also parent interface will work fine with types.
#JsonTypeInfo(use = JsonTypeInfo.Id.NONE)
public class Person implements TestInterface{
}

MapStruct: Is it possible to specify using the same named mapping for all nested fields of a type?

Is it possible to specify the qualifier for a nested mapper without having to specify it for each instance of a particular type of bean?
Some code to illustrate my point. I have a Parent object like:
public class ParentDTO {
ChildDTO childA;
ChildDTO childB;
ChildDTO childC;
// getters, setters, etc.
}
and I have a ChildMapper that includes more than one mapping:
#Mapper
public interface ChildMapper {
#Named("MinimalChildMapper")
#Mapping(target = "someAttribute", ignore = true)
ChildDTO toMinimalChildDTO(Child child);
#Named("ChildMapper")
ChildDTO toChildDTO(Child child);
}
I know that I can specify which child mapper to use for each child like this:
#Mapper(uses = ChildMapper.class)
public interface ParentMapper {
#Mapping(target = "childA", qualifiedByName = "MinimalChildMapping")
#Mapping(target = "childB", qualifiedByName = "MinimalChildMapping")
#Mapping(target = "childC", qualifiedByName = "MinimalChildMapping")
ParentDTO toParentDTO(Parent parent);
}
What I'm trying to figure out is if there is some way to use the mapping indicated in each qualifiedByName for the type (ChildDTO) rather that having to specify it for each instance of the type (childA, childB, childC). Is this possible?
Currently this is not possible.
However, what you could do is to have 2 ChildMapper(s). That way you can use the one with the minimal in your ParentMapper. One other option would be to defined the minimal mapping in your ParentMapper instead of reusing the ChildMapper.

How to learn Jackson to cast inheritors of abstract class?

I have a class:
#EqualsAndHashCode(callSuper = true)
#Data
public class AppealTemplateDto extends AbstractDto {
private List<AbstractFieldDto> fields;
}
This class contains list of AbstractFieldDto inheritors, e.g.:
#EqualsAndHashCode(callSuper = true)
#Data
#NoArgsConstructor
public class InputFieldDto extends AbstractFieldDto {
private String fieldType = FieldType.INPUT.name();
private String text;
}
Totally, there are near 6-7 inheritors, & AbstractTemplateDto may contain any set of them.
Controller:
#PostMapping
public ResponseEntity<AppealTemplateDto> create(#RequestBody AppealTemplateDto dto) {
return ResponseEntity.ok(service.save(dto));
}
When Jackson trying to parse AppealTemplateDto, it crashes with exception:
Caused by:
com.fasterxml.jackson.databind.exc.InvalidDefinitionException: Cannot
construct instance of
ru.appeal.template.dto.field.AbstractFieldDto
(no Creators, like default construct, exist): abstract types either
need to be mapped to concrete types, have custom deserializer, or
contain additional type information
As I understand, Jackson can't define, how to cast incoming AbstractFieldDto. Please, advice me, what to do?
The Annotation your are needing are:
#JsonTypeInfo
#JsonSubType
#JsonTypeName
Some explanation: if you have many implementation of your abstract type, Jackson can't guess which type is your json, you need to add a type name in json, for example as a new property (this is one of the strategies):
//tell to jackson where to find the type name
#JsonTypeInfo( use = JsonTypeInfo.Id.NAME, include = As.PROPERTY, property = "type")
// tell to jackson the implementations to scan
#JsonSubTypes({
#JsonSubTypes.Type(value = InputFieldDto.class, name = "input")
//, ...
})
public class AbstractFieldDto {
}
//tell to jackson what is the type name in json
#JsonTypeName("input")
public class InputFieldDto extends AbstractFieldDto {
private String fieldType = FieldType.INPUT.name();
private String text;
}

How to use Lombok's toBuilder on #SuperBuilder

Currently I have these three classes:
#Value
#NonFinal
#SuperBuilder
public class Parent {
// Some fields
}
#Value
#EqualsAndHashCode(callSuper = true)
#SuperBuilder(toBuilder = true)
public class ChildA extends Parent {
// Some fields
}
#Value
#EqualsAndHashCode(callSuper = true)
#SuperBuilder(toBuilder = true)
public class ChildB extends Parent {
// Some fields
}
I want to use it in a mapper as follows to avoid duplicating any code:
private ChildA buildChildA(Entity entity) {
Parent parent = ((ChildB) buildParent(entity, ChildA.builder().build()))
.toBuilder()
// Populate Child A fields from entity
.build();
}
private ChildB buildChildB(Entity entity) {
Parent parent = ((ChildA) buildParent(entity, ChildA.builder().build()))
.toBuilder()
// Populate Child B fields from entity
.build();
}
private Parent buildParent(Partner entity, Parent parent) {
return parent.toBuilder()
// Populate Parent fields here
.build();
}
However when I try to compile I get:
ChildA.java:13: error: method does not override or implement a method from a supertype
#SuperBuilder(toBuilder = true)
^
ChildB.java:13: error: method does not override or implement a method from a supertype
#SuperBuilder(toBuilder = true)
^
2 errors
How do you use toBuilder with #SuperBuilder? I'm using lombok v1.18.4.
If you want to use #SuperBuilder with toBuilder, all classes in the hierarchy must have toBuilder=true. The reason is that the toBuilder() method only copies the field values from its respective class, but delegates the copying of the field values from the supertypes to the supertypes' toBuilder() methods.
So just add toBuilder=true to your Parent class, too.

Jackson - #JsonTypeInfo property is being mapped as null?

I have this response:
{
"id":"decaa828741611e58bcffeff819cdc9f",
"statement":"question statement",
"exercise_type":"QUESTION"
}
Then, based on exercise_type attribute, I want to instantiate different objects instances (subclasses of ExerciseResponseDTO), so I create this mix in:
#JsonTypeInfo(
use = JsonTypeInfo.Id.NAME,
include = JsonTypeInfo.As.PROPERTY,
property = "exercise_type")
#JsonSubTypes({
#Type(value = ExerciseChoiceResponseDTO.class, name = "CHOICE"),
#Type(value = ExerciseQuestionResponseDTO.class, name = "QUESTION")})
public abstract class ExerciseMixIn
{}
public abstract class ExerciseResponseDTO {
private String id;
private String statement;
#JsonProperty(value = "exercise_type") private String exerciseType;
// Getters and setters
}
public class ExerciseQuestionResponseDTO
extends ExerciseResponseDTO {}
public class ExerciseChoiceResponseDTO
extends ExerciseResponseDTO {}
So I create my ObjectMapper as follows
ObjectMapper mapper = new ObjectMapper();
mapper.addMixIn(ExerciseResponseDTO.class, ExerciseMixIn.class);
My test:
ExerciseResponseDTO exercise = mapper.readValue(serviceResponse, ExerciseResponseDTO.class)
Assert.assertTrue(exercise.getClass() == ExerciseQuestionResponseDTO.class); // OK
Assert.assertEquals("decaa828741611e58bcffeff819cdc9f" exercise.getId()); // OK
Assert.assertEquals("question statement", exercise.getStatement()); // OK
Assert.assertEquals("QUESTION", exercise.getExerciseType()); // FAIL. Expected: "QUESTION", actual: null
The problem is that, for some reason, the exercise_type attribute being used as property on #JsonTypeInfo is being mapped as null.
Any idea how i can solve this?
Finally, I've found the solution in the API Doc
Note on visibility of type identifier: by default, deserialization
(use during reading of JSON) of type identifier is completely handled
by Jackson, and is not passed to deserializers. However, if so
desired, it is possible to define property visible = true in which
case property will be passed as-is to deserializers (and set via
setter or field) on deserialization.
So the solution was simply adding the 'visible' attribute as follows
#JsonTypeInfo(
use = JsonTypeInfo.Id.NAME,
include = JsonTypeInfo.As.PROPERTY,
property = "exercise_type",
visible = true)
#JsonSubTypes({
#Type(value = ExerciseChoiceResponseDTO.class, name = "CHOICE"),
#Type(value = ExerciseQuestionResponseDTO.class, name = "QUESTION")})
public abstract class ExerciseMixIn
{}
As per #jscherman answer by setting, 'visible' true in JsonTypeInfo will help in accessing exercise_type as a field.
If you use the same class to serialize also then resulting JSON will have exercise_type appear twice. So it's better to also update include to JsonTypeInfo.As.EXISTING_PROPERTY
And it's also worth looking at all other options for include.

Categories

Resources