214

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

10 Answers 10

215

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();
}
Sign up to request clarification or add additional context in comments.

5 Comments

@Shamik: This is a compelling reason not to use path variables, in my opinion. The combinatorial proliferation can quickly get out of hand.
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.
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
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: stackoverflow.com/questions/17821731/…
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
140

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"
    }       
}

5 Comments

you sure this would work? Your own answer here 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.
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.
Is it possible that the same value, etc., type would be RequestParam and RequestParam too?
It works, but anyway the controller's function won't be called because it expects a parameter there
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.
83

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();
    }
}

1 Comment

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" }
33

You could use a :

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

for optional params rather than a pathVariable

5 Comments

This is version specific, it seems. No go for Spring 3.
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.
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.
PathVariable and RequestParam are different.
This is explained well in this baeldung post link
23

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));
}

Comments

8

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();
  }

2 Comments

@PathVariable(required = false) not supported when i tried?!
What springboot version? Maybe it has been removed LOL
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.

Comments

2

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

Comments

2

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);
}

Comments

-6
$.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 ) {}

1 Comment

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).

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.