55import io .beanmapper .annotations .BeanRecordConstruct ;
66import io .beanmapper .annotations .BeanRecordConstructMode ;
77import io .beanmapper .config .Configuration ;
8+ import io .beanmapper .core .BeanProperty ;
9+ import io .beanmapper .core .BeanPropertyCreator ;
10+ import io .beanmapper .core .BeanPropertyMatchupDirection ;
811import io .beanmapper .core .converter .BeanConverter ;
912import io .beanmapper .core .inspector .PropertyAccessor ;
1013import io .beanmapper .core .inspector .PropertyAccessors ;
1114import io .beanmapper .exceptions .BeanInstantiationException ;
15+ import io .beanmapper .exceptions .BeanNoSuchPropertyException ;
1216import io .beanmapper .exceptions .RecordConstructorConflictException ;
1317import io .beanmapper .exceptions .RecordNoAvailableConstructorsExceptions ;
1418import io .beanmapper .utils .BeanMapperTraceLogger ;
@@ -65,8 +69,9 @@ public <S, T> T map(final S source) {
6569
6670 Map <String , PropertyAccessor > sourcePropertyAccessors = getSourcePropertyAccessors (source );
6771 Constructor <T > constructor = (Constructor <T >) getSuitableConstructor (sourcePropertyAccessors , targetClass );
68- String [] fieldNamesForConstructor = getNamesOfConstructorParameters (targetClass , constructor );
69- List <Object > values = getValuesOfFields (source , sourcePropertyAccessors , Arrays .stream (fieldNamesForConstructor ));
72+ String [] parameterNames = getParameterNames (constructor );
73+ String [] beanPropertyPaths = getNamesOfRecordComponents (targetClass );
74+ List <Object > values = getValuesOfFieldsWithNestedPathSupport (source , sourcePropertyAccessors , parameterNames , beanPropertyPaths );
7075
7176 return targetClass .cast (constructTargetObject (constructor , values ));
7277 }
@@ -98,46 +103,58 @@ private <S> Map<String, PropertyAccessor> getSourcePropertyAccessors(final S sou
98103 * <p>The names of the RecordComponents are retrieved either from the {@link RecordComponent#getName()}-method, or
99104 * from an available {@link io.beanmapper.annotations.BeanProperty BeanProperty}-annotation.</p>
100105 *
106+ * <p>Note: Since @BeanProperty does not have @Target(ElementType.RECORD_COMPONENT), we need to read
107+ * the annotation from the accessor method where Java propagates it.</p>
108+ *
101109 * @param targetClass The class of the target record.
102110 * @param <T> The type of the target record.
103111 * @return The names of the RecordComponents as a String-array.
104112 */
105113 private <T > String [] getNamesOfRecordComponents (final Class <T > targetClass ) {
106114 return Arrays .stream (targetClass .getRecordComponents ())
107115 .map (recordComponent -> {
108- if (recordComponent .isAnnotationPresent (io .beanmapper .annotations .BeanProperty .class )) {
109- return recordComponent .getAnnotation (io .beanmapper .annotations .BeanProperty .class ).value ();
116+ // Read @BeanProperty from accessor method where Java propagates it
117+ // (RecordComponent.isAnnotationPresent only works for @Target(RECORD_COMPONENT))
118+ io .beanmapper .annotations .BeanProperty beanProperty =
119+ recordComponent .getAccessor ().getAnnotation (io .beanmapper .annotations .BeanProperty .class );
120+ if (beanProperty != null && !beanProperty .value ().isEmpty ()) {
121+ return beanProperty .value ();
110122 }
111123 return recordComponent .getName ();
112124 })
113125 .toArray (String []::new );
114126 }
115127
116128 /**
117- * Gets the names of constructor parameters.
118- *
119- * <p>Prefers to use the RecordConstruct-annotation, if it is present. Otherwise, it will use the RecordComponents of the record, to determine the names and
120- * order of the parameters.</p>
129+ * Gets the parameter names from the constructor.
121130 *
122- * @param targetClass The target record.
123- * @param constructor The target constructor.
131+ * @param constructor The constructor to get parameter names from.
124132 * @param <T> The type of the target class.
125133 * @return The String-array containing the names of the constructor-parameters.
126134 */
127- private <T > String [] getNamesOfConstructorParameters ( final Class < T > targetClass , final Constructor <T > constructor ) {
135+ private <T > String [] getParameterNames ( final Constructor <T > constructor ) {
128136 if (constructor .isAnnotationPresent (BeanRecordConstruct .class )) {
129137 return constructor .getAnnotation (BeanRecordConstruct .class ).value ();
130138 }
131139
132- // We can only use this in cases where the compiler added the parameter-name to the classfile. If the name is
133- // present, we use the parameter-names, otherwise we use the names as set in the record-components.
134140 var parameters = constructor .getParameters ();
135- if (constructor . getParameters () [0 ].isNamePresent ()) {
141+ if (parameters . length > 0 && parameters [0 ].isNamePresent ()) {
136142 return Arrays .stream (parameters )
137143 .map (Parameter ::getName )
138144 .toArray (String []::new );
139145 }
140- return getNamesOfRecordComponents (targetClass );
146+
147+ // Fallback: use record component names
148+ Class <?> declaringClass = constructor .getDeclaringClass ();
149+ if (declaringClass .isRecord ()) {
150+ return Arrays .stream (declaringClass .getRecordComponents ())
151+ .map (RecordComponent ::getName )
152+ .toArray (String []::new );
153+ }
154+
155+ return Arrays .stream (parameters )
156+ .map (Parameter ::getName )
157+ .toArray (String []::new );
141158 }
142159
143160 private <S > List <Object > getValuesOfFields (final S source , final Map <String , PropertyAccessor > accessors ,
@@ -147,6 +164,60 @@ private <S> List<Object> getValuesOfFields(final S source, final Map<String, Pro
147164 .toList ();
148165 }
149166
167+ private <S > List <Object > getValuesOfFieldsWithNestedPathSupport (S source ,
168+ Map <String , PropertyAccessor > sourcePropertyAccessors ,
169+ String [] parameterNames ,
170+ String [] beanPropertyPaths ) {
171+ List <Object > values = new ArrayList <>();
172+ for (int i = 0 ; i < parameterNames .length ; i ++) {
173+ String parameterName = parameterNames [i ];
174+ String beanPropertyPath = beanPropertyPaths [i ];
175+ values .add (resolveSourceValue (source , parameterName , beanPropertyPath , sourcePropertyAccessors ));
176+ }
177+ return values ;
178+ }
179+
180+ private <S > Object resolveSourceValue (S source , String parameterName , String beanPropertyPath ,
181+ Map <String , PropertyAccessor > sourcePropertyAccessors ) {
182+ // If @BeanProperty specifies a nested path (contains a dot), use path resolution
183+ if (beanPropertyPath .contains ("." )) {
184+ return resolveNestedPath (source , beanPropertyPath );
185+ }
186+
187+ // Try parameter name first (supports @BeanAlias on source side)
188+ PropertyAccessor accessor = sourcePropertyAccessors .get (parameterName );
189+ if (accessor != null ) {
190+ return getValueFromField (source , accessor );
191+ }
192+
193+ // If @BeanProperty specifies a different name, try that in sourcePropertyAccessors
194+ if (!beanPropertyPath .equals (parameterName )) {
195+ accessor = sourcePropertyAccessors .get (beanPropertyPath );
196+ if (accessor != null ) {
197+ return getValueFromField (source , accessor );
198+ }
199+ // As last resort, try direct property access via BeanPropertyCreator
200+ return resolveNestedPath (source , beanPropertyPath );
201+ }
202+
203+ return null ;
204+ }
205+
206+ private <S > Object resolveNestedPath (S source , String fieldName ) {
207+ try {
208+ BeanProperty beanProperty = new BeanPropertyCreator (
209+ BeanPropertyMatchupDirection .SOURCE_TO_TARGET ,
210+ source .getClass (),
211+ fieldName
212+ ).determineNodesForPath ();
213+
214+ return beanProperty .getObject (source );
215+ } catch (BeanNoSuchPropertyException e ) {
216+ // Path doesn't exist in source - return null
217+ return null ;
218+ }
219+ }
220+
150221 private Object [] getConstructorArgumentsMappedToCorrectTargetType (final Parameter [] parameters , final List <Object > values ) {
151222 Object [] arguments = new Object [parameters .length ];
152223 for (var i = 0 ; i < parameters .length ; ++i ) {
0 commit comments