Hello !

I was expecting my Spring web controller to be able to convert a string parameter to an enum by using the jackson @JsonValue or @JsonCreator behavior.

Then i saw that org.springframework.core.convert.support.StringToEnumConverterFactory.StringToEnum just use Enum::valueOf.

What do you think ? Shouldn't default behavior be aware of those jackson annotations for a better conversion ?

It could either be another converter used by default (like StringToJsonEnumConverterFactory) or an update of the actual.

You can check at this project that reproduce the issue, with the associated failing tests.

Comment From: thomascaillier

What i actually do is writing my own ConverterFactory :

@SuppressWarnings({"rawtypes", "unchecked"})
public final class StringToEnumConverterFactory implements ConverterFactory<String, Enum> {

    public <T extends Enum> @NonNull Converter<String, T> getConverter(@NonNull Class<T> targetType) {
        return new StringToEnum(getEnumType(targetType));
    }

    private record StringToEnum<T extends Enum>(Class<T> enumType) implements Converter<String, T> {
        @Nullable
        public T convert(String source) {
            if (source.isEmpty()) {
                return null;
            }
            var jsonValueField = Stream.of(enumType.getDeclaredFields())
                    .filter(field ->
                            field.getDeclaredAnnotationsByType(JsonValue.class).length > 0
                            && String.class.isAssignableFrom(field.getType()))
                    .findFirst();

            if (jsonValueField.isPresent()) {
                for (T constant : enumType.getEnumConstants()) {
                    try {
                        jsonValueField.get().setAccessible(true);
                        var jsonValue = jsonValueField.get().get(constant);
                        if (source.trim().equals((String) jsonValue)) {
                            return constant;
                        }
                    } catch (IllegalAccessException e) {
                        throw new RuntimeException(e);
                    }
                }
            }
            return (T) Enum.valueOf(this.enumType, source.trim());
        }
    }

    public static Class<?> getEnumType(@NonNull 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;
    }

}

and adding it to the FormatterRegistry (example with WebFlux):

@Configuration
public class ConversionConfiguration implements WebFluxConfigurer {

    @Override
    public void addFormatters(FormatterRegistry registry) {
        WebFluxConfigurer.super.addFormatters(registry);
        registry.addConverterFactory(new StringToEnumConverterFactory());
    }

}

Comment From: bclozel

Thanks for the proposal, but I don't think it's wise to support custom Jackson annotations in a place that has no link to JSON (de)serialization. The out-of-the-box String to Enum converter works already with valueOf(String) and has a well-defined behavior.

Here, it seems that you would like to reject uppercase variants and only accept lowercase values. I think it is easier to consider this as a custom converter and register it for your application:

import java.util.Locale;

import org.springframework.context.annotation.Configuration;
import org.springframework.core.convert.converter.Converter;
import org.springframework.format.FormatterRegistry;
import org.springframework.web.reactive.config.WebFluxConfigurer;

@Configuration
public class WebConfiguration implements WebFluxConfigurer {

    @Override
    public void addFormatters(FormatterRegistry registry) {
        registry.addConverter(new StringToUserTypeConverter());
    }

    static class StringToUserTypeConverter implements Converter<String, UserType> {

        @Override
        public UserType convert(String source) {
            // check case and throw illegalargumentexception?
            return UserType.valueOf(source.toUpperCase(Locale.ROOT));
        }
    }
}

Thanks for sharing a minimal and focused sample! Cheers