🧭 @PathVariable — URL Path Segments¶
What it does: Binds URL template variables from the path to controller method parameters.
Use it when the variable is part of the resource identity: /users/{id}, /shops/{shopId}/orders/{orderId}. For query filters or toggles, prefer @RequestParam.
1) Daily patterns¶
// /users/42
@GetMapping("/users/{id}")
public UserDto get(@PathVariable long id) { /* ... */ }
// Multiple variables: /shops/7/orders/99
@GetMapping("/shops/{shopId}/orders/{orderId}")
public OrderDto get(@PathVariable long shopId, @PathVariable long orderId) { /* ... */ }
// Rename the variable: /files/2025-10/report.pdf
@GetMapping("/files/{fileName}")
public FileDto get(@PathVariable("fileName") String name) { /* ... */ }
// Grab them all (debuggy): /echo/anything/here
@GetMapping("/echo/{x}/{y}")
public Map<String,String> echo(@PathVariable Map<String,String> vars) { return vars; }
2) Type conversion (built-in + custom)¶
Spring converts strings from the path via the ConversionService.
Works out-of-the-box for: primitives/wrappers, UUID, enums, LocalDate/LocalDateTime, BigDecimal, etc.
// /invoices/550e8400-e29b-41d4-a716-446655440000
@GetMapping("/invoices/{id}")
public InvoiceDto get(@PathVariable UUID id) { /* ... */ }
// Custom type
@Component
class SlugToProductId implements Converter<String, ProductId> {
public ProductId convert(String s) { return ProductId.of(s.toLowerCase()); }
}
@GetMapping("/products/{pid}")
public ProductDto get(@PathVariable ProductId pid) { /* ... */ }
3) Constraining with regex¶
Lock paths down to valid shapes:
// digits only: /users/123
@GetMapping("/users/{id:\\d+}")
// year-month: /reports/2025-10
@GetMapping("/reports/{ym:\\d{4}-\\d{2}}")
// enum-like set: /status/ACTIVE
@GetMapping("/status/{s:ACTIVE|SUSPENDED}")
Regex is per-segment (no slashes unless you use a catch-all—see below).
4) Catch-all (include slashes)¶
Sometimes you need “the rest of the path,” e.g., serving files under a base.
- AntPathMatcher style (older config):
"{path:**}" - PathPatternParser style (modern Spring/Boot 3+):
"{*path}"
// /raw/a/b/c.txt → path = "a/b/c.txt"
@GetMapping("/raw/{*path}") // If PathPatternParser is enabled (default in recent Boot)
public Resource raw(@PathVariable String path) { /* ... */ }
// Fallback if using AntPathMatcher:
@GetMapping("/raw/{path:**}")
public Resource rawLegacy(@PathVariable("path") String path) { /* ... */ }
5) Optional path variables (the right way)¶
Path variables are required by default. To make a segment optional, provide two mappings and mark the parameter optional.
// /users → id = null
// /users/42 → id = 42
@GetMapping({"/users", "/users/{id}"})
public List<UserDto> list(@PathVariable(required = false) Long id) { /* ... */ }
There’s no native “optional segment” syntax in a single pattern for MVC; use multiple paths.
6) Dots, dashes, spaces & URL decoding¶
- Spring decodes
%20→ space,%2B→+, etc. - Dots (
.) are safe in recent Spring Boot defaults (suffix pattern matching is off by default). If you still see truncation after a dot, ensure you’re using the modern PathPatternParser and not legacy suffix pattern matching. - A single
{var}never includes slashes. Use a catch-all to include/.
7) Validation & error handling¶
If binding fails:
- Missing variable →
MissingPathVariableException(500 if your method signature requires it but mapping doesn’t define it; usually a route/config bug). - Type mismatch →
MethodArgumentTypeMismatchException(400).
Centralize:
@RestControllerAdvice
class ApiErrors {
@ExceptionHandler(MethodArgumentTypeMismatchException.class)
@ResponseStatus(HttpStatus.BAD_REQUEST)
ErrorDto badPath(MethodArgumentTypeMismatchException ex) {
return new ErrorDto("BAD_PATH", ex.getMessage());
}
}
You can add Bean Validation to converted types if you bind to a record/POJO via @ModelAttribute (less common for pure path vars).
8) Versioning & hierarchy examples¶
// Identity first, then sub-resource
@GetMapping("/users/{id}/addresses/{addrId}")
public AddressDto get(@PathVariable long id, @PathVariable long addrId) { /* ... */ }
// API version in the path
@GetMapping("/v1/users/{id}")
public UserDto v1(@PathVariable long id) { /* ... */ }
9) @PathVariable vs friends¶
@PathVariable— identity in the URL:/users/{id}.@RequestParam— filters/sorting/pagination:/users?page=2&active=true.@RequestBody— JSON body for create/update.@MatrixVariable— semi-colon parameters in a path segment (/cars;color=red;year=2025) when matrix vars are enabled.
10) Trailing slashes, case, and matching engine¶
- Trailing slash:
/users/42/vs/users/42— by default, treated differently. Add both mappings or configure to be tolerant. - Case sensitivity: paths are case-sensitive by default.
- Matching engine: Modern Spring MVC favors PathPatternParser (faster, more precise). It changes catch-all syntax (
{*var}) compared to the older Ant style ({var:**}). Pick one and stick with it across your project.
11) Minimal idiomatic set¶
// 1) Simple identity
@GetMapping("/items/{id}")
public ItemDto one(@PathVariable long id) { /* ... */ }
// 2) Nested resource
@GetMapping("/shops/{shopId}/items/{itemId}")
public ItemDto shopItem(@PathVariable long shopId, @PathVariable long itemId) { /* ... */ }
// 3) Regex guard
@GetMapping("/tickets/{num:\\d+}")
public TicketDto ticket(@PathVariable int num) { /* ... */ }
// 4) Catch-all
@GetMapping("/assets/{*path}")
public Resource asset(@PathVariable String path) { /* ... */ }
12) Quick reference table¶
| Need | Pattern | Example |
|---|---|---|
| Single segment | /{id} |
/users/42 |
| Rename arg | @PathVariable("userId") Long id |
/users/42 |
| Multiple vars | /{a}/{b} |
/a/10/b/20 |
| Regex constraint | /{id:\\d+} |
/users/123 only |
| Catch-all (modern) | /{*path} |
/raw/a/b/c.txt |
| Catch-all (legacy Ant) | /{path:**} |
/raw/a/b/c.txt |
| Optional segment | @GetMapping({"/u", "/u/{id}"}) |
/u or /u/7 |
| Map of vars | @PathVariable Map<String,String> |
/x/1/y/2 |
| Type conversion | UUID, Enum, LocalDate, custom Converter |
/inv/uuid |
Mental model¶
@PathVariable is about identity baked into the URL. It’s segment-based, decoded to types, and happiest when the path shape is explicit: constrain with regex when it helps, and use catch-alls only where they truly make sense.