Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

SpringBoot: Custom validation for a @RequestParam parameter in a REST endpint

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?

like image 786
Neuromante Avatar asked Aug 19 '20 12:08

Neuromante


People also ask

How do I validate a parameter in Spring boot?

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.

How do I validate a number in Spring boot?

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.


2 Answers

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.


Compare against enum values

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:

EnumHasValue example

(As you can see, in this case, lower/upper case are taking into account, you can change it in the validator if you want)


Compare against internal enum property

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:

EnumHasValue example

The source code of the last annotation and validator can be found here

like image 135
doctore Avatar answered Sep 27 '22 23:09

doctore


You can use your enum as the type of your parameter instead of String

like image 34
greenPadawan Avatar answered Sep 27 '22 23:09

greenPadawan