Why is this an issue?

The @PathVariable annotation in Spring extracts values from the URI path and binds them to method parameters in a Spring MVC controller. It is commonly used with @GetMapping, @PostMapping, @PutMapping, and @DeleteMapping to capture path variables from the URI. These annotations map HTTP requests to specific handler methods in a controller. They are part of the Spring Web module and are commonly used to define the routes for different HTTP operations in a RESTful API.

If a method has a path template containing a placeholder, like "/api/resource/{id}", and there’s no @PathVariable annotation on a method parameter to capture the id path variable, Spring will disregard the id variable.

Path variables can also be automatically bound to class or record properties when used as method parameters (without @PathVariable). For Spring Web versions prior to 5.3, the @ModelAttribute annotation is required on the parameter to enable binding to class properties. Starting with Spring Web 5.3, this binding happens automatically without requiring @ModelAttribute. For classes, Spring binds path variables to properties that have setter methods (including Lombok-generated setters). For records (Spring Web 5.3+), Spring binds path variables to record components through their accessor methods. The @BindParam annotation can be used on record components to specify custom binding names.

This rule will raise an issue if a method has a path template with a placeholder, but no corresponding @PathVariable, matching class property, or matching record component, or vice-versa.

How to fix it

Code examples

Noncompliant code example

@GetMapping("/api/resource/{id}")
public ResponseEntity<String> getResourceById(Long id) { // Noncompliant - The 'id' parameter will not be automatically populated with the path variable value
  return ResponseEntity.ok("Fetching resource with ID: " + id);
}

@GetMapping("/api/asset/")
public ResponseEntity<String> getAssetById(@PathVariable Long id) { // Noncompliant - The 'id' parameter does not have a corresponding placeholder
  return ResponseEntity.ok("Fetching asset with ID: " + id);
}

Compliant solution

@GetMapping("/api/resource/{id}")
public ResponseEntity<String> getResourceById(@PathVariable Long id) { // Compliant
  return ResponseEntity.ok("Fetching resource with ID: " + id);
}

@GetMapping("/api/asset/{id}")
public ResponseEntity<String> getAssetById(@PathVariable Long id) {
  return ResponseEntity.ok("Fetching asset with ID: " + id);
}

Noncompliant code example

// Spring Web < 5.3: Class properties require @ModelAttribute
@Data
class UserRequest {
  private String userId;
  private String accountId;
}

@GetMapping("/api/user/{userId}/{accountId}")
public ResponseEntity<String> getUser(UserRequest request) { // Noncompliant - @ModelAttribute annotation is required for Spring Web < 5.3
  return ResponseEntity.ok("User: " + request.getUserId());
}

Compliant solution

// Spring Web < 5.3: Class properties require @ModelAttribute
@Data
class UserRequest {
  private String userId;
  private String accountId;
}

@GetMapping("/api/user/{userId}/{accountId}")
public ResponseEntity<String> getUser(@ModelAttribute UserRequest request) { // Compliant - @ModelAttribute enables binding to class properties
  return ResponseEntity.ok("User: " + request.getUserId());
}

Noncompliant code example

// Spring Web 5.3+: Class properties
@Data
class UserRequest {
  private String userId;
  private String accountId;
}

@GetMapping("/api/user/{userId}/{companyId}")
public ResponseEntity<String> getUser(UserRequest request) { // Noncompliant - 'companyId' path variable doesn't match any property with a setter
  return ResponseEntity.ok("User: " + request.getUserId());
}

Compliant solution

// Spring Web 5.3+: Class properties
@Data
class UserRequest {
  private String userId;
  private String accountId;
}

@GetMapping("/api/user/{userId}/{accountId}")
public ResponseEntity<String> getUser(UserRequest request) { // Compliant - path variables match properties with setters
  return ResponseEntity.ok("User: " + request.getUserId());
}

Noncompliant code example

// Spring Web 5.3+: Record components
record OrderRequest(String orderId, String customerId) {}

@GetMapping("/api/order/{order-id}/{customer-id}")
public ResponseEntity<String> getOrder(OrderRequest request) { // Noncompliant - Record component names don't match path variables
  return ResponseEntity.ok("Order: " + request.orderId());
}

Compliant solution

// Spring Web 5.3+: Record components
record OrderRequest(@BindParam("order-id") String orderId, @BindParam("customer-id") String customerId) {}

@GetMapping("/api/order/{order-id}/{customer-id}")
public ResponseEntity<String> getOrder(OrderRequest request) { // Compliant - @BindParam maps component names to path variables
  return ResponseEntity.ok("Order: " + request.orderId());
}

Resources

Documentation

Articles & blog posts