Spring's @RequestParam with Enum
Asked Answered
I

8

98

I have this enum :

public enum SortEnum {
    asc, desc;
}

That I want to use as a parameter of a rest request :

@RequestMapping(value = "/events", method = RequestMethod.GET, produces = MediaType.APPLICATION_JSON_VALUE)
public List<Event> getEvents(@RequestParam(name = "sort", required = false) SortEnum sort) {

It works fine when I send these requests

/events 
/events?sort=asc
/events?sort=desc

But when I send :

/events?sort=somethingElse

I get a 500 response and this message in the console :

2016-09-29 17:20:51.600 DEBUG 5104 --- [  XNIO-2 task-6] com.myApp.aop.logging.LoggingAspect   : Enter: com.myApp.web.rest.errors.ExceptionTranslator.processRuntimeException() with argument[s] = [org.springframework.web.method.annotation.MethodArgumentTypeMismatchException: Failed to convert value of type [java.lang.String] to required type [com.myApp.common.SortEnum]; nested exception is org.springframework.core.convert.ConversionFailedException: Failed to convert from type [java.lang.String] to type [@org.springframework.web.bind.annotation.RequestParam com.myApp.common.SortEnum] for value 'somethingElse'; nested exception is java.lang.IllegalArgumentException: No enum constant com.myApp.common.SortEnum.somethingElse]
2016-09-29 17:20:51.600 DEBUG 5104 --- [  XNIO-2 task-6] com.myApp.aop.logging.LoggingAspect   : Exit: com.myApp.web.rest.errors.ExceptionTranslator.processRuntimeException() with result = <500 Internal Server Error,com.myApp.web.rest.errors.ErrorVM@1e3343c9,{}>
2016-09-29 17:20:51.601  WARN 5104 --- [  XNIO-2 task-6] .m.m.a.ExceptionHandlerExceptionResolver : Resolved exception caused by Handler execution: org.springframework.web.method.annotation.MethodArgumentTypeMismatchException: Failed to convert value of type [java.lang.String] to required type [com.myApp.common.SortEnum]; nested exception is org.springframework.core.convert.ConversionFailedException: Failed to convert from type [java.lang.String] to type [@org.springframework.web.bind.annotation.RequestParam com.myApp.common.SortEnum] for value 'somethingElse'; nested exception is java.lang.IllegalArgumentException: No enum constant com.myApp.common.SortEnum.somethingElse

Is there a way to prevent spring from throwing these exceptions and set the enum to null ?

EDIT

The Strelok's accepted answer works. However, I decided to deal with handling the MethodArgumentTypeMismatchException.

@ControllerAdvice
public class ExceptionTranslator {

    @ExceptionHandler(MethodArgumentTypeMismatchException.class)
    @ResponseBody
    public ResponseEntity<Object> handleMethodArgumentTypeMismatchException(MethodArgumentTypeMismatchException e) {
        Class<?> type = e.getRequiredType();
        String message;
        if(type.isEnum()){
            message = "The parameter " + e.getName() + " must have a value among : " + StringUtils.join(type.getEnumConstants(), ", ");
        }
        else{
            message = "The parameter " + e.getName() + " must be of type " + type.getTypeName();
        }
        return buildResponse(HttpStatus.UNPROCESSABLE_ENTITY, message);
    }
Ikon answered 29/9, 2016 at 15:30 Comment(2)
I have checked the meaning of 422 and it says: "syntax of the request entity is correct", which I don't think is the case if the string does not match the enum.Practicable
the appropriate error code should be 400 ("Bad Request")Photocathode
C
66

You can create a custom converter that will return null instead of an exception when an invalid value is supplied.

Something like this:

@Configuration
public class MyConfig extends WebMvcConfigurationSupport {
   @Override
   public FormattingConversionService mvcConversionService() {
       FormattingConversionService f = super.mvcConversionService();
       f.addConverter(new MyCustomEnumConverter());
       return f;
   }
}

And a simple converter might look like this:

public class MyCustomEnumConverter implements Converter<String, SortEnum> {
    @Override
    public SortEnum convert(String source) {
       try {
          return SortEnum.valueOf(source);
       } catch(Exception e) {
          return null; // or SortEnum.asc
       }
    }
}
Cornu answered 29/9, 2016 at 15:44 Comment(7)
this is the correct answer if you want this behavior globally for all endpoints. If you want this behavior just for your one controller, then satish chennupati had the right solution.Clemons
After doing this, somehow, my oauth2 endpoint got messed up and the user could not authenticateBundelkhand
Is that com.fasterxml.jackson.databind.util.Converter?Mcglynn
@CharlesWood no, that's an org.springframework.core.convert.converter.ConverterSeigneur
Will this have a significant performance hit if the first parameter is a String? I have not checked under the hood, but was a little worried that it will try to find a converter from each and every path and query parameter for each and every controller.Nanoid
Be aware that extending WebMvcConfigurationSupport may have side effect, for example, used with spring-boot-starter-actuator result in duplicated bean conflict. A solution is to get the FormattingConversionService bean using @Autowired and add your converters to it.Super
could you plz help me #67211046Halfblood
S
84

If you are using Spring Boot, this is the reason that you should not use WebMvcConfigurationSupport.

The best practice, you should implement interface org.springframework.core.convert.converter.Converter, and with annotation @Component. Then Spring Boot will auto load all Converter's bean. Spring Boot code

@Component
public class GenderEnumConverter implements Converter<String, GenderEnum> {
    @Override
    public GenderEnum convert(String value) {
        return GenderEnum.of(Integer.valueOf(value));
    }
}

Demo Project

Stagehand answered 12/11, 2018 at 16:12 Comment(2)
This is the most concise answer! In later Spring Boot version (here 2.5.2) this is done differently: github.com/spring-projects/spring-boot/blob/v2.5.2/… github.com/spring-projects/spring-boot/blob/v2.5.2/…Mccarthyism
I needed this, but also had to make sure that the pathvariable name matched the actual method variable name as well.Maleeny
C
66

You can create a custom converter that will return null instead of an exception when an invalid value is supplied.

Something like this:

@Configuration
public class MyConfig extends WebMvcConfigurationSupport {
   @Override
   public FormattingConversionService mvcConversionService() {
       FormattingConversionService f = super.mvcConversionService();
       f.addConverter(new MyCustomEnumConverter());
       return f;
   }
}

And a simple converter might look like this:

public class MyCustomEnumConverter implements Converter<String, SortEnum> {
    @Override
    public SortEnum convert(String source) {
       try {
          return SortEnum.valueOf(source);
       } catch(Exception e) {
          return null; // or SortEnum.asc
       }
    }
}
Cornu answered 29/9, 2016 at 15:44 Comment(7)
this is the correct answer if you want this behavior globally for all endpoints. If you want this behavior just for your one controller, then satish chennupati had the right solution.Clemons
After doing this, somehow, my oauth2 endpoint got messed up and the user could not authenticateBundelkhand
Is that com.fasterxml.jackson.databind.util.Converter?Mcglynn
@CharlesWood no, that's an org.springframework.core.convert.converter.ConverterSeigneur
Will this have a significant performance hit if the first parameter is a String? I have not checked under the hood, but was a little worried that it will try to find a converter from each and every path and query parameter for each and every controller.Nanoid
Be aware that extending WebMvcConfigurationSupport may have side effect, for example, used with spring-boot-starter-actuator result in duplicated bean conflict. A solution is to get the FormattingConversionService bean using @Autowired and add your converters to it.Super
could you plz help me #67211046Halfblood
L
27

you need to do the following

@InitBinder
public void initBinder(WebDataBinder dataBinder) {
    dataBinder.registerCustomEditor(YourEnum.class, new YourEnumConverter());
}

refer the following : https://machiel.me/post/java-enums-as-request-parameters-in-spring-4/

Lynda answered 29/9, 2016 at 15:51 Comment(0)
P
10

If you have multiple enums then if you follow the other answers you'll end up creating one converter for each one.

Here is a solution that works for all enums.

Converter or PropertyEditorSupport are not appropriate in this case because they don't allow us to know the target class.

In this example I have used the Jackson ObjectMapper, but you could replace this part by a call to the static method via reflection or move the call to values() to the converter.

@Component
public class JacksonEnumConverter implements GenericConverter {

    private ObjectMapper mapper;

    private Set<ConvertiblePair> set;

    @Autowired
    public JacksonEnumConverter(ObjectMapper mapper) {
        set = new HashSet<>();
        set.add(new ConvertiblePair(String.class, Enum.class));
        this.mapper = mapper;
    }

    @Override
    public Set<ConvertiblePair> getConvertibleTypes() {
        return set;
    }

    @Override
    public Object convert(Object source, TypeDescriptor sourceType, TypeDescriptor targetType) {
        if (source == null) {
            return null;
        }
        try {
            return mapper.readValue("\"" + source + "\"", targetType.getType());
        } catch (IOException e) {
            throw new InvalidFieldException(targetType.getName(),source.toString());
        }
    }
}

and in this case, because I'm using Jackson, the enum class has to have a static method annotated with @JsonCreator so I can map using the value rather than the constant name:

public enum MyEnum {

    VAL_1("val-1"), VAL_2("val-2");

    private String value;

    MyEnum(String value) {
        this.value = value;
    }

    @JsonValue
    public String getValue() {
        return value;
    }

    @JsonCreator(mode = JsonCreator.Mode.DELEGATING)
    public static MyEnum fromValue(String value) {
        for (MyEnum e : values()) {
            if (e.value.equalsIgnoreCase(value)) {
                return e;
            }
        }
        throw new InvalidFieldException("my-enum", value);
    }
}

Instead of returning null it is better to throw an exception.

Practicable answered 14/4, 2020 at 15:31 Comment(4)
I was exactly looking for a solution that I wouldn't end up with a ton of converters. Thanks!Britain
one nice thing I added was a fallback: If I can't map it with JSON, I fallback searching for the enum value.Britain
Don't return null. Rather return a default enum eg. UNKNOWN or throw an exception.Boehm
Well remembered. I have updated the answer. In any case I think this comment would have more value if placed in the question because this is what it is asking for.Practicable
E
6

The answers provided so far are not complete. Here is an answer example step-by-step that worked for me:-

1st Define the enum in your endpoint signature(subscription type).
Example:

public ResponseEntity v1_getSubscriptions(@PathVariable String agencyCode,
                                          @RequestParam(value = "uwcompany", required = false) String uwCompany,
                                          @RequestParam(value = "subscriptiontype", required = false) SubscriptionType subscriptionType,
                                          @RequestParam(value = "alert", required = false) String alert,

2nd Define a custom property editor that will be used to translate from String to enum:

import java.beans.PropertyEditorSupport;

public class SubscriptionTypeEditor extends PropertyEditorSupport {

    public void setAsText(String text) {
        try {
            setValue(SubscriptionType.valueOf(text.toUpperCase()));
        } catch (Exception ex) {
            setValue(null);
        }
    }
}

3rd Register the property editor with the controller:

@InitBinder ("subscriptiontype")
public void initBinder(WebDataBinder dataBinder) {
    dataBinder.registerCustomEditor(SubscriptionType.class, new SubscriptionTypeEditor());
}

Translations from string to enum should happen perfectly now.

Extremism answered 4/5, 2018 at 21:28 Comment(1)
This works but why does it require such a complicated answer for something that SHOULD only require a single method with an @Override in it?Perr
A
1

If you are already implementing WebMvcConfigurer, instead of WebMvcConfigurationSupport, you can add new converter by implementing the method addFormatters

  @Override
  public void addFormatters(FormatterRegistry registry) {
    registry.addConverter(new MyCustomEnumConverter());
  }
Abrupt answered 4/11, 2019 at 12:36 Comment(0)
A
0

You can use @JsonValue annotation in the ENUM. Check this - https://www.baeldung.com/jackson-serialize-enums

Alroi answered 12/3, 2021 at 11:41 Comment(0)
C
-3

you can use String instead of SortEnum param

@RequestParam(name = "sort", required = false) String sort

and convert it using

SortEnum se;
try {
   se = SortEnum.valueOf(source);
} catch(IllegalArgumentException e) {
   se = null;
}

inside of getEvents(...) endpoint method losing elegance but gaining more control over conversion and possible error handling.

Claviform answered 26/3, 2019 at 13:39 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.