211

With Spring 3.0, can I have an optional path variable?

For example

@RequestMapping(value = "/json/{type}", method = RequestMethod.GET)
public @ResponseBody TestBean testAjax(
        HttpServletRequest req,
        @PathVariable String type,
        @RequestParam("track") String track) {
    return new TestBean();
}

Here I would like /json/abc or /json to call the same method.
One obvious workaround declare type as a request parameter:

@RequestMapping(value = "/json", method = RequestMethod.GET)
public @ResponseBody TestBean testAjax(
        HttpServletRequest req,
        @RequestParam(value = "type", required = false) String type,
        @RequestParam("track") String track) {
    return new TestBean();
}

and then /json?type=abc&track=aa or /json?track=rr will work

rogerdpack
  • 62,887
  • 36
  • 269
  • 388
Shamik
  • 6,938
  • 11
  • 55
  • 72

10 Answers10

212

You can't have optional path variables, but you can have two controller methods which call the same service code:

@RequestMapping(value = "/json/{type}", method = RequestMethod.GET)
public @ResponseBody TestBean typedTestBean(
        HttpServletRequest req,
        @PathVariable String type,
        @RequestParam("track") String track) {
    return getTestBean(type);
}

@RequestMapping(value = "/json", method = RequestMethod.GET)
public @ResponseBody TestBean testBean(
        HttpServletRequest req,
        @RequestParam("track") String track) {
    return getTestBean();
}
lambda
  • 3,295
  • 1
  • 26
  • 32
earldouglas
  • 13,265
  • 5
  • 41
  • 50
  • 5
    @Shamik: This is a compelling reason *not* to use path variables, in my opinion. The combinatorial proliferation can quickly get out of hand. – skaffman Feb 05 '11 at 11:40
  • 9
    Actually not because the path can't be that complex while being filled up with optional components. If you have more than one or max two optional path elements you should seriously consider switching a few of them to request parameters. – Patrick Cornelissen Apr 23 '12 at 10:44
  • 1
    And for some people, having the second controller method call the first controller method may work as well, if for instance the differing parameter can be provided by some other means – chrismarx May 02 '12 at 14:06
  • 5
    Please consider updating your answer, instead of creating two controller methods in newer version of Spring we may just use `@RequestMapping` with two values as in: http://stackoverflow.com/questions/17821731/spring-mvc-how-to-indicate-whether-a-path-variable-is-required-or-not – csharpfolk Nov 06 '16 at 18:12
  • omg how do you expect to maintain these endpoints ? And if instead of only one path variable we have 5, do the math for me, how many endpoints would you do ? Please do me a favor and replace `@PathVariable` to `@RequestParam` – Guilherme Alencar Mar 31 '20 at 16:30
137

If you are using Spring 4.1 and Java 8 you can use java.util.Optional which is supported in @RequestParam, @PathVariable, @RequestHeader and @MatrixVariable in Spring MVC -

