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