Skip to content
Open
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Next Next commit
feat: add @MappingSource annotation for implicit mapping control
Support:
1. enabling Map-to-Bean implicit mapping in multi-source
2.disabling implicit mapping for bean
  • Loading branch information
Yang committed Jun 17, 2025
commit 399458b2dd7c9d5d00e6dccc5ebf0adb488fcb90
73 changes: 73 additions & 0 deletions core/src/main/java/org/mapstruct/MappingSource.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,73 @@
/*
* Copyright MapStruct Authors.
*
* Licensed under the Apache License version 2.0, available at http://www.apache.org/licenses/LICENSE-2.0
*/
package org.mapstruct;

import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;

/**
* Marks a parameter with specific handling behavior during mapping.
* This annotation controls how source parameters are handled in multi-source mapping scenarios.
* <p>
* By default,:
* - all parameters have their properties automatically used for implicit mapping
* - Map parameters in multi-source scenarios don't have their entries automatically used for implicit mapping
* <p>
* The implicitMapping attribute allows overriding these defaults:
* - For Bean parameters: setting implicitMapping=false prevents automatic property expansion
* - For Map parameters: setting implicitMapping=true enables using Map entries for implicit mapping
* <p>
* This annotation is primarily useful in multi-source parameter scenarios.
*
* <pre><code class='java'>
* // Preventing a bean parameter's properties from being used in implicit mapping
* {@literal @}Mapper
* public interface MultiSourceMapper {
* {@literal @}Mapping(target = "name", source = "otherSource.name")
* TargetDto map(
* {@literal @}MappingSource(implicitMapping = false) UserEntity user,
* OtherSource otherSource);
* }
*
* // Enabling Map entries for implicit mapping in multi-source scenarios
* {@literal @}Mapper
* public interface MultiSourceMapper {
* {@literal @}Mapping(target = "id", source = "entity.id")
* TargetDto map({@literal @}MappingSource(implicitMapping = true)
* Map&lt;String, Object&gt; sourceMap, Entity entity);
* }
* </code></pre>
*
* @since 1.6.0
*/
@Target(ElementType.PARAMETER)
@Retention(RetentionPolicy.CLASS)
public @interface MappingSource {

/**
* Controls whether this parameter participates in implicit mapping.
* The effect depends on the parameter type:
* <p>
* - For Bean parameters:
* true (default): Properties are automatically used for implicit mapping
* false: Properties are not automatically used for implicit mapping
* <p>
* - For Map parameters (in multi-source scenarios):
* true: Map entries are used for implicit mapping (similar to single-source behavior)
* false (default): Map entries are not automatically used for implicit mapping
* <p>
* - For other types (Collection, Path, etc.):
* This setting has no effect as these types are never automatically expanded
* <p>
* Note: This setting only affects implicit mapping. Explicit mappings defined with
* {@literal @}Mapping annotations are always processed regardless of this setting.
*
* @return whether implicit mapping should be enabled for this parameter
*/
boolean implicitMapping() default true;
}
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,8 @@
import org.mapstruct.Context;
import org.mapstruct.DecoratedWith;
import org.mapstruct.EnumMapping;
import org.mapstruct.Ignored;
import org.mapstruct.IgnoredList;
import org.mapstruct.InheritConfiguration;
import org.mapstruct.InheritInverseConfiguration;
import org.mapstruct.IterableMapping;
Expand All @@ -26,8 +28,7 @@
import org.mapstruct.Mapper;
import org.mapstruct.MapperConfig;
import org.mapstruct.Mapping;
import org.mapstruct.Ignored;
import org.mapstruct.IgnoredList;
import org.mapstruct.MappingSource;
import org.mapstruct.MappingTarget;
import org.mapstruct.Mappings;
import org.mapstruct.Named;
Expand Down Expand Up @@ -68,6 +69,7 @@
@GemDefinition(TargetType.class)
@GemDefinition(TargetPropertyName.class)
@GemDefinition(MappingTarget.class)
@GemDefinition(MappingSource.class)
@GemDefinition(DecoratedWith.class)
@GemDefinition(MapperConfig.class)
@GemDefinition(InheritConfiguration.class)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -1696,12 +1696,16 @@ private SourceReference getSourceRefByTargetName(Parameter sourceParameter, Stri

SourceReference sourceRef = null;

if ( sourceParameter.getType().isPrimitive() || sourceParameter.getType().isArrayType() ) {
return sourceRef;
if ( ( sourceParameter.isMappingSource() && !sourceParameter.isImplicitMapping() )
|| sourceParameter.getType().isPrimitive()
|| sourceParameter.getType().isArrayType() ) {
return null;
}

boolean allowedMapToBean =
method.getSourceParameters().size() == 1 || ( sourceParameter.isImplicitMapping() );
ReadAccessor sourceReadAccessor = sourceParameter.getType()
.getReadAccessor( targetPropertyName, method.getSourceParameters().size() == 1 );
.getReadAccessor( targetPropertyName, allowedMapToBean );
if ( sourceReadAccessor != null ) {
// property mapping
PresenceCheckAccessor sourcePresenceChecker =
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@
import javax.lang.model.element.VariableElement;

import org.mapstruct.ap.internal.gem.ContextGem;
import org.mapstruct.ap.internal.gem.MappingSourceGem;
import org.mapstruct.ap.internal.gem.MappingTargetGem;
import org.mapstruct.ap.internal.gem.SourcePropertyNameGem;
import org.mapstruct.ap.internal.gem.TargetPropertyNameGem;
Expand All @@ -35,6 +36,8 @@ public class Parameter extends ModelElement {
private final boolean mappingContext;
private final boolean sourcePropertyName;
private final boolean targetPropertyName;
private final boolean mappingSource;
private final boolean implicitMapping;

private final boolean varArgs;

Expand All @@ -48,12 +51,18 @@ private Parameter(Element element, Type type, boolean varArgs) {
this.mappingContext = ContextGem.instanceOn( element ) != null;
this.sourcePropertyName = SourcePropertyNameGem.instanceOn( element ) != null;
this.targetPropertyName = TargetPropertyNameGem.instanceOn( element ) != null;

// Handle MappingSource annotation
MappingSourceGem mappingSourceGem = MappingSourceGem.instanceOn( element );
this.mappingSource = mappingSourceGem != null;
this.implicitMapping = this.mappingSource && mappingSourceGem.implicitMapping().get();

this.varArgs = varArgs;
}

private Parameter(String name, Type type, boolean mappingTarget, boolean targetType, boolean mappingContext,
boolean sourcePropertyName, boolean targetPropertyName,
boolean varArgs) {
boolean sourcePropertyName, boolean targetPropertyName, boolean mappingSource,
boolean implicitMapping, boolean varArgs) {
this.element = null;
this.name = name;
this.originalName = name;
Expand All @@ -63,11 +72,13 @@ private Parameter(String name, Type type, boolean mappingTarget, boolean targetT
this.mappingContext = mappingContext;
this.sourcePropertyName = sourcePropertyName;
this.targetPropertyName = targetPropertyName;
this.mappingSource = mappingSource;
this.implicitMapping = implicitMapping;
this.varArgs = varArgs;
}

public Parameter(String name, Type type) {
this( name, type, false, false, false, false, false, false );
this( name, type, false, false, false, false, false, false, false, false );
}

public Element getElement() {
Expand Down Expand Up @@ -105,7 +116,8 @@ private String format() {
+ ( mappingContext ? "@Context " : "" )
+ ( sourcePropertyName ? "@SourcePropertyName " : "" )
+ ( targetPropertyName ? "@TargetPropertyName " : "" )
+ "%s " + name;
+ ( mappingSource ? "@MappingSource " : "" )
+ "%s " + name;
}

@Override
Expand All @@ -129,16 +141,24 @@ public boolean isSourcePropertyName() {
return sourcePropertyName;
}

public boolean isMappingSource() {
return mappingSource;
}

public boolean isImplicitMapping() {
return implicitMapping;
}

public boolean isVarArgs() {
return varArgs;
}

public boolean isSourceParameter() {
return !isMappingTarget() &&
!isTargetType() &&
!isMappingContext() &&
!isSourcePropertyName() &&
!isTargetPropertyName();
!isTargetPropertyName() &&
( !isMappingContext() || isMappingSource() );
}

@Override
Expand Down Expand Up @@ -183,6 +203,8 @@ public static Parameter forForgedMappingTarget(Type parameterType) {
false,
false,
false,
false,
false,
false
);
}
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,101 @@
/*
* Copyright MapStruct Authors.
*
* Licensed under the Apache License version 2.0, available at http://www.apache.org/licenses/LICENSE-2.0
*/
package org.mapstruct.ap.test.mappingsource;

import java.util.Map;

import org.mapstruct.Mapper;
import org.mapstruct.MappingSource;
import org.mapstruct.ReportingPolicy;
import org.mapstruct.factory.Mappers;

@Mapper(unmappedTargetPolicy = ReportingPolicy.IGNORE)
public interface MappingSourceImplicitMappingMapper {

MappingSourceImplicitMappingMapper INSTANCE = Mappers.getMapper( MappingSourceImplicitMappingMapper.class );

MapTarget multiSourceWithImplicitMap(@MappingSource Map<String, String> mapSource, OtherSource otherSource);

BeanTarget multiSourceWithImplicitBean(@MappingSource(implicitMapping = false) BeanSource beanSource,
OtherSource otherSource);

MapTarget singleWithImplicitMap(@MappingSource(implicitMapping = false) Map<String, String> map);

class MapTarget {
private String mapId;
private String mapName;

public String getMapId() {
return mapId;
}

public void setMapId(String mapId) {
this.mapId = mapId;
}

public String getMapName() {
return mapName;
}

public void setMapName(String mapName) {
this.mapName = mapName;
}
}

class BeanTarget {
private Integer beanId;
private String beanName;

public Integer getBeanId() {
return beanId;
}

public void setBeanId(Integer beanId) {
this.beanId = beanId;
}

public String getBeanName() {
return beanName;
}

public void setBeanName(String beanName) {
this.beanName = beanName;
}
}

class BeanSource {
private Integer beanId;
private String beanName;

public Integer getBeanId() {
return beanId;
}

public void setBeanId(Integer beanId) {
this.beanId = beanId;
}

public String getBeanName() {
return beanName;
}

public void setBeanName(String beanName) {
this.beanName = beanName;
}
}

class OtherSource {
private final Long id;

public OtherSource(Long id) {
this.id = id;
}

public Long getId() {
return id;
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,63 @@
/*
* Copyright MapStruct Authors.
*
* Licensed under the Apache License version 2.0, available at http://www.apache.org/licenses/LICENSE-2.0
*/
package org.mapstruct.ap.test.mappingsource;

import java.util.HashMap;
import java.util.Map;

import org.mapstruct.ap.test.mappingsource.MappingSourceImplicitMappingMapper.BeanSource;
import org.mapstruct.ap.test.mappingsource.MappingSourceImplicitMappingMapper.BeanTarget;
import org.mapstruct.ap.test.mappingsource.MappingSourceImplicitMappingMapper.MapTarget;
import org.mapstruct.ap.testutil.IssueKey;
import org.mapstruct.ap.testutil.ProcessorTest;
import org.mapstruct.ap.testutil.WithClasses;

import static org.assertj.core.api.Assertions.assertThat;

@IssueKey("2559")
@WithClasses(MappingSourceImplicitMappingMapper.class)
public class MappingSourceImplicitMappingTest {

@ProcessorTest
public void testMultiSourceWithImplicitMapping() {
Map<String, String> mapSource = new HashMap<>();
mapSource.put( "mapId", "1" );
mapSource.put( "mapName", "MapTest" );

BeanSource beanSource = new MappingSourceImplicitMappingMapper.BeanSource();
beanSource.setBeanId( 2 );
beanSource.setBeanName( "BeanTest" );

MapTarget mapTarget = MappingSourceImplicitMappingMapper.INSTANCE.multiSourceWithImplicitMap(
mapSource,
null
);

assertThat( mapTarget ).isNotNull();
assertThat( mapTarget.getMapId() ).isEqualTo( "1" );
assertThat( mapTarget.getMapName() ).isEqualTo( "MapTest" );

BeanTarget beanTarget = MappingSourceImplicitMappingMapper.INSTANCE.multiSourceWithImplicitBean(
beanSource,
null
);
assertThat( beanTarget ).isNotNull();
assertThat( beanTarget.getBeanId() ).isNull();
assertThat( beanTarget.getBeanName() ).isNull();
}

@ProcessorTest
public void shouldDisableImplicitMappingForSingleMapSource() {
Map<String, String> mapSource = Map.of( "mapId", "1", "mapName", "MapTest" );

MapTarget mapTarget = MappingSourceImplicitMappingMapper.INSTANCE.singleWithImplicitMap( mapSource );

assertThat( mapTarget ).isNotNull();
assertThat( mapTarget.getMapId() ).isNull();
assertThat( mapTarget.getMapName() ).isNull();
}
}

Loading