Bas Passon

3 minute read

Sometimes you run into something that makes you think twice. While programming a Spring MVC controller I wanted to have an optional path parameter. It is possible just not as obvious as you would expect.

Let’s assume we have a controller that serves persons based on a substring and all persons if you specify no substring at all. A straightforward way of implementing the controller could be something like:

@RestController
@AllArgsConstructor
public class PersonController {

    private PersonRepository personRepository;

    @RequestMapping(path = "/persons/{substring}")
    public List<Person> getMatchingPersons(@PathVariable(name = "substring") String substring) {
        return personRepository.getPersons()
                .filter(p -> p.getName().toLowerCase().contains(substring.toLowerCase()))
                .collect(Collectors.toList());
    }

    @RequestMapping(path = "/persons")
    public List<Person> getPersons() {
        return personRepository.getPersons().collect(Collectors.toList());
    }

In taking a closer look I noticed the /persons mapping is practically the same as /persons/{substring}. The only difference is the path variable {substring} which is present or not. The @PathVariable annotation has the option to make the path variable optional using @PathVariable(required=false). This makes the path argument optional, but consequently the substring method parameter gets assigned null, which leads to less readable code due to a null check now needed to not break the code.

@RestController
@AllArgsConstructor
public class PersonController {

    private PersonRepository personRepository;

    @RequestMapping(path = {"/persons/{substring}", "/persons"})
    public List<Person> getPersons(@PathVariable(name = "substring", required = false) String substring) {
        final String sub = substring == null ? "" : substring;
        return personRepository.getPersons()
                .filter(p -> p.getName().toLowerCase().contains(sub.toLowerCase()))
                .collect(Collectors.toList());
    }
}

In this case it is not too ugly as the default is really simple, but it remains added clutter. Combining the Optional introduced in Java 8 and the fact Spring MVC from version 4 and up allows for path variable binding to an Optional, I came up with a little bit more elegant solution.

@RestController
@AllArgsConstructor
public class PersonController {

    private PersonRepository personRepository;

    @RequestMapping(path = {"/persons/{substring}", "/persons"})
    public List<Person> getPersons(@PathVariable(name = "substring", required = false) Optional<String> substring) {
        return personRepository.getPersons()
                .filter(p -> p.getName().toLowerCase().contains(substring.orElse("").toLowerCase()))
                .collect(Collectors.toList());
    }
}

This way I effectively created an optional path variable with a default. It boiled down to changing the path variable mapping to @PathVariable(name = “substring”, required = false), updating the request mapping to @RequestMapping(path = {“/persons/{substring}”, “/persons”}) and adding a sensible default using the Optional.orElse(T other) method. If you need a not so straighforward default you can use the Optional.orElseGet(Supplier<? extends T> other) method to produce it for you.

Combining the path mappings makes the code in my opinion cleaner. I have to write less request mapping methods. Not everybody likes using Optional as a method parameter, but I guess that is a matter of taste. I personally find writing less code preferable over avoiding the use of optionals.