How to validate Spring MVC @PathVariable values?
Asked Answered
S

8

32

For a simple RESTful JSON api implemented in Spring MVC, can I use Bean Validation (JSR-303) to validate the path variables passed into the handler method?

For example:

 @RequestMapping(value = "/number/{customerNumber}")
 @ResponseBody
 public ResponseObject searchByNumber(@PathVariable("customerNumber") String customerNumber) {
 ...
 }

Here, I need to validate the customerNumber variables's length using Bean validation. Is this possible with Spring MVC v3.x.x? If not, what's the best approach for this type of validations?

Thanks.

Sawfish answered 17/10, 2013 at 5:55 Comment(0)
A
46

Spring does not support @javax.validation.Valid on @PathVariable annotated parameters in handler methods. There was an Improvement request, but it is still unresolved.

Your best bet is to just do your custom validation in the handler method body or consider using org.springframework.validation.annotation.Validated as suggested in other answers.

Astrolabe answered 17/10, 2013 at 15:21 Comment(0)
C
26

You can use like this: use org.springframework.validation.annotation.Validated to valid RequestParam or PathVariable.

 *
 * Variant of JSR-303's {@link javax.validation.Valid}, supporting the
 * specification of validation groups. Designed for convenient use with
 * Spring's JSR-303 support but not JSR-303 specific.
 *

step.1 init ValidationConfig

@Configuration
public class ValidationConfig {
    @Bean
    public MethodValidationPostProcessor methodValidationPostProcessor() {
        MethodValidationPostProcessor processor = new MethodValidationPostProcessor();
        return processor;
    }
}

step.2 Add @Validated to your controller handler class, Like:

@RequestMapping(value = "poo/foo")
@Validated
public class FooController {
...
}

step.3 Add validators to your handler method:

   @RequestMapping(value = "{id}", method = RequestMethod.DELETE)
   public ResponseEntity<Foo> delete(
           @PathVariable("id") @Size(min = 1) @CustomerValidator int id) throws RestException {
        // do something
        return new ResponseEntity(HttpStatus.OK);
    }

final step. Add exception resolver to your context:

@Component
public class BindExceptionResolver implements HandlerExceptionResolver {

    @Override
    public ModelAndView resolveException(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) {
        if (ex.getClass().equals(BindException.class)) {
            BindException exception = (BindException) ex;

            List<FieldError> fieldErrors = exception.getFieldErrors();
            return new ModelAndView(new MappingJackson2JsonView(), buildErrorModel(request, response, fieldErrors));
        }
    }
}
Coverley answered 26/10, 2016 at 8:14 Comment(2)
The key here is to explicitly add MethodValidationPostProcessor in your WebMvc context even if it's already defined in your Root context.Jeepers
Step 1 is can be discarded if you are running a Spring Boot application.Pibroch
L
8

Instead of using @PathVariable, you can take advantage of Spring MVC ability to map path variables into a bean:

@RestController
@RequestMapping("/user")
public class UserController {

    @GetMapping("/{id}")
    public void get(@Valid GetDto dto) {
        // dto.getId() is the path variable
    }

}

And the bean contains the actual validation rules:

@Data
public class GetDto {
     @Min(1) @Max(99)
     private long id;
}

Make sure that your path variables ({id}) correspond to the bean fields (id);

Laverne answered 19/7, 2019 at 18:50 Comment(0)
P
7

The solution is simple:

@GetMapping(value = {"/", "/{hash:[a-fA-F0-9]{40}}"})
public String request(@PathVariable(value = "hash", required = false) String historyHash)
{
    // Accepted requests: either "/" or "/{40 character long hash}"
}

And yes, PathVariables are ment to be validated, like any user input.

Pressor answered 7/10, 2018 at 5:47 Comment(1)
This will return a 404 instead of a 400 which I think is what the OP wants.Applegate
E
2

@PathVariable is not meant to be validated in order to send back a readable message to the user. As principle a pathVariable should never be invalid. If a pathVariable is invalid the reason can be:

  1. a bug generated a bad url (an href in jsp for example). No @Valid is needed and no message is needed, just fix the code;
  2. "the user" is manipulating the url. Again, no @Valid is needed, no meaningful message to the user should be given.

