Understand what Spring @PathVariable annotation is and practice examples of reading request URL Path Variables in Spring Rest Controllers using @PathVariable.
Tutorial Contents
Overview
In this tutorial, we will learn about @PathVariable annotation in Spring applications. We use this annotation on controller method parameters to map specific variables in the request URI template into them.
First, we will have a basic overview of the annotation. Then, we will explore various ways of binding request URI path variables to controller method arguments – like mapping a single variable, mapping multiple variables, or mapping variables of a specific type and name. We will also cover how to map all the path variables using a Java HashMap or How to allow some path variables to be optional.
@PathVariable Overview
The Spring @PathVariable annotation is helpful to bind request URI template variables into controller method parameters. We can use this annotation on the arguments of a controller method, which is mapped to a request, using @RequestMapping annotation (or Specific method mapping @GetMapping, @PostMapping, etc.).
@Target({ElementType.PARAMETER})
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface PathVariable {
@AliasFor("name")
String value() default "";
@AliasFor("value")
String name() default "";
boolean required() default true;
}
Code language: Java (java)
There are only two fields in the annotation:
name: The name field denotes the name of a Request URI template variable or a path variable. We need to use it when the name of a path variable and the name of its respective method argument are different.
required: Denotes if the request URI path variable is mandatory. All the fields marked with @PathVariable are, by default, mandatory.
Reading Path Variable from Request URI
We will quickly see how we can map a simple URI Path Variable into a method argument.
Let’s create a controller endpoint that accepts a single Path Variable.
@GetMapping("/v1/students/{id}")
public String mapSimplePathVariable(@PathVariable String id) {
return "id: " + id;
}
Code language: Java (java)
We have used @PathVariable annotation on the method argument to match the URI template variable {id}.
Now, we will test if the URI template variable is correctly mapped to the controller method argument.
~ curl http://localhost:8080/v1/students/ap4324
--
id: ap4324
Code language: Bash (bash)
The output shows that the path variable id is correctly read into the controller method argument. Note that you can also test this functionality using any browser.
Mapping Path Variable by Name
In the previous example, the method argument and the path variable had the same name. However, we may want to name the method argument differently. In such cases, we can use the name field in the @PathVariable.
Our Controller endpoint reads a URI path variable by name.
@GetMapping("/v2/students/{id}")
public String mapPathVariableByName(
@PathVariable("id") String studentId) {
return "id: " + studentId;
}
Code language: Java (java)
~ curl http://localhost:8080/v2/students/ap213
--
id: ap213
Code language: Bash (bash)
Mapping Multiple Path Variables
We can also read multiple path variables from a single request URI and map them into respective method arguments.
@GetMapping("/v3/students/{id}/terms/{termId}")
public String mapMultiplePathVariables(
@PathVariable String id,
@PathVariable String termId) {
return "id: " + id + ", term id: " + termId;
}
Code language: Java (java)
Here, we have two URI template variables and the same number of method arguments.
~ curl http://localhost:8080/v3/students/ap2121/terms/2
--
id: ap2121, term id: 2
Code language: Bash (bash)
Mapping Multiple Path Variables as Java Map
We can bind multiple path variables in individual arguments of a constructor method. However, we can also bind more than one path variable from a request as a method parameter of type Java Map.
@GetMapping("/v4/students/{id}/terms/{termId}")
public String mapMultiplePathVariablesAsMap(
@PathVariable Map<String, String> pathVariables) {
return pathVariables.toString();
}
Code language: Java (java)
All the URI Path Variables are, by default, read as a String. Hence, we have used Map of type Map<String, String>
~ curl http://localhost:8080/v4/students/ap2121/terms/2
--
{id=ap2121, termId=2}
Code language: Bash (bash)
Mapping Path Variable with Specific Data Type
By default, all the URI template variables are String type. However, Spring @PathVariable allows us to use more specific data types in the method argument.
Example of reading Path Variable of type Long and mapping it with method argument.
@GetMapping("/v5/students/{id}")
public String mapPathVariableAsLong(
@PathVariable("id") Long id) {
return "id: " + id;
}
Code language: Java (java)
~ curl http://localhost:8080/v5/students/3243
--
id: 3243
Code language: Bash (bash)
When we use a method argument of a specific type, Spring casts the String path variables into that type. When it fails to cast, it returns 400.
For example, we will pass non-numeric characters in the previous request.
~ curl http://localhost:8080/v5/students/ap12323
--
{
"timestamp":"...",
"status":400,
"error":"Bad Request",
"path":"/v5/students/ap12323"
}
Code language: Bash (bash)
Mapping Optional Path Variables
By default, all the path variables marked with @PathVariable annotation are mandatory.
We will invoke our previously written endpoint that maps multiple path variables to try. However, we will not pass a path variable this time.
~ curl http://localhost:8080/v3/students/ap12323/terms/
--
{
"timestamp":"...",
"status":404,
"error":"Not Found",
"path":"/v3/students/ap12323/terms/"
}
Code language: Bash (bash)
If a path variable is missing, like in our case, Spring returns 400. However, Spring also allows some ways to make the Path Variables optional.
Not Required Path Variables
The first way is to use the required=false
flag on the @PathVariable annotation. Let’s create a new controller endpoint that can read optional URI template variables from a request.
@GetMapping({
"/v6/students/{id}/terms/{termId}",
"/v6/students/{id}/terms/"
})
public String mapNotRequiredPathVariables(
@PathVariable String id,
@PathVariable(required = false) String termId) {
return "id: " + id + ", term id: " + termId;
}
Code language: Java (java)
Note that we have mapped two different URI patterns to the endpoint. The first URI Pattern consists of two variables, while the second has just one. To make the path variable optional, we have used the flag required = false.
~ curl http://localhost:8080/v6/students/ap12323/terms/
--
id: ap12323, term id: null
Code language: Bash (bash)
We can see that the second path variable is null, and our request did not fail this time. However, if we include the path variable, Spring will correctly read it.
~ curl http://localhost:8080/v6/students/ap12323/terms/2
--
id: ap12323, term id: 2
Code language: Bash (bash)
Optional Path Variables
Alternatively, we can use Java Optional to read non-mandatory path variables from a request using @PathVariable.
We are mapping two different URI Patterns to the same controller method. The first one includes the path variable, and the second doesn’t.
@GetMapping({
"/v7/students/{id}/terms/{termId}",
"/v7/students/{id}/terms/"
})
public String mapOptionalPathVariables(
@PathVariable String id,
@PathVariable Optional<String> termId) {
return "id: " + id + ", term id: " + termId.orElse("3");
}
Code language: Java (java)
We are not using the required=false flag here. Instead, we are using Java Optional to make the Path Variable non-mandatory. Using Java Optional, we can also set a default value for the optional @PathVariable.
~ curl http://localhost:8080/v7/students/ap12323/terms/
--
id: ap12323, term id: 3
Code language: Bash (bash)
Note that we did not include the optional path variable in the request and got its default value in the response. However, if we send the variable in the URL, we get the actual value.
~ curl http://localhost:8080/v7/students/ap12323/terms/2
--
id: ap12323, term id: 2
Code language: Bash (bash)
Using Two Different Endpoints
Lastly, we can also support optional (not required) URI template variables using two different controller endpoints.
Instead of mapping two different URI templates to a single method, we will create two different methods.
@GetMapping("/v8/students/{id}/terms/{termId}")
public String mapPathVariableWithTermId(
@PathVariable String id,
@PathVariable String termId) {
return "id: " + id + ", term id: " + termId;
}
@GetMapping("/v8/students/{id}/terms/")
public String mapPathVariableWithoutTermId(
@PathVariable String id) {
return "id: " + id;
}
Code language: Java (java)
Both mapped URI templates are the same, except that the second Path Variable is missing in the second one.
~ curl http://localhost:8080/v8/students/ap12323/terms/2
id: ap12323, term id: 2
Code language: Bash (bash)
Calling the URI that includes the path variable correctly invokes the respective endpoint. However, excluding the variable will invoke the other endpoint.
~ curl http://localhost:8080/v8/students/ap12323/terms/
id: ap12323
Code language: Bash (bash)
Thus, we implemented an effectively optional @PathVariables using two separate endpoints.
Summary
We explored How to use Spring @PathVariable annotation to read and map URI Template variables into controller method arguments.
We tried different ways and scenarios like – mapping a single path variable, multiple path variables, and optional path variables. Also, we learned that we could easily map Path Variables using the most specific data type or map multiple path variables as Java HashMap.
You can refer to our GitHub Repository for the complete source code of the examples used in this tutorial.