We use org.modelmapper
for our bean mapping. Here is the data model (using public
for brevity):
class Root {
public String foo;
public List<Element> elements;
}
class Element {
public String foo; // <- this is the field that should be replicated from RootDTO
public String bar;
}
class RootDTO {
public String foo;
public List<ElementDTO> elements;
}
class ElementDTO {
public String bar; // notice how there is no `foo` attribute in the DTO
}
Input:
RootDTO
|-- foo = "foo"
|-- elements
|--ElementDTO(bar="bar")
|--ElementDTO(bar="something")
|--ElementDTO(bar="else")
Output:
Root
|-- foo = "foo"
|-- elements
|--Element(foo="foo", bar="bar")
|--Element(foo="foo", bar="something")
|--Element(foo="foo", bar="else")
As you can see, ModelMapper
would handle this case very easily if it wasn't for the Element.foo
field that should take its value from RootDTO.foo
.
How do I configure ModelMapper
to achieve my goal?
I'd want the solution to:
ModelMapper
.myModelMapper.typeMap(ElementDTO.class, Element.class).implicitMappings();
myModelMapper.typeMap(RootDTO.class, Root.class).implicitMappings();
Root mappedRoot = myModelMapper.map(rootDto, Root.class);
assertEquals(rootDto.foo, mappedRoot.foo);
assertEquals(rootDto.elements.size(), mappedRoot.elements.size());
// let's assume we have 1 Element
assertEquals(rootDto.elements.get(0).bar, mappedRoot.elements.get(0).bar);
assertEquals(rootDto.foo, mappedRoot.elements.get(0).foo); // FAILS! But this is what I want
// indeed, the value of `mappedRoot.elements.get(0).foo` is `null` because it was not mapped
It seems to me like if I could set an order I could simply configure it this way:
Converter<RootDTO, Root> replicateFooValue = new Converter<>() {
@Override
public Root convert(MappingContext<RootDTO, Root> context) {
final String valueToReplicate = context.getSource().foo;
for (Element elem : context.getDestination().elements) {
elem.foo = valueToReplicate;
}
return context.getDestination();
}
};
myModelMapper.typeMap(ElementDTO.class, Element.class).implicitMappings();
myModelMapper.typeMap(RootDTO.class, Root.class).implicitMappings()
.thenUseConverter(replicateFooValue);
... but I do not think this is possible.
If I could use the ModelMapper
's mapping functionality from within the Converter<>
itself, then I could use the implicitMappings()
before trying to simply set the value I want, but once again: I do not think that this is possible.
One way to be sure that it'd work would be:
Converter<RootDTO, Root> myCustomConverter = new Converter<>() {
@Override
public Root convert(MappingContext<RootDTO, Root> context) {
final Root mappedRoot = new Root();
// map each field individually, by hand
return mappedRoot;
}
};
myModelMapper.addConverter(myCustomConverter);
... but it requires maintenance if I was to add new fields in my data model.
Add a Converter<RootDTO, Element>
that would only populate the foo
value. That would result in the following usage:
Root mappedRoot = myModelMapper.map(rootDto, Root.class);
mappedRoot.elements.forEach(e -> myModelMapper.map(rootDto, e));
This is not ideal because then the entire expected behavior is not encapsulated in a single call: the developers have to know (and remember) to make that second call as well to achieve the desired mapping.
Make a utility class that encompasses the logic shown by Idea 4
.
This is also a bad idea because it requires developers to know (and remember) about why they need to do that specific mapping this way instead of using the ModelMapper
.
Turns out there is a way to introduce some ordering. Here is the solution:
Converter<RootDTO, Root> finishMapping = new Converter<>() {
@Override
public Root convert(MappingContext<RootDTO, Root> context) {
final String srcFoo = context.getSource();
var dest = context.getDestination();
dest.elements.stream().forEach(e -> e.foo = srcFoo);
return dest;
}
};
myModelMapper.typeMap(ElementDTO.class, Element.class).implicitMappings();
myModelMapper.typeMap(RootDTO.class, Root.class).implicitMappings()
.setPostConverter(finishMapping); // this is executed AFTER the implicit mapping