javamapstruct

Why can't I reference a @Context parameter in the @Mapping attributes?


Using mapstruct, what I need is a mapping method with several sources and that these several sources are passed around to other mapping methods so I can have all my several sources for all mapping methods where I need these additional sources.

Currently there are two features that could be made to work together maybe :

So the feature need would be either to allow secondary source parameters to be passed around to other mapping methods or to make the @Context parameter able to be referenced by a @Mapping(target="something", source="ctx.somethingElse") or @Mapping(target="ctx.something", source="somethingElse)

Example :

// source classes : `Instant timestamp` is a field I obtain separately

Instant timestamp;

class WrapperSource
   List<NestedSource> nested;

class NestedSource
    String name;



// target classes : I want to map the nested and name field but also to insert the timestamp in both the WrapperTarget and every NestedTarget in the nested list

class WrapperTarget
   Instant timestamp;
   List<NestedTarget> nested;

class NestedTarget
    String name;
    Instant timestamp;

Ideally, the mapping would be something like :

// Currently this doesn't work because we can't reference the @Context in the source attribute

@Mapping(target = "nested", source="source.nested")
@Mapping(target = "timestamp", source="timestamp")
WrapperTarget map(WrapperSource source, @Context Instant timestamp);

@Mapping(target = "name", source="source.name")
@Mapping(target = "timestamp", source="timestamp")
NestedTarget map(NestedSource source, @Context Instant timestamp);

Or :

// Currently this doesn't work because the second method with 2 sources in not called by the first generated method

@Mapping(target = "nested", source="source.nested")
@Mapping(target = "timestamp", source="timestamp")
WrapperTarget map(WrapperSource source, Instant timestamp);

@Mapping(target = "name", source="source.name")
@Mapping(target = "timestamp", source="timestamp")
NestedTarget map(NestedSource source, Instant timestamp);

The only (verbose) workaround that works for me is :

// @Context is passed around and I can manually use it as a source in an @AfterMapping but it requires additional code

WrapperTarget map(WrapperSource source, @Context Instant timestamp);

@AfterMapping
void map(WrapperSource source, @MappingTarget WrapperTarget target, @Context Instant timestamp) {
    target.setTimestamp(timestamp);
}

NestedTarget map(NestedSource source, @Context Instant timestamp);

@AfterMapping
void map(NestedSource source, @MappingTarget NestedTarget target, @Context Instant timestamp) {
    target.setTimestamp(timestamp);
}

This works alright but it required additional manual code, so a better alternative would be to be able to reference a @Context in a @Mapping's attributes. This way I could use the first "ideal" mapping example.

Is there a better workaround for this issue ?


Solution

  • @Context should be what the parameter suggests what it is. Context information to a mapping and hence not partake in the mapping itself. Mapping is in principle from source to target.

    Remember: MapStruct can solve a lot of problems, but it will never be able to solve them all.

    However: you can try this:

    class WrapperTarget implements TimeStamped
       Instant timestamp;
       List<NestedTarget> nested;
    
    class NestedTarget implements TimeStamped
        String name;
        Instance timestamp;
    
    interface TimeStamped{
       void setTimestamp(Instance timeStamp);
    }
    
    

    Define your own context... MapStruct calls the after mapping on the context you defined automatically. You can put even more things in the context like this, e.g. resolving stuff from repositories in before mapping, id's... etc.

    class MyContext {
    
        Instance timestamp;
    
        @AfterMapping
        map(@MappingTarget TimeStamped timeStamped)
    }
    

    You mapping remains clean from the context in that case. You need to initialise the context of course before you call the method. Perhaps you can construct the time instant in the construction of the context (if the requirement is that you include the same time instant everywhere)..

    @Mapping(target = "nested", source="source.nested")
    WrapperTarget map(WrapperSource source, @Context MyContext ctx);
    
    @Mapping(target = "name", source="source.name")
    NestedTarget map(NestedSource source, @Context MyContext ctx);
    
    

    For use of context, you can checkout this example.