61

If I have a RequestMapping in a Spring controller like so...

@RequestMapping(method = RequestMethod.GET, value = "{product}")
public ModelAndView getPage(@PathVariable Product product)

And Product is an enum. eg. Product.Home

When I request the page, mysite.com/home

I get

Unable to convert value "home" from type 'java.lang.String' to type 'domain.model.product.Product'; nested exception is java.lang.IllegalArgumentException: No enum const class domain.model.product.Product.home

Is there a way to have the enum type converter to understand that lower case home is actually Home?

I'd like to keep the url case insensitive and my Java enums with standard capital letters.

Thanks

Solution

public class ProductEnumConverter extends PropertyEditorSupport
{
    @Override public void setAsText(final String text) throws IllegalArgumentException
    {
        setValue(Product.valueOf(WordUtils.capitalizeFully(text.trim())));
    }
}

registering it

<bean class="org.springframework.beans.factory.config.CustomEditorConfigurer">
        <property name="customEditors">
            <map>
                <entry key="domain.model.product.Product" value="domain.infrastructure.ProductEnumConverter"/>
            </map>
        </property>
    </bean>

Add to controllers that need special conversion

@InitBinder
public void initBinder(WebDataBinder binder)
{
    binder.registerCustomEditor(Product.class, new ProductEnumConverter());
} 
dom farr
  • 4,041
  • 4
  • 32
  • 38
  • 1
    There are classes `RelaxedConversionService` and `StringToEnumIgnoringCaseConverterFactory` in Spring Boot, but they are not public. – OrangeDog Aug 25 '16 at 09:52
  • Here a solution to use `StringToEnumIgnoringCaseConverterFactory` https://stackoverflow.com/questions/55169848/spring-boot-convert-enum-ignoring-case – Mohicane Jul 31 '19 at 15:24
  • I haven't tested this but looks like this will work: https://vianneyfaivre.com/tech/spring-boot-enum-as-parameter-ignore-case – Colm Bhandal Jul 07 '21 at 09:46

5 Answers5

28

Broadly speaking, you want to create a new PropertyEditor that does the normalisation for you, and then you register that in your Controller like so:

@InitBinder
 public void initBinder(WebDataBinder binder) {

  binder.registerCustomEditor(Product.class,
    new CaseInsensitivePropertyEditor());
 }
GaryF
  • 23,950
  • 10
  • 60
  • 73
  • 3
    Don't want to have to add this to every controller that uses this enum. Is there a global solution? – dom farr Jan 06 '11 at 17:03
  • @dom farr: There is, but it looks like skaffman has beaten me to it. See his answer. – GaryF Jan 06 '11 at 22:16
  • 6
    For Spring 3.2 users; you can now use `@ControllerAdvice` to register global init binders (among other things). See [the reference guide](http://static.springsource.org/spring/docs/3.2.x/spring-framework-reference/html/new-in-3.2.html#new-in-3.2-webmvc-controller-advice) for more information. – tmbrggmn Aug 22 '13 at 08:33
  • 1
    @GaryF And so we don't need to create a CustomEditorConfigurer bean ? The init binder in a controller advice is enough ? – Stephane Oct 22 '14 at 12:55
  • 1
    It seems it doesn't work for `RequestBody`. Any way for that? – Vikas Prasad Sep 20 '17 at 10:07
  • @VikasPrasad I haven't tested this but some of the following solutions might work: https://www.baeldung.com/jackson-serialize-enums – Colm Bhandal Jul 07 '21 at 09:53
18

I think you will have to implement a Custom PropertyEditor.

Something like this:

public class ProductEditor extends PropertyEditorSupport{

    @Override
    public void setAsText(final String text){
        setValue(Product.valueOf(text.toUpperCase()));
    }

}

See GaryF's answer on how to bind it

Here's a more tolerant version in case you use lower case in your enum constants (which you probably shouldn't, but still):

@Override
public void setAsText(final String text){
    Product product = null;
    for(final Product candidate : Product.values()){
        if(candidate.name().equalsIgnoreCase(text)){
            product = candidate;
            break;
        }
    }
    setValue(product);
}
Community
  • 1
  • 1
