Spring Bootの`@RequestParam`と`@PathVariable`は、どちらもURLまわりの値をControllerで受け取るアノテーションですが、値を取り出す場所が違います。`/products/42`のようなパス内のIDは`@PathVariable`、`/products?category=book&page=2`のような検索条件は`@RequestParam`が基本です。URL設計で迷ったら「対象を特定する値か、一覧を絞り込む条件か」を先に考えましょう。
最初に比較:同じURLでも受け取る場所が違う
| 観点 | @PathVariable | @RequestParam |
|---|---|---|
| 受け取る場所 | URLパスの`{id}`などの変数 | クエリ文字列(Spring MVCではフォームパラメータも対象) |
| 代表URL | `GET /products/42` | `GET /products?category=book` |
| よくある用途 | 詳細対象のID、親子リソースの識別子 | 検索、並び替え、ページ番号、絞り込み |
| Controller側の例 | `@PathVariable("id") Long id` | `@RequestParam("category") String category` |
| 省略可能な値 | URLパターン自体との一致も考慮する | `required=false`や`defaultValue`で扱える |
どちらが技術的に優秀という話ではありません。APIのURLから何がリソースを指し、何が検索条件なのかが読み取れる形にするのが目的です。これらはSpring MVCのアノテーションであり、ここではServletベースのSpring Boot Web MVCを対象とします。
@PathVariable:URLパスのIDを受け取る
商品詳細を`/products/42`で表示する場合、`42`はパスの一部分です。`@GetMapping("/{id}")`と`@PathVariable`を組み合わせると、その部分を引数に受け取れます。
@RestController
@RequestMapping("/products")
class ProductController {
private final ProductService products;
ProductController(ProductService products) {
this.products = products;
}
@GetMapping("/{id}")
ProductDto findOne(@PathVariable("id") Long id) {
return products.findById(id);
}
}
`GET /products/42`なら`id`へ数値42が渡ります。上記の`ProductService`と`ProductDto`は、アプリケーション側で別途定義する型です。この例ではメソッドを短くして、受け取り方に絞っています。
パスに必要な部分が存在しなければ、まずURLがマッピングに一致するかどうかの問題になります。`@PathVariable(required=false)`を付けただけで`/products`へ自動的に同じメソッドが対応するわけではありません。省略を許すなら、別のマッピングやパターンを設計します。
@RequestParam:検索条件やページ番号を受け取る
一覧ページの「カテゴリ」と「何ページ目か」は、通常は特定リソースそのものではなく結果を調整する条件です。`?`以降のクエリパラメータに置くと、URLの意図が見えやすくなります。
@RestController
@RequestMapping("/products")
class ProductSearchController {
private final ProductService products;
ProductSearchController(ProductService products) {
this.products = products;
}
@GetMapping
List<ProductDto> search(
@RequestParam(name = "category", required = false) String category,
@RequestParam(name = "page", defaultValue = "0") int page
) {
return products.search(category, page);
}
}
`GET /products?category=book&page=2`なら`category`に`book`、`page`に2が入ります。`category`を省略した場合は`null`、`page`を省略した場合は既定値の0として扱います。Spring MVCではクエリだけでなく、フォームデータなどのServletリクエストパラメータも`@RequestParam`の対象です。
一覧APIでは不正な負数ページや過大なサイズが送られても安全に処理できるよう、値の検証と上限設定も実装してください。
URL設計は「対象」と「条件」を分けると決めやすい
| やりたいこと | URL例 | 推奨アノテーション |
|---|---|---|
| 商品1件を取得 | `/products/42` | `@PathVariable` |
| 商品一覧をカテゴリで検索 | `/products?category=book` | `@RequestParam` |
| 商品一覧を並び替え | `/products?sort=price` | `@RequestParam` |
| ユーザーの注文を特定 | `/users/7/orders/19` | `@PathVariable`を複数 |
| 注文一覧を日付で絞る | `/users/7/orders?from=2026-10-01` | ユーザーIDは`@PathVariable`、期間は`@RequestParam` |
たとえばユーザーIDを`?userId=7`で渡す設計も技術的に不可能ではありません。ここで示したのはAPI設計を分かりやすく保つための一般的な判断基準であって、ルール違反を機械的に判定するものではありません。
同じControllerで両方を組み合わせてもよい
実際のAPIでは、対象IDと表示条件を一緒に扱うケースがあります。
@GetMapping("/{id}/history")
List<HistoryDto> history(
@PathVariable("id") Long id,
@RequestParam(name = "limit", defaultValue = "20") int limit
) {
return historyService.find(id, limit);
}
このメソッドなら`GET /products/42/history?limit=10`の`42`は商品ID、`10`は返す件数の条件です。上限チェックやアクセス権限の確認は別途必要です。URLのIDを受け取れたことは、そのリソースへのアクセス許可を得たことを意味しません。
値が取れないときの切り分け
- `@PathVariable`が空になる → `@GetMapping`の`{id}`と`@PathVariable("id")`の名前が一致しているか確認する
- `@RequestParam`を省略するとエラー → 既定で必須なので、必要なら`required=false`や`defaultValue`を指定する
- 文字列から数値への変換に失敗 → `Long`や`int`の引数型とURLの入力値を確認する
- URLが404になる → 引数以前に、HTTPメソッドとControllerのマッピングが一致しているか確認する
- `@RequestBody`との違いが曖昧 → リクエスト本文のJSON受信とURL/フォームパラメータ受信は用途を分ける
欠落した必須クエリパラメータや数値変換失敗では、Spring MVCの標準構成で通常クライアントエラーとして扱われますが、実際のHTTP応答は例外ハンドラやアプリ設定によっても変わります。正確に確認するにはコントローラの結合テストで応答を見ましょう。
@RequestBodyや@ModelAttributeと混同しない
JSON本文をJavaオブジェクトへ読み込む`@RequestBody`と、クエリ・フォーム・パスを扱う今回の2つは用途が異なります。フォーム全体をまとめて受け取るなら`@ModelAttribute`も選択肢です。既存記事では、この2つの違いを詳しく整理しています。
(フォームオブジェクトの受け取り方を確認するについては『【初心者向け】Spring Boot のパラメータ受け取り|@ModelAttribute と @RequestBody の違いと使い分け』をご参照ください)
また、`@GetMapping`と`@PostMapping`はHTTPメソッドをどの処理へ結び付けるかという別の話です。
(GETとPOSTの違いを整理するについては『Spring Bootで理解するGETとPOSTの違い|Thymeleaf実装例付きで初心者の画面遷移・エラー対応まで丁寧解説』をご参照ください)
よくある質問
パスに検索キーワードを入れても動きますか?
URIパターンとしてマッピングできれば受け取れます。ただし検索語や絞り込み条件は変化・組合せが多いため、通常はクエリパラメータの方が扱いやすい設計になります。
@PathVariableと@RequestParamを1つのメソッドで使えますか?
はい。パス内の対象IDとクエリのフィルタ条件を同時に引数へ受け取れます。`/products/42/history?limit=10`などが一例です。
@RequestParamのrequired=falseとdefaultValueの違いは?
`required=false`は省略を許して、値がなければnullなどで受け取る使い方です。`defaultValue`は未指定や空値に代入する既定値を指定します。
@PathVariableのIDは安全な値ですか?
いいえ。URLの値は外部入力です。型変換だけに頼らず、必要な入力検証と認可を必ず行ってください。
まとめ:詳細IDはパス、一覧の絞り込みはクエリを第一候補に
- `@PathVariable`はURLの`{id}`などを取り出し、特定対象の識別子に向く。
- `@RequestParam`はクエリやフォームのリクエストパラメータを取り出し、検索条件やページ番号に向く。
- 省略時の扱いは異なる。マッピングそのものと、引数が必須かどうかを分けて考える。
- HTTPメソッド、入力検証、認可は別の設計要素として確認する。
既存のControllerで`@GetMapping`のURLパターンを確認し、各引数が「対象」なのか「条件」なのかを分類するところから始めてみてください。