Skip to content

Fix OptionalToObjectConverter applicability check#36913

Open
junhyeong9812 wants to merge 1 commit into
spring-projects:mainfrom
junhyeong9812:fix/optional-to-object-converter-matches
Open

Fix OptionalToObjectConverter applicability check#36913
junhyeong9812 wants to merge 1 commit into
spring-projects:mainfrom
junhyeong9812:fix/optional-to-object-converter-matches

Conversation

@junhyeong9812

Copy link
Copy Markdown

Overview

OptionalToObjectConverter (registered by DefaultConversionService for Optional -> Object)
unwraps an Optional and delegates the contained value to the ConversionService.

Problem

matches() derived the contained type with TypeDescriptor.getElementTypeDescriptor():

return ConversionUtils.canConvertElements(sourceType.getElementTypeDescriptor(), targetType, this.conversionService);

TypeDescriptor.getElementTypeDescriptor() only resolves element types for arrays, streams and
collections, so for an Optional it returns null. ConversionUtils.canConvertElements() then
treats a null source element type as "maybe" and returns true unconditionally. As a result
matches() always matched, and ConversionService.canConvert(Optional<X>, target) returned
true even when X cannot be converted to target — violating the canConvert contract (a
true result implies convert is capable of converting), since the subsequent conversion fails.

For example, with the out-of-the-box DefaultConversionService:

TypeDescriptor optionalInteger = new TypeDescriptor(
        ResolvableType.forClassWithGenerics(Optional.class, Integer.class), null, null);
cs.canConvert(optionalInteger, TypeDescriptor.valueOf(LocalDate.class)); // true (incorrect)
cs.convert(Optional.of(42), optionalInteger, TypeDescriptor.valueOf(LocalDate.class)); // fails

Fix

Resolve the Optional's element type from its generic and check it against the target, mirroring
the sibling ObjectToOptionalConverter. A raw Optional (or otherwise unresolved element type)
stays permissive, since the element type is unknown. A regression test is added to
DefaultConversionServiceTests.

Note on impact

This is a contract-correctness fix rather than a fix for data corruption: the conversion itself
already fails loudly (the subsequent convert throws), so nothing is silently mis-converted.
The value is that canConvert/matches is a predicate callers rely on to decide whether to
attempt a conversion or take a different path. Returning an incorrect true defeats that — it
turns what should be a clean negative answer into a deferred exception. Restoring an accurate
matches() lets callers get the correct answer up front. Impact is bounded, since it only
affects Optional used as a conversion source whose element type is not convertible to the
target.

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<X>, 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 <pickjog@gmail.com>
@spring-projects-issues spring-projects-issues added the status: waiting-for-triage An issue we've not yet triaged or decided on label Jun 13, 2026
@sbrannen sbrannen self-assigned this Jun 13, 2026
@sbrannen sbrannen added the in: core Issues in core modules (aop, beans, core, context, expression) label Jun 13, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

in: core Issues in core modules (aop, beans, core, context, expression) status: waiting-for-triage An issue we've not yet triaged or decided on

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants