Avoid exception in ConversionService.canConvert() for Enum targets

Prior to this commit, ConversionService#canConvert(Class, Class)
threw an IllegalArgumentException when invoked with Enum.class as the
target type (i.e., `canConvert(String.class, Enum.class)`), because
ConverterFactory#getConverter() in StringToEnumConverterFactory and
IntegerToEnumConverterFactory eagerly resolved the concrete enum type.

To address that, StringToEnumConverterFactory and
IntegerToEnumConverterFactory now implement ConditionalConverter so
that matches() can reject non-concrete-enum targets before
getConverter() is ever invoked.

Closes gh-34532

Signed-off-by: MoonFruit <dkmoonfruit@gmail.com>

Co-authored-by: Sam Brannen <104798+sbrannen@users.noreply.github.com>
This commit is contained in:
MoonFruit
2026-09-08 13:29:30 +02:00
committed by Sam Brannen
co-authored by Sam Brannen
parent 3c6b001349
commit e2fae069dc
4 changed files with 45 additions and 7 deletions
@@ -22,7 +22,6 @@ import org.springframework.core.convert.ConversionFailedException;
import org.springframework.core.convert.ConversionService;
import org.springframework.core.convert.TypeDescriptor;
import org.springframework.core.convert.converter.GenericConverter;
import org.springframework.util.Assert;
import org.springframework.util.ClassUtils;
/**
@@ -71,12 +70,17 @@ abstract class ConversionUtils {
return false;
}
public static Class<?> getEnumType(Class<?> targetType) {
/**
* Resolve the enum type for the supplied target type.
* @param targetType the target type for which to resolve the enum type
* @return the resolved enum type, or {@code null} if the supplied target type
* does not refer to an enum
*/
public static @Nullable Class<?> resolveEnumType(Class<?> targetType) {
Class<?> enumType = targetType;
while (enumType != null && !enumType.isEnum()) {
enumType = enumType.getSuperclass();
}
Assert.notNull(enumType, () -> "The target type " + targetType.getName() + " does not refer to an enum");
return enumType;
}
@@ -18,8 +18,11 @@ package org.springframework.core.convert.support;
import org.jspecify.annotations.Nullable;
import org.springframework.core.convert.TypeDescriptor;
import org.springframework.core.convert.converter.ConditionalConverter;
import org.springframework.core.convert.converter.Converter;
import org.springframework.core.convert.converter.ConverterFactory;
import org.springframework.util.Assert;
/**
* Converts from an Integer to a {@link java.lang.Enum} by calling {@link Class#getEnumConstants()}.
@@ -29,11 +32,18 @@ import org.springframework.core.convert.converter.ConverterFactory;
* @since 4.3
*/
@SuppressWarnings({"rawtypes", "unchecked"})
final class IntegerToEnumConverterFactory implements ConverterFactory<Integer, Enum> {
final class IntegerToEnumConverterFactory implements ConverterFactory<Integer, Enum>, ConditionalConverter {
@Override
public <T extends Enum> Converter<Integer, @Nullable T> getConverter(Class<T> targetType) {
return new IntegerToEnum(ConversionUtils.getEnumType(targetType));
Class<?> enumType = ConversionUtils.resolveEnumType(targetType);
Assert.notNull(enumType, () -> "The target type " + targetType.getName() + " does not refer to an enum");
return new IntegerToEnum(enumType);
}
@Override
public boolean matches(TypeDescriptor sourceType, TypeDescriptor targetType) {
return (ConversionUtils.resolveEnumType(targetType.getType()) != null);
}
@@ -18,8 +18,11 @@ package org.springframework.core.convert.support;
import org.jspecify.annotations.Nullable;
import org.springframework.core.convert.TypeDescriptor;
import org.springframework.core.convert.converter.ConditionalConverter;
import org.springframework.core.convert.converter.Converter;
import org.springframework.core.convert.converter.ConverterFactory;
import org.springframework.util.Assert;
/**
* Converts from a String to a {@link java.lang.Enum} by calling {@link Enum#valueOf(Class, String)}.
@@ -29,11 +32,18 @@ import org.springframework.core.convert.converter.ConverterFactory;
* @since 3.0
*/
@SuppressWarnings({"rawtypes", "unchecked"})
final class StringToEnumConverterFactory implements ConverterFactory<String, Enum> {
final class StringToEnumConverterFactory implements ConverterFactory<String, Enum>, ConditionalConverter {
@Override
public <T extends Enum> Converter<String, @Nullable T> getConverter(Class<T> targetType) {
return new StringToEnum(ConversionUtils.getEnumType(targetType));
Class<?> enumType = ConversionUtils.resolveEnumType(targetType);
Assert.notNull(enumType, () -> "The target type " + targetType.getName() + " does not refer to an enum");
return new StringToEnum(enumType);
}
@Override
public boolean matches(TypeDescriptor sourceType, TypeDescriptor targetType) {
return (ConversionUtils.resolveEnumType(targetType.getType()) != null);
}
@@ -503,6 +503,20 @@ class GenericConversionServiceTests {
assertThat(conversionService.convert("base1", MyEnum.class)).isEqualTo(MyEnum.A);
}
@Test // gh-34532
void canConvertToEnumDoesNotThrowForNonEnumTargetType() {
conversionService.addConverterFactory(new StringToEnumConverterFactory());
conversionService.addConverterFactory(new IntegerToEnumConverterFactory());
assertThat(conversionService.canConvert(String.class, Enum.class)).isFalse();
assertThat(conversionService.canConvert(Integer.class, Enum.class)).isFalse();
assertThat(conversionService.canConvert(String.class, MyEnum.class)).isTrue();
assertThat(conversionService.canConvert(Integer.class, MyEnum.class)).isTrue();
assertThat(conversionService.convert("A", MyEnum.class)).isEqualTo(MyEnum.A);
assertThat(conversionService.convert(0, MyEnum.class)).isEqualTo(MyEnum.A);
}
@Test
void convertNullAnnotatedStringToString() throws Exception {
String source = null;