Spring BootRequest Parameters
Spring Boot

Request Parameters

Spring Boot binds incoming HTTP data to method parameters through a set of binding annotations. @RequestParam binds query string values, @PathVariable captures URI template segments, @RequestBody deserializes the request body, @RequestHeader reads headers, and @CookieValue reads cookies. This entry covers all five, their attributes, type conversion, and common patterns.

@RequestParam — Query and Form Parameters

@RequestParam binds a query string parameter or a form field to a method parameter. Mark optional parameters with required = false and provide a defaultValue to avoid NullPointerException. For multi-value parameters, bind to List<T>.
Java
@RestController
@RequestMapping("/api/v1/products")
@RequiredArgsConstructor
@Validated
public class ProductController {

    private final ProductService productService;

    // ── Required parameter ─────────────────────────────────────────────
    @GetMapping("/search")
    public List<ProductResponse> search(
            @RequestParam String query) {   // 400 if missing
        return productService.search(query);
    }

    // ── Optional with default ──────────────────────────────────────────
    @GetMapping
    public Page<ProductResponse> findAll(
            @RequestParam(required = false) String category,
            @RequestParam(defaultValue = "0")  int page,
            @RequestParam(defaultValue = "20") int size,
            @RequestParam(defaultValue = "createdAt") String sort) {
        return productService.findAll(category, page, size, sort);
    }

    // ── Renamed parameter ─────────────────────────────────────────────
    @GetMapping("/by-sku")
    public ProductResponse findBySku(
            @RequestParam("sku_code") String sku) {   // ?sku_code=ABC123
        return productService.findBySku(sku);
    }

    // ── Multi-value: ?tag=electronics&tag=sale ────────────────────────
    @GetMapping("/tagged")
    public List<ProductResponse> findByTags(
            @RequestParam List<String> tag) {
        return productService.findByTags(tag);
    }

    // ── Enum binding ──────────────────────────────────────────────────
    @GetMapping("/by-status")
    public List<ProductResponse> findByStatus(
            @RequestParam ProductStatus status) {   // ?status=ACTIVE
        return productService.findByStatus(status);
    }

    // ── Validated parameter ───────────────────────────────────────────
    @GetMapping("/range")
    public List<ProductResponse> findByPriceRange(
            @RequestParam @DecimalMin("0.0") BigDecimal minPrice,
            @RequestParam @DecimalMin("0.0") BigDecimal maxPrice) {
        return productService.findByPriceRange(minPrice, maxPrice);
    }
}

@PathVariable — URI Template Segments

@PathVariable captures a dynamic segment from the URI path. The variable name defaults to the parameter name; use @PathVariable("name") when they differ or when the segment name contains a hyphen. Multiple variables are independent and can appear anywhere in the path template.
Java
@RestController
@RequestMapping("/api/v1")
@RequiredArgsConstructor
public class OrderController {

    private final OrderService orderService;

    // ── Single variable ────────────────────────────────────────────────
    @GetMapping("/orders/{id}")
    public ResponseEntity<OrderResponse> findById(
            @PathVariable Long id) {
        return ResponseEntity.ok(orderService.findById(id));
    }

    // ── Multiple variables ─────────────────────────────────────────────
    @GetMapping("/users/{userId}/orders/{orderId}")
    public ResponseEntity<OrderResponse> findUserOrder(
            @PathVariable Long userId,
            @PathVariable Long orderId) {
        return ResponseEntity.ok(
            orderService.findByUserAndId(userId, orderId));
    }

    // ── Renamed binding (hyphenated segment name) ─────────────────────
    @GetMapping("/orders/{order-id}/items/{item-id}")
    public ResponseEntity<OrderItemResponse> findItem(
            @PathVariable("order-id") Long orderId,
            @PathVariable("item-id")  Long itemId) {
        return ResponseEntity.ok(
            orderService.findItem(orderId, itemId));
    }

    // ── Regex constraint ───────────────────────────────────────────────
    @GetMapping("/orders/{id:[0-9]+}")
    public ResponseEntity<OrderResponse> findByNumericId(
            @PathVariable Long id) {
        return ResponseEntity.ok(orderService.findById(id));
    }

    // ── String path variable ───────────────────────────────────────────
    @GetMapping("/orders/ref/{reference}")
    public ResponseEntity<OrderResponse> findByReference(
            @PathVariable String reference) {
        return ResponseEntity.ok(
            orderService.findByReference(reference));
    }
}

@RequestBody — Deserializing the Request Body

@RequestBody reads and deserializes the HTTP request body using the registered HttpMessageConverter. Combine it with @Valid to trigger Bean Validation. For optional bodies or partial updates, use required = false — but prefer a dedicated PATCH DTO with all fields nullable.
Java
@RestController
@RequestMapping("/api/v1/users")
@RequiredArgsConstructor
public class UserController {

    private final UserService userService;

    // ── Required body + validation ─────────────────────────────────────
    @PostMapping
    public ResponseEntity<UserResponse> create(
            @RequestBody @Valid CreateUserRequest request,
            UriComponentsBuilder ucb) {
        UserResponse created = userService.create(request);
        return ResponseEntity.created(
            ucb.path("/api/v1/users/{id}")
               .buildAndExpand(created.id()).toUri())
            .body(created);
    }

    // ── Full replacement ───────────────────────────────────────────────
    @PutMapping("/{id}")
    public ResponseEntity<UserResponse> update(
            @PathVariable Long id,
            @RequestBody @Valid UpdateUserRequest request) {
        return ResponseEntity.ok(userService.update(id, request));
    }

