Question is easy, but I haven't found a solution for this:
I got this:
@RequestMapping("/example")
public class ExampleController {
@GetMapping("get")
public List<WhateverObject> getWhateverObjects(@RequestParam String objectName) {
/* Code */
}
}
We are using SpringBoot, and I'm looking to validate "objectName" against a defined list of values (This list is in an enum type, but that part is prone to change, so I wont mind if I need to write the values down by hand). All I've seen regarding validation of @RequestParam
objects covers just basic stuff (@Min(value)
, @NotNull
and all that.
I know about CustomValidators for beans, but it does not applies to my current problematic (And I can't change the type of parameter). Does Spring has something specific for this custom validation or do I need to make the validation "directly" in the /* Code */
section?
Validating a PathVariable Just as with @RequestParam, we can use any annotation from the javax. validation. constraints package to validate a @PathVariable. The default message can be easily overwritten by setting the message parameter in the @Size annotation.
In Spring MVC Validation, we can validate the user's input within a number range. The following annotations are used to achieve number validation: @Min annotation - It is required to pass an integer value with @Min annotation. The user input must be equal to or greater than this value.
You can create your own ConstraintValidator
, however you don't say if you need to compare your value against the values of an Enum
or with an internal property inside it. I will include an example of both cases in the next sections.
As greenPadawan mentioned, you can change the type of parameter by your Enum
, if you can/only need it, that is the best option.
The following example explains you how to customize that use case if you want to keep the String
(even updating it to include more/other checks if you want). The first step is create the annotation you will use to check the constraint:
/**
* The annotated element must be included in value of the given accepted {@link Class} of {@link Enum}.
*/
@Documented
@Retention(RUNTIME)
@Target({FIELD, ANNOTATION_TYPE, PARAMETER})
@Constraint(validatedBy = EnumHasValueValidator.class)
public @interface EnumHasValue {
String message() default "must be one of the values included in {values}";
Class<?>[] groups() default {};
Class<? extends Payload>[] payload() default {};
/**
* @return {@link Class} of {@link Enum} used to check the value
*/
Class<? extends Enum> enumClass();
/**
* @return {@code true} if {@code null} is accepted as a valid value, {@code false} otherwise.
*/
boolean isNullAccepted() default false;
}
The second is create your validator itself:
/**
* Validates if the given {@link String} matches with one of the values belonging to the
* provided {@link Class} of {@link Enum}
*/
public class EnumHasValueValidator implements ConstraintValidator<EnumHasValue, String> {
private static final String ERROR_MESSAGE_PARAMETER = "values";
List<String> enumValidValues;
String constraintTemplate;
private boolean isNullAccepted;
@Override
public void initialize(final EnumHasValue hasValue) {
enumValidValues = Arrays.stream(hasValue.enumClass().getEnumConstants())
.map(Enum::name)
.collect(Collectors.toList());
constraintTemplate = hasValue.message();
isNullAccepted = hasValue.isNullAccepted();
}
@Override
public boolean isValid(String value, ConstraintValidatorContext context) {
boolean isValid = null == value ? isNullAccepted
: enumValidValues.contains(value);
if (!isValid) {
HibernateConstraintValidatorContext hibernateContext = context.unwrap(HibernateConstraintValidatorContext.class);
hibernateContext.disableDefaultConstraintViolation();
hibernateContext.addMessageParameter(ERROR_MESSAGE_PARAMETER, enumValidValues)
.buildConstraintViolationWithTemplate(constraintTemplate)
.addConstraintViolation();
}
return isValid;
}
}
Now you can use it in the following example:
public enum IngredientEnum {
CHEESE,
HAM,
ONION,
PINEAPPLE,
BACON,
MOZZARELLA
}
And the controller:
@AllArgsConstructor
@RestController
@RequestMapping("/test")
@Validated
public class TestController {
@GetMapping("/testAgainstEnum")
public List<WhateverObject> testAgainstEnum(@RequestParam @EnumHasValue(enumClass=IngredientEnum.class) String objectName) {
...
}
}
You can see an example in the following picture:
(As you can see, in this case, lower/upper case are taking into account, you can change it in the validator if you want)
In this case, the first step is define a way to extract such internal property:
/**
* Used to get the value of an internal property in an {@link Enum}.
*/
public interface IEnumInternalPropertyValue<T> {
/**
* Get the value of an internal property included in the {@link Enum}.
*/
T getInternalPropertyValue();
}
public enum PizzaEnum implements IEnumInternalPropertyValue<String> {
MARGUERITA("Margherita"),
CARBONARA("Carbonara");
private String internalValue;
PizzaEnum(String internalValue) {
this.internalValue = internalValue;
}
@Override
public String getInternalPropertyValue() {
return this.internalValue;
}
}
The required annotation and related validator are quite similar to the previous ones:
/**
* The annotated element must be included in an internal {@link String} property of the given accepted
* {@link Class} of {@link Enum}.
*/
@Documented
@Retention(RUNTIME)
@Target({FIELD, ANNOTATION_TYPE, PARAMETER})
@Constraint(validatedBy = EnumHasInternalStringValueValidator.class)
public @interface EnumHasInternalStringValue {
String message() default "must be one of the values included in {values}";
Class<?>[] groups() default {};
Class<? extends Payload>[] payload() default {};
/**
* @return {@link Class} of {@link Enum} used to check the value
*/
Class<? extends Enum<? extends IEnumInternalPropertyValue<String>>> enumClass();
/**
* @return {@code true} if {@code null} is accepted as a valid value, {@code false} otherwise.
*/
boolean isNullAccepted() default false;
}
Validator:
/**
* Validates if the given {@link String} matches with one of the internal {@link String} property belonging to the
* provided {@link Class} of {@link Enum}
*/
public class EnumHasInternalStringValueValidator implements ConstraintValidator<EnumHasInternalStringValue, String> {
private static final String ERROR_MESSAGE_PARAMETER = "values";
List<String> enumValidValues;
String constraintTemplate;
private boolean isNullAccepted;
@Override
public void initialize(final EnumHasInternalStringValue hasInternalStringValue) {
enumValidValues = Arrays.stream(hasInternalStringValue.enumClass().getEnumConstants())
.map(e -> ((IEnumInternalPropertyValue<String>)e).getInternalPropertyValue())
.collect(Collectors.toList());
constraintTemplate = hasInternalStringValue.message();
isNullAccepted = hasInternalStringValue.isNullAccepted();
}
@Override
public boolean isValid(String value, ConstraintValidatorContext context) {
boolean isValid = null == value ? isNullAccepted
: enumValidValues.contains(value);
if (!isValid) {
HibernateConstraintValidatorContext hibernateContext = context.unwrap(HibernateConstraintValidatorContext.class);
hibernateContext.disableDefaultConstraintViolation();
hibernateContext.addMessageParameter(ERROR_MESSAGE_PARAMETER, enumValidValues)
.buildConstraintViolationWithTemplate(constraintTemplate)
.addConstraintViolation();
}
return isValid;
}
}
And the controller:
@AllArgsConstructor
@RestController
@RequestMapping("/test")
@Validated
public class TestController {
@GetMapping("/testStringInsideEnum")
public List<WhateverObject> testStringInsideEnum(@RequestParam @EnumHasInternalStringValue(enumClass=PizzaEnum.class) String objectName) {
...
}
}
You can see an example in the following picture:
The source code of the last annotation and validator can be found here
You can use your enum as the type of your parameter instead of String
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With