From 563515d4fbc8b2abe796bb2ee513144081282009 Mon Sep 17 00:00:00 2001 From: junhyeong9812 Date: Sat, 13 Jun 2026 18:50:46 +0900 Subject: [PATCH] Fix OptionalToObjectConverter applicability check OptionalToObjectConverter.matches() used TypeDescriptor.getElementTypeDescriptor(), which returns null for an Optional (element types are only resolved for arrays, streams and collections). ConversionUtils.canConvertElements() then treats a null source element type as "maybe" and returns true unconditionally, so ConversionService.canConvert(Optional, target) reported true even when X is not convertible to the target -- a violation of the canConvert contract, since the subsequent conversion fails. Resolve the Optional's element type from its generic and check it against the target, mirroring ObjectToOptionalConverter. A raw or otherwise unresolved element type remains permissive. Signed-off-by: junhyeong9812 --- .../support/OptionalToObjectConverter.java | 10 +++++++++- .../DefaultConversionServiceTests.java | 20 +++++++++++++++++++ 2 files changed, 29 insertions(+), 1 deletion(-) diff --git a/spring-core/src/main/java/org/springframework/core/convert/support/OptionalToObjectConverter.java b/spring-core/src/main/java/org/springframework/core/convert/support/OptionalToObjectConverter.java index d7b31d09c501..8372ae3c8f3d 100644 --- a/spring-core/src/main/java/org/springframework/core/convert/support/OptionalToObjectConverter.java +++ b/spring-core/src/main/java/org/springframework/core/convert/support/OptionalToObjectConverter.java @@ -21,6 +21,7 @@ import org.jspecify.annotations.Nullable; +import org.springframework.core.ResolvableType; import org.springframework.core.convert.ConversionService; import org.springframework.core.convert.TypeDescriptor; import org.springframework.core.convert.converter.ConditionalGenericConverter; @@ -51,7 +52,14 @@ public Set getConvertibleTypes() { @Override public boolean matches(TypeDescriptor sourceType, TypeDescriptor targetType) { - return ConversionUtils.canConvertElements(sourceType.getElementTypeDescriptor(), targetType, this.conversionService); + ResolvableType elementType = sourceType.getResolvableType().getGeneric(); + if (elementType.resolve() == null) { + // Unknown Optional element type (raw Optional, wildcard, or unresolved + // type variable): remain permissive. + return true; + } + TypeDescriptor sourceElementType = new TypeDescriptor(elementType, null, null); + return ConversionUtils.canConvertElements(sourceElementType, targetType, this.conversionService); } @Override diff --git a/spring-core/src/test/java/org/springframework/core/convert/converter/DefaultConversionServiceTests.java b/spring-core/src/test/java/org/springframework/core/convert/converter/DefaultConversionServiceTests.java index 03205ad34ec5..b374d07f1e65 100644 --- a/spring-core/src/test/java/org/springframework/core/convert/converter/DefaultConversionServiceTests.java +++ b/spring-core/src/test/java/org/springframework/core/convert/converter/DefaultConversionServiceTests.java @@ -25,6 +25,7 @@ import java.sql.Time; import java.sql.Timestamp; import java.time.Instant; +import java.time.LocalDate; import java.time.ZoneId; import java.util.AbstractList; import java.util.ArrayList; @@ -962,6 +963,25 @@ class OptionalConversionTests { private static final TypeDescriptor rawOptionalType = TypeDescriptor.valueOf(Optional.class); + @Test + void canConvertOptionalToObjectReflectsContainedElementType() { + TypeDescriptor integerOptionalType = + new TypeDescriptor(ResolvableType.forClassWithGenerics(Optional.class, Integer.class), null, null); + TypeDescriptor localDateType = TypeDescriptor.valueOf(LocalDate.class); + + // Integer -> String is convertible + assertThat(conversionService.canConvert(integerOptionalType, TypeDescriptor.valueOf(String.class))).isTrue(); + + // Integer -> LocalDate is not convertible, so canConvert must not over-report... + assertThat(conversionService.canConvert(integerOptionalType, localDateType)).isFalse(); + // ...and must stay consistent with convert(): no converter is selected + assertThatExceptionOfType(ConverterNotFoundException.class) + .isThrownBy(() -> conversionService.convert(Optional.of(42), integerOptionalType, localDateType)); + + // An Optional with an unknown element type remains permissive + assertThat(conversionService.canConvert(rawOptionalType, localDateType)).isTrue(); + } + @Test @SuppressWarnings("unchecked") void convertObjectToOptional() {