    // ── Partial update — all fields nullable ──────────────────────────
    @PatchMapping("/{id}")
    public ResponseEntity<UserResponse> patch(
            @PathVariable Long id,
            @RequestBody @Valid PatchUserRequest request) {
        return ResponseEntity.ok(userService.patch(id, request));
    }

    // ── Raw Map for dynamic / schema-less payloads ────────────────────
    @PatchMapping("/{id}/metadata")
    public ResponseEntity<Void> patchMetadata(
            @PathVariable Long id,
            @RequestBody Map<String, Object> metadata) {
        userService.patchMetadata(id, metadata);
        return ResponseEntity.noContent().build();
    }

    // ── List body ─────────────────────────────────────────────────────
    @PostMapping("/batch")
    public ResponseEntity<List<UserResponse>> createBatch(
            @RequestBody @Valid List<CreateUserRequest> requests) {
        return ResponseEntity.status(HttpStatus.CREATED)
            .body(userService.createBatch(requests));
    }
}

@RequestHeader — Reading HTTP Headers

@RequestHeader binds a named HTTP header to a method parameter. Mark optional headers with required = false to avoid 400 errors when the client omits them. To read all headers at once, bind to HttpHeaders or Map<String, String>.
Java
@RestController
@RequestMapping("/api/v1/events")
@RequiredArgsConstructor
public class EventController {

    private final EventService eventService;

    // ── Required header ────────────────────────────────────────────────
    @PostMapping("/webhook")
    public ResponseEntity<Void> webhook(
            @RequestHeader("X-Webhook-Secret") String secret,
            @RequestBody WebhookPayload payload) {
        eventService.processWebhook(secret, payload);
        return ResponseEntity.ok().build();
    }

    // ── Optional header with default ──────────────────────────────────
    @GetMapping
    public List<EventResponse> findAll(
            @RequestHeader(value = "X-Tenant-ID",
                           required = false,
                           defaultValue = "default") String tenantId) {
        return eventService.findAll(tenantId);
    }

    // ── Standard headers ──────────────────────────────────────────────
    @GetMapping("/{id}")
    public ResponseEntity<EventResponse> findById(
            @PathVariable Long id,
            @RequestHeader(value = HttpHeaders.IF_NONE_MATCH,
                           required = false) String etag,
            @RequestHeader(value = HttpHeaders.ACCEPT_LANGUAGE,
                           required = false,
                           defaultValue = "en") String lang) {
        EventResponse event = eventService.findById(id, lang);
        String currentEtag = """ + event.version() + """;
        if (currentEtag.equals(etag)) {
            return ResponseEntity.status(HttpStatus.NOT_MODIFIED).build();
        }
        return ResponseEntity.ok().eTag(currentEtag).body(event);
    }

    // ── All headers as a map ───────────────────────────────────────────
    @PostMapping("/debug")
    public Map<String, String> debug(
            @RequestHeader Map<String, String> headers) {
        return headers;
    }

    // ── Bind to HttpHeaders for typed access ──────────────────────────
    @GetMapping("/typed-headers")
    public ResponseEntity<Void> typedHeaders(
            @RequestHeader HttpHeaders headers) {
        MediaType contentType = headers.getContentType();
        List<Locale> langs    = headers.getAcceptLanguage()
            .stream().map(r -> Locale.forLanguageTag(r.getRange())).toList();
        return ResponseEntity.ok().build();
    }
}

@ModelAttribute and @CookieValue

@ModelAttribute binds multiple query parameters or form fields to a single object by field name — ideal for filter DTOs. @CookieValue reads a named cookie from the request. Both support required and defaultValue attributes.
Java
// ── @ModelAttribute — binds query params to a DTO ────────────────────
@GetMapping("/search")
public Page<ProductResponse> search(
        @ModelAttribute @Valid ProductFilter filter,
        @PageableDefault(size = 20) Pageable pageable) {
    // GET /products/search?category=books&minPrice=10&maxPrice=50
    // Spring maps each query param to the matching field in ProductFilter
    return productService.search(filter, pageable);
}

@Data
public class ProductFilter {
    private String     category;
    @DecimalMin("0") private BigDecimal minPrice;
    @DecimalMin("0") private BigDecimal maxPrice;
    private ProductStatus status;

    @DateTimeFormat(iso = DateTimeFormat.ISO.DATE)
    private LocalDate addedAfter;
}

// ── @CookieValue — read a named cookie ────────────────────────────────
@GetMapping("/preferences")
public UserPreferences getPreferences(
        @CookieValue(value = "user_prefs",
                     required = false) String prefsJson) {
    if (prefsJson == null) return UserPreferences.defaults();
    return userService.parsePreferences(prefsJson);
}

// ── Track session with a cookie ────────────────────────────────────────
@GetMapping("/cart")
public CartResponse getCart(
        @CookieValue(value = "session_id",
                     required = false,
                     defaultValue = "") String sessionId,
        HttpServletResponse response) {

    if (sessionId.isEmpty()) {
        sessionId = UUID.randomUUID().toString();
        Cookie cookie = new Cookie("session_id", sessionId);
        cookie.setHttpOnly(true);
        cookie.setSecure(true);
        cookie.setMaxAge(7 * 24 * 3600);   // 7 days
        response.addCookie(cookie);
    }
    return cartService.getOrCreate(sessionId);
}