Search code examples
javamappingmodelmapper

ModelMapper: transferring attribute from root to each elements of a list


Model

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
}

Expected mapping (example)

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")

Problem

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:

  1. Not require developers to change the mapping's code if new matching attributes were to be introduced in the model.
  2. To be done through a single method call to the ModelMapper.

Current configuration

myModelMapper.typeMap(ElementDTO.class, Element.class).implicitMappings();
myModelMapper.typeMap(RootDTO.class, Root.class).implicitMappings();

Results when using that configuration

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

Explored avenues

Idea 1

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.

Idea 2

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.

Idea 3

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.

Idea 4

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.

Idea 5

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.


Solution

  • 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