'MapStruct - Map Generic List to Non-Generic List containing different types

I am attempting to use MapStruct to convert between 2 internal models. These models are generated from the same specification and represent a large tree-like structure that is constantly being added to. The goal of using MapStruct is to have an efficient, low (non-generated) code way of performing this conversion that stays up to date with additions to the specification. As an example, my models would look like:

package com.mycompany.models.speca;

public class ModelSpecA {
    private String name;
    private int biggestNumberFound;
    private com.mycompany.models.speca.InternalModel internalModel;
    private List<com.mycompany.models.speca.InternalModel> internalModelList;
}
package com.mycompany.models.specb;

public class ModelSpecB {
    private String name;
    private int biggestNumberFound;
    private com.mycompany.models.specb.InternalModel internalModel;
    private List internalModelList;
}

with all of the expected getters and setters and no-arg constructors. MapStruct is able to generate code for the mapping extremely easily with the code looking like:

interface ModelSpecMapper {
    ModelSpecB map(ModelSpecA source);
}

From unit testing and inspecting the generated code, the mapping is accurate and complete except in one regard: the mapping of the internalModelList member in each class. The generated code looks like the following:

...
if (sourceInternalModelList != null) {
    specBTarget.setInternalModelList( specASource.getInternalModelList() );
}
...

I.e. It is mapping from the generic List<com.mycompany.models.speca.InternalModel> to the non-generic List without doing model conversion. This passes at compile time and runtime in unit tests, but will cause errors in later code when we expect to be able to cast to the SpecB version of the model.

So far, I've investigated if it is possible to force a mapping of the parameterized type in the source to its corresponding type without using expensive reflection operations, which would eliminate the gains from using MapStruct as a solution. This is my first experience with MapStruct, so there may be an obvious solution I am simply unaware of. Adding an explicit mapping is infeasible as I need this to be forward compatible with future additions to the model including new Lists.

TLDR; How do I use MapStruct to convert the contents of a generic List to a non-generic List? E.g. List<com.mycompany.a.ComplexModel> --> List whose members are of type com.mycompany.b.ComplexModel.



Solution 1:[1]

Based on suggestions by @chrylis -cautiouslyoptimistic-, I managed to successfully accomplish the mapping by using Jackson to perform the mapping directly from type to type, but that is tangential to the MapStruct problem. I was able to accomplish the stated goal of mapping a generic list to a non-generic list by adding a default mapping to my MapStruct mapper:

     /**
      * Map a generic List which contains object of any type to a non-generic List which will contain objects
      * of the resulting mapping.
      * E.g. It maps a generic list of T to a non-generic list of contents mapped from T.
      * @param source Source generic List to map from.
      * @return A non-generic List whose contents are the contents of <i>source</i> with mapping implementation applied.
      * @param <T>
      */
     default <T> List mapGenericToNonGeneric(List<T> source) {
         if (source == null) {
             return null;
         }
         if (source.isEmpty()) {
             return new LinkedList();
         }
         // Handle the most common known cases as an optimization without expensive reflection.
         final Class<?> objectClass = source.get(0).getClass();
         if (ClassUtils.isPrimitiveOrWrapper(objectClass)) {
             return new LinkedList(source);
         }
 
         if (String.class.equals(objectClass)) {
             return new LinkedList(source);
         }
    
         try {
             Method mapperMethod = Stream.of(this.getClass().getDeclaredMethods())
                     .map(method -> {
                         Parameter[] params = method.getParameters();
                         // If this method is a mapper that takes the type of our list contents
                         if (params.length == 1 && params[0].getParameterizedType().equals(objectClass)) {
                             return method;
                         }
                         return null;
                     })
                     .filter(Objects::nonNull)
                     .findFirst()
                     .orElse(null);
             if (mapperMethod != null) {
                 final List result = new LinkedList();
                 for (T sourceObject : source) {
                     result.add(mapperMethod.invoke(this, sourceObject));
                 }
                 log.info("Executed slow generic list conversion for type {}", objectClass.getName());
                 return result;
             }
         } catch (Exception e) {
             throw new RuntimeException(e);
         }
         return null;
     }

From inspecting the generated code and adding assertions to the type of each collection contents, this is passing my property-based testing suite. It still makes use of reflection to determine the parameterized type but is able to handle arbitrary additions to the model and outperforms the previous Jackson solution substantially.

Sources

This article follows the attribution requirements of Stack Overflow and is licensed under CC BY-SA 3.0.

Source: Stack Overflow

Solution Source
Solution 1 hpabst