In both cases just leave an exception bubble up until it is catched by the usual Spring ExceptionHandlers in order to generate a nice error page or a meaningful json response indicating the error. In order to get this result you can do some validation using custom editors.

Create a CustomerNumber class, possibly as immutable (implementing a CharSequence is not needed but allows you to use it basically as if it were a String)

public class CustomerNumber implements CharSequence {

    private String customerNumber;

    public CustomerNumber(String customerNumber) {
        this.customerNumber = customerNumber;
    }

    @Override
    public String toString() {
        return customerNumber == null ? null : customerNumber.toString();
    }

    @Override
    public int length() {
        return customerNumber.length();
    }

    @Override
    public char charAt(int index) {
        return customerNumber.charAt(index);
    }

    @Override
    public CharSequence subSequence(int start, int end) {
        return customerNumber.subSequence(start, end);
    }

    @Override
    public boolean equals(Object obj) {
        return customerNumber.equals(obj);
    }

    @Override
    public int hashCode() {
        return customerNumber.hashCode();
    }
}

Create an editor implementing your validation logic (in this case no whitespaces and fixed length, just as an example)

public class CustomerNumberEditor extends PropertyEditorSupport {

    @Override
    public void setAsText(String text) throws IllegalArgumentException {

        if (StringUtils.hasText(text) && !StringUtils.containsWhitespace(text) && text.length() == YOUR_LENGTH) {
            setValue(new CustomerNumber(text));
        } else {
            throw new IllegalArgumentException();
            // you could also subclass and throw IllegalArgumentException
            // in order to manage a more detailed error message
        }
    }

    @Override
    public String getAsText() {
        return ((CustomerNumber) this.getValue()).toString();
    }
}

Register the editor in the Controller

@InitBinder
public void initBinder(WebDataBinder binder) {

    binder.registerCustomEditor(CustomerNumber.class, new CustomerNumberEditor());
    // ... other editors
}

Change the signature of your controller method accepting CustomerNumber instead of String (whatever your ResponseObject is ...)

@RequestMapping(value = "/number/{customerNumber}")
@ResponseBody
public ResponseObject searchByNumber(@PathVariable("customerNumber") CustomerNumber customerNumber) {
    ...
}
Eruct answered 17/1, 2015 at 17:43 Comment(0)
D
2

You can create the answer you want by using the fields in the ConstraintViolationException with the following method;

    @ExceptionHandler(ConstraintViolationException.class)
    protected ResponseEntity<Object> handlePathVariableError(final ConstraintViolationException exception) {
        log.error(exception.getMessage(), exception);

        final List<SisSubError> subErrors = new ArrayList<>();
        exception.getConstraintViolations().forEach(constraintViolation -> subErrors.add(generateSubError(constraintViolation)));

        final SisError error = generateErrorWithSubErrors(VALIDATION_ERROR, HttpStatus.BAD_REQUEST, subErrors);
        return new ResponseEntity<>(error, HttpStatus.BAD_REQUEST);
    }

You need to added an @Validated annotation to Controller class and any validation annotation before path variable field

Dunno answered 27/8, 2022 at 18:54 Comment(0)
G
0

Path variable may not be linked with any bean in your system. What do you want to annotate with JSR-303 annotations? To validate path variable you should use this approach Problem validating @PathVariable url on spring 3 mvc

Greegree answered 17/10, 2013 at 7:37 Comment(1)
The link you've given says: "If the PathVariable parameter fails validation, then Spring will add the error to the request's BindingResult automatically, you don't need to do that yourself." Does that mean bean validation is possible on @PathVariable params? It doesn't clearly say whether it is possible or not. May be I'm mis-understanding. I've tried it the same way as suggested but the Binding result does not have any errors when validation fails.Sawfish
M
0

Actually there is a very simple solution to this. Add or override the same controller method with its request mapping not having the placeholder for the path variable and throw ResponseStatusException from it. Code given below

 @RequestMapping(value = "/number")
 @ResponseBody
 public ResponseObject searchByNumber() {
 throw new ResponseStatusException(HttpStatus.BAD_REQUEST,"customer number missing")
 }
Mattie answered 11/1, 2023 at 11:17 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.