@RequestMapping(value = {"/json/{type}", "/json" }, method = RequestMethod.GET)
public @ResponseBody TestBean typedTestBean(
    @PathVariable Optional<String> type,
    @RequestParam("track") String track) {      
    if (type.isPresent()) {
        //type.get() will return type value
        //corresponds to path "/json/{type}"
    } else {
        //corresponds to path "/json"
    }       
}
Aniket Thakur
  • 66,731
  • 38
  • 279
  • 289
  • you sure this would work? Your own answer [here](http://stackoverflow.com/a/34670775/1345021) suggests that the controller won't be hit if {type} is absent from the path. I think PathVariable Map would be a better approach, or using separate controllers. – Anshul Tiwari Oct 17 '16 at 13:39
  • 5
    Yes if you just have `"/json/{type}"` and type is not present it will not be hit (as my linked answer suggests) but here you have `value = {"/json/{type}", "/json" }`. So if any one matches controller method will be hit. – Aniket Thakur Oct 17 '16 at 19:04
  • Is it possible that the same value, etc., type would be RequestParam and RequestParam too? – zygimantus Feb 11 '17 at 22:15
  • It works, but anyway the controller's function won't be called because it expects a parameter there – EliuX Mar 30 '17 at 17:15
  • 19
    This works, and since Spring 4.3.3, you can also go with `@PathVariable(required = false)` and get null if the variable is not present. – Nicolai Ehemann Oct 05 '17 at 14:21
81

It's not well known that you can also inject a Map of the path variables using the @PathVariable annotation. I'm not sure if this feature is available in Spring 3.0 or if it was added later, but here is another way to solve the example:

@RequestMapping(value={ "/json/{type}", "/json" }, method=RequestMethod.GET)
public @ResponseBody TestBean typedTestBean(
    @PathVariable Map<String, String> pathVariables,
    @RequestParam("track") String track) {

    if (pathVariables.containsKey("type")) {
        return new TestBean(pathVariables.get("type"));
    } else {
        return new TestBean();
    }
}
Paul Wardrip
  • 964
  • 6
  • 3
  • 1
    I use it all the time .This comes handy when i want a single method to handle different uri types ex:{ "/json/{type}","/json/{type}/{xyz}","/json/{type}/{abc}", "/json/{type}/{abc}/{something}","/json" } – Vaibs Aug 23 '17 at 13:21
32

You could use a :

@RequestParam(value="somvalue",required=false)

for optional params rather than a pathVariable

Maleck13
  • 1,709
  • 15
  • 17
  • 1
    This is version specific, it seems. No go for Spring 3. – Stu Thompson Feb 06 '13 at 18:08
  • 5
    Currently using this method for a spring 3.1 project, and the docs say that it works for 2.5+, so it definitely works for Spring 3. EDIT: [source](http://static.springsource.org/spring/docs/3.0.x/api/org/springframework/web/bind/annotation/RequestParam.html). – Evan Byrne Mar 22 '13 at 16:21
  • 22
    True, but this is not what the question is about. Using *request* parameters is indeed mentioned in the question as *"One obvious workaround"*, but the question itself is about *path* parameters. This is not a solution for optional path parameters. – Arjan Apr 14 '13 at 10:29
  • 9
    PathVariable and RequestParam are different. – Timeless Mar 16 '15 at 01:43
  • This is explained well in this baeldung post [link](https://www.baeldung.com/spring-optional-path-variables) – Paul Salmon Aug 24 '23 at 09:10
21

Spring 5 / Spring Boot 2 examples:

blocking

@GetMapping({"/dto-blocking/{type}", "/dto-blocking"})
public ResponseEntity<Dto> getDtoBlocking(
        @PathVariable(name = "type", required = false) String type) {
    if (StringUtils.isEmpty(type)) {
        type = "default";
    }
    return ResponseEntity.ok().body(dtoBlockingRepo.findByType(type));
}

reactive

@GetMapping({"/dto-reactive/{type}", "/dto-reactive"})
public Mono<ResponseEntity<Dto>> getDtoReactive(
        @PathVariable(name = "type", required = false) String type) {
    if (StringUtils.isEmpty(type)) {
        type = "default";
    }
    return dtoReactiveRepo.findByType(type).map(dto -> ResponseEntity.ok().body(dto));
}
kinjelom
  • 6,105
  • 3
  • 35
  • 61
7

Simplified example of Nicolai Ehmann's comment and wildloop's answer (works with Spring 4.3.3+), basically you can use required = false now:

  @RequestMapping(value = {"/json/{type}", "/json" }, method = RequestMethod.GET)
  public @ResponseBody TestBean testAjax(@PathVariable(required = false) String type) {
    if (type != null) {
      // ...
    }
    return new TestBean();
  }
rogerdpack
  • 62,887
  • 36
  • 269
  • 388
3

Check this Spring 3 WebMVC - Optional Path Variables. It shows an article of making an extension to AntPathMatcher to enable optional path variables and might be of help. All credits to Sebastian Herold for posting the article.

Uresh K
  • 1,136
  • 11
  • 16
2

Here is the answer straight from baeldung's reference page :- https://www.baeldung.com/spring-optional-path-variables

user1354825
  • 1,108
  • 4
  • 21
  • 54
0

thanks Paul Wardrip in my case I use required.

@RequestMapping(value={ "/calificacion-usuario/{idUsuario}/{annio}/{mes}", "/calificacion-usuario/{idUsuario}" }, method=RequestMethod.GET)
public List<Calificacion> getCalificacionByUsuario(@PathVariable String idUsuario
        , @PathVariable(required = false) Integer annio
        , @PathVariable(required = false) Integer mes) throws Exception {
    return repositoryCalificacion.findCalificacionByName(idUsuario, annio, mes);
}
Dave Rincon
  • 308
  • 3
  • 10
-5
$.ajax({
            type : 'GET',
            url : '${pageContext.request.contextPath}/order/lastOrder',
            data : {partyId : partyId, orderId :orderId},
            success : function(data, textStatus, jqXHR) });

@RequestMapping(value = "/lastOrder", method=RequestMethod.GET)
public @ResponseBody OrderBean lastOrderDetail(@RequestParam(value="partyId") Long partyId,@RequestParam(value="orderId",required=false) Long orderId,Model m ) {}
Soner Gönül
  • 97,193
  • 102
  • 206
  • 364
  • 3
    You might want to edit in some text in your answer, explaining *why* you think this contributes to solving the issue at hand (4 years later). – Qirel Oct 13 '15 at 06:54