2323import jakarta .persistence .TupleElement ;
2424import jakarta .persistence .TypedQuery ;
2525
26+ import java .lang .reflect .Constructor ;
2627import java .util .Arrays ;
2728import java .util .Collection ;
2829import java .util .HashMap ;
5455import org .springframework .jdbc .support .JdbcUtils ;
5556import org .springframework .lang .Nullable ;
5657import org .springframework .util .Assert ;
58+ import org .springframework .util .ClassUtils ;
5759import org .springframework .util .ReflectionUtils ;
5860
5961/**
@@ -345,7 +347,7 @@ public TupleConverter(ReturnedType type, boolean nativeQuery) {
345347 && !type .getInputProperties ().isEmpty ();
346348
347349 if (this .dtoProjection ) {
348- this .preferredConstructor = PreferredConstructorDiscoverer .discover (String . class );
350+ this .preferredConstructor = PreferredConstructorDiscoverer .discover (type . getReturnedType () );
349351 } else {
350352 this .preferredConstructor = null ;
351353 }
@@ -373,18 +375,35 @@ public Object convert(Object source) {
373375
374376 Object [] ctorArgs = new Object [type .getInputProperties ().size ()];
375377
378+ boolean containsNullValue = false ;
376379 for (int i = 0 ; i < type .getInputProperties ().size (); i ++) {
377- ctorArgs [i ] = tuple .get (i );
380+ Object value = tuple .get (i );
381+ ctorArgs [i ] = value ;
382+ if (!containsNullValue && value == null ) {
383+ containsNullValue = true ;
384+ }
378385 }
379386
380387 try {
381388
382- if (preferredConstructor .getParameterCount () == ctorArgs .length ) {
389+ if (preferredConstructor != null && preferredConstructor .getParameterCount () == ctorArgs .length ) {
383390 return BeanUtils .instantiateClass (preferredConstructor .getConstructor (), ctorArgs );
384391 }
385392
386- return BeanUtils .instantiateClass (type .getReturnedType ()
387- .getConstructor (Arrays .stream (ctorArgs ).map (Object ::getClass ).toArray (Class <?>[]::new )), ctorArgs );
393+ Constructor <?> ctor = null ;
394+
395+ if (!containsNullValue ) { // let's seem if we have an argument type match
396+ ctor = type .getReturnedType ()
397+ .getConstructor (Arrays .stream (ctorArgs ).map (Object ::getClass ).toArray (Class <?>[]::new ));
398+ }
399+
400+ if (ctor == null ) { // let's see if there's more general constructor we could use that accepts our args
401+ ctor = findFirstMatchingConstructor (ctorArgs );
402+ }
403+
404+ if (ctor != null ) {
405+ return BeanUtils .instantiateClass (ctor , ctorArgs );
406+ }
388407 } catch (ReflectiveOperationException e ) {
389408 ReflectionUtils .handleReflectionException (e );
390409 }
@@ -393,6 +412,30 @@ public Object convert(Object source) {
393412 return new TupleBackedMap (tupleWrapper .apply (tuple ));
394413 }
395414
415+ @ Nullable
416+ private Constructor <?> findFirstMatchingConstructor (Object [] ctorArgs ) {
417+
418+ for (Constructor <?> ctor : type .getReturnedType ().getDeclaredConstructors ()) {
419+
420+ if (ctor .getParameterCount () == ctorArgs .length ) {
421+ boolean itsAMatch = true ;
422+ for (int i = 0 ; i < ctor .getParameterCount (); i ++) {
423+ if (ctorArgs [i ] == null ) {
424+ continue ;
425+ }
426+ if (!ClassUtils .isAssignable (ctor .getParameterTypes ()[i ], ctorArgs [i ].getClass ())) {
427+ itsAMatch = false ;
428+ break ;
429+ }
430+ }
431+ if (itsAMatch ) {
432+ return ctor ;
433+ }
434+ }
435+ }
436+ return null ;
437+ }
438+
396439 /**
397440 * A {@link Map} implementation which delegates all calls to a {@link Tuple}. Depending on the provided
398441 * {@link Tuple} implementation it might return the same value for various keys of which only one will appear in the
0 commit comments