Sean Patrick Floyd
  • 292,901
  • 67
  • 465
  • 588
  • Seems like the GenericConversionService.convert gets in the way of this type of solution. – dom farr Jan 06 '11 at 17:04
  • @dom farr: yes, if you use `ConversionService` you will also need to [implement a custom Converter](http://static.springsource.org/spring/docs/3.0.x/spring-framework-reference/html/validation.html#core-convert). – Sean Patrick Floyd Jan 06 '11 at 17:11
  • 1
    Yes, that was what I tried first. It just didn't work, so I came here and asked the question. Perhaps I didn't register it correctly? I used the ConversionServiceFactoryBean, but as I said, no go. – dom farr Jan 06 '11 at 17:29
  • Sean, I'm on spring boot and the initbinder method seems to be called after the string is mapped to the enum on my object that's my controller parameter. Am I missing something? – Alexander Suraphel May 09 '16 at 13:52
  • @AlexanderSuraphel see comments above. These days, ConversionService is probably enabled by default. Implement a custom converter – Sean Patrick Floyd May 09 '16 at 13:56
17

It's also possible to create a generic converter that will work with all Enums like this:

public class CaseInsensitiveConverter<T extends Enum<T>> extends PropertyEditorSupport {

    private final Class<T> typeParameterClass;

    public CaseInsensitiveConverter(Class<T> typeParameterClass) {
        super();
        this.typeParameterClass = typeParameterClass;
    }

    @Override
    public void setAsText(final String text) throws IllegalArgumentException {
        String upper = text.toUpperCase(); // or something more robust
        T value = T.valueOf(typeParameterClass, upper);
        setValue(value);
    }
}

Usage:

@InitBinder
public void initBinder(WebDataBinder binder) {
    binder.registerCustomEditor(MyEnum.class, new CaseInsensitiveConverter<>(MyEnum.class));
}

Or globally as skaffman explains

  • Nice, I like the generic class! It may not be as efficient but I'd prefer to do an equalsIgnoreCase instead of your toUpperCase+valueOf. getEnumConstants should be available off of the typeParameterClass field. Going to combine this implementation with the method from @SeanPatrickFloyd – Ryan Jun 03 '15 at 18:13
  • On second thought iterating the enum constants on every request is too painful. I'm going to stick a HashMap inside CaseInsensitiveConverter to speed up the mappings. – Ryan Jun 03 '15 at 18:49
15

In Spring Boot 2 you can use ApplicationConversionService. It provides some useful converters, especially org.springframework.boot.convert.StringToEnumIgnoringCaseConverterFactory - responsible for converting a string value to an enum instance. This is the most generic (we don't need to create separate converter/formatter per enum) and simplest solution I've managed to find.

import org.springframework.boot.convert.ApplicationConversionService;
import org.springframework.context.annotation.Configuration;
import org.springframework.format.FormatterRegistry;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;

@Configuration
public class AppWebMvcConfigurer implements WebMvcConfigurer {
    @Override
    public void addFormatters(FormatterRegistry registry) {
        ApplicationConversionService.configure(registry);
    }
}

I know that questions is regarding Spring 3 but this is the first result in google when searching for a spring mvc enums case insensitive phrase.

tstec
  • 313
  • 5
  • 9
7

To add to @GaryF's answer, and to address your comment to it, you can declare global custom property editors by injecting them into a custom AnnotationMethodHandlerAdapter. Spring MVC normally registers one of these by default, but you can give it a specially-configured one if you choose, e.g.

<bean class="org.springframework.web.servlet.mvc.annotation.AnnotationMethodHandlerAdapter">
  <property name="webBindingInitializer">
    <bean class="org.springframework.web.bind.support.ConfigurableWebBindingInitializer">
      <property name="propertyEditorRegistrars">
        <list>
          <bean class="com.xyz.MyPropertyEditorRegistrar"/>
        </list>
      </property>
    </bean>
  </property>
</bean>

MyPropertyEditorRegistrar is an instance of PropertyEditorRegistrar, which in turns registers custom PropertyEditor objects with Spring.

Simply declaring this should be enough.

skaffman
  • 398,947
  • 96
  • 818
  • 769
  • @skaffman. Can't seem to get this to work if I remove the individual initBinder in all my controllers. I'll keep digging on this one though. Thanks – dom farr Jan 07 '11 at 09:29
  • @dom: The approach does work, I use it myself, but I may have some of the specifics wrong – skaffman Jan 07 '11 at 10:01
  • 1
    @skaffman. Perhaps this is my issue: https://jira.springsource.org/browse/SPR-7077 – dom farr Jan 07 '11 at 10:45
  • @dom: Yes, good catch, that will indeed be the problem. Do you specifically need ``? Unless you're using Jackson/JSON mapping, or `@Valid`, you can probably leave it out. – skaffman Jan 07 '11 at 10:47
  • @skaffman. Jackson/JSON mapping and @Valid are being used unfortunately, which means I will have to stick with @InitBinder in each controller for now. – dom farr Jan 07 '11 at 10:58
  • @dom: Actually, you can explicitly configure validation and Jackson support. `` is essentially just a macro that assembles a pre-configured `AnnotationMethodHandlerAdapter`, you can manually configure it instead. – skaffman Jan 07 '11 at 11:02