Search code examples
javamapstructvalue-objects

Generic mapping of Value Objects with mapstruct


Trying to use value objects in my business model, I'm facing issues with the following code :

@Mapper
public abstract class TestMapstruct {

    public ValueObjectA mapA(String value){
        return new ValueObjectA(value);
    }

    public abstract BusinessObject map(DTO dto);

    @Value
    public static class ValueObjectA {
        private String a;
    }

    @Value
    public static class ValueObjectB {
        private String b;
    }

    @Data
    public static class BusinessObject {
        private ValueObjectA a;
        private ValueObjectB b;
    }

    @Data
    public static class DTO {
        private String a;
        private String b;
    }
}

Missing mapping (String -> ValueObjectB) would lead to the following compilation error message :

Can't map property "java.lang.String b" to "test.ValueObjectB b". Consider to declare/implement a mapping method: "test.ValueObjectB map(java.lang.String value)".

I totally understand this, but I would rather not to declare a method for each of my ValueObjects (could have dozens in a project).

Is there a generic way to declare (String -> ValueObject) mapping method ?


Solution

  • There is no generic way to do this if you don't have a common interface between all the methods.

    However, if you have a common interface then it is possible. For example:

    public interface ValueObject<T> {
    
        T getValue();
    
        void setValue(T value);
    
    }
    

    Then you need a helper mapper:

    public interface ValueObjectMapper {
    
    
        static <V extends ValueObject<T>, T> T mapToValue(V valueObject) {
            return valueObject == null ? null : valueObject.getValue();
        }
    
        static <V extends ValueObject<T>, T> V mapFromValueObject(T value, @TargetType Class<V> valueObjectClass) {
            if (value == null) {
                return null;
            }
    
    
            try {
                V valueObject = valueObjectClass.getDeclaredConstructor().newInstance();
                valueObject.setValue(value);
    
                return valueObject;
            } catch (InstantiationException | IllegalAccessException | InvocationTargetException | NoSuchMethodException e) {
                throw new RuntimeException(e);
            }
        }
    
    }
    

    Edit: Add example with immutable Value objects.

    In case you want your value objects to be immutable then you can do the following:

    public interface ValueObject<T> {
        T getValue();
    }
    
    
    public interface ValueObjectMapper {
    
    
        static <V extends ValueObject<T>, T> T mapToValue(V valueObject) {
            return valueObject == null ? null : valueObject.getValue();
        }
    
        static <V extends ValueObject<T>, T> V mapFromValueObject(T value, @TargetType Class<V> valueObjectClass) {
            if (value == null) {
                return null;
            }
    
    
            try {
                V valueObject = valueObjectClass.getDeclaredConstructor(value.getClass()).newInstance(value);
    
                return valueObject;
            } catch (InstantiationException | IllegalAccessException | InvocationTargetException | NoSuchMethodException e) {
                throw new RuntimeException(e);
            }
        }
    
    }
    

    Note: To make this work you will have to make sure that all your ValueObjects have a constructor with that value.