Here is the English translation:
Once you start writing Controllers in Spring Boot, the first thing you inevitably run into is “how do I receive the values sent from the client?” Query strings, URL paths, JSON bodies, forms. Since the annotation you use differs depending on where the input comes from, at first you tend to wonder, “Was it @RequestParam or @PathVariable for this?”
In this article, we’ll organize things starting from “where the input is located” so that you can confidently choose among the four main annotations.
Quick Reference: The Four Locations of Request Input and Their Annotations
Let’s start with the conclusion. The annotation you use is almost entirely determined by where in the request the value you want to receive is located.
| Location of Input | Example | Annotation to Use |
|---|---|---|
| Single value in query string | ?keyword=spring | @RequestParam |
| Part of the URL path | /users/42 | @PathVariable |
| JSON body | {"name":"太郎"} | @RequestBody |
| Form / bundling multiple parameters | ?page=1&size=20 | @ModelAttribute |
The decision criterion is simple: just look at “where the value is in the URL, or whether it’s in the body.” In the following sections, we’ll go through how to use each one and the points where people tend to stumble, in order.
@RequestParam: Receiving a Single Parameter from a Query String or Form
When receiving a query parameter such as ?keyword=spring, use @RequestParam.
@GetMapping("/search")
public String search(@RequestParam String keyword) {
return "keyword = " + keyword;
}
When you access /search?keyword=spring, keyword gets spring. If the parameter name and the method argument name are the same, you can omit specifying value or name.
By the way, @RequestParam can receive not only GET query parameters but also a single parameter from a form sent as application/x-www-form-urlencoded in the same way. It’s handy to remember that it also works with POST form submissions.
Choosing Between required, defaultValue, and Name Specification
Something that often trips people up in practice is handling required/optional parameters and default values. @RequestParam is required by default, so if the parameter is missing, you get a 400 error.
@GetMapping("/search")
public String search(
@RequestParam(required = false) String keyword,
@RequestParam(defaultValue = "10") int size,
@RequestParam("q") String query) {
return keyword + " / " + size + " / " + query;
}
Setting required = false makes it optional, and when unspecified, null is assigned. However, since null cannot be assigned to a primitive type like int, for numeric values you want to make optional, use a wrapper type such as Integer or use defaultValue.
When you specify defaultValue, that parameter is automatically treated as optional, and the value is supplied when it’s unspecified. When the argument name differs from the parameter name, specify the name explicitly like @RequestParam("q").
Optional parameters can also be received as Optional<String>. This is another option when you want to write explicit null checks.
Receiving Multiple Values as List or T[]
When multiple parameters with the same name arrive, like ?ids=1&ids=2&ids=3, you can receive them as a List.
@GetMapping("/items")
public String items(@RequestParam List<Long> ids) {
return "count = " + ids.size();
}
You can also receive them as a Long[] array instead of List<Long>.
There’s one thing to note here. When you send a single comma-separated parameter like ?ids=1,2,3, Spring will also split it into a List of three elements. However, these two are just different ways of sending, and since their behavior is similar, they tend to get confused. It’s safer to align beforehand on which format the client side uses.
@PathVariable: Receiving Values Embedded in the URL Path
For IDs and the like that uniquely identify a resource, the REST API convention is to embed them in the path rather than the query. Values contained in the path are received with @PathVariable.
@GetMapping("/users/{id}")
public String getUser(@PathVariable Long id) {
return "user id = " + id;
}
@GetMapping("/users/{userId}/posts/{postId}")
public String getPost(
@PathVariable("userId") Long uid,
@PathVariable("postId") Long pid) {
return uid + " / " + pid;
}
The parts written in curly braces, like {id}, are path variables. If the template variable name and the argument name are the same, you can omit the specification; when they differ, specify the name explicitly like @PathVariable("userId").
As a guideline for choosing, it’s easier to organize your thinking if you consider IDs that identify a resource as paths, and conditions for filtering or sorting as queries. /users/42 means “user number 42,” while /users?role=admin means “a list of users filtered by admin.” For the big picture of REST APIs, also refer to Tutorial: Building a REST API CRUD with Spring Boot.
@RequestBody: Mapping a JSON Body to an Object
In APIs that send JSON via POST or PUT, you receive the entire body as a single object. This is where @RequestBody comes in.
@PostMapping("/users")
public String create(@RequestBody UserRequest request) {
return "name = " + request.getName();
}
The DTO for receiving it looks like this.
public class UserRequest {
private String name;
private int age;
public String getName() { return name; }
public void setName(String name) { this.name = name; }
public int getAge() { return age; }
public void setAge(int age) { this.age = age; }
}
The corresponding request looks like this.
POST /users
Content-Type: application/json
{
"name": "太郎",
"age": 30
}
Internally, Jackson reads the JSON and converts it into an object based on the field names. For detailed conversion settings, see How to Handle JSON with Jackson in Spring Boot.
Typical Causes of @RequestBody Failing to Receive JSON
Questions like “all the values come out null” or “I just get a 400” are usually one of the following.
- Content-Type is not set. Unless you send
application/json, Spring won’t treat it as JSON. - The DTO lacks getters/setters or a no-args constructor. Jackson can’t deserialize it and fails. Using Lombok’s
@Dataor similar to provide them is one approach. - Forgetting to add
@RequestBody. If you forget it, the body isn’t read and the fields remain null. - Mismatch between field names and JSON keys. A discrepancy like
userNameversususer_nameresults in just that field being null.
If everything is null, suspect @RequestBody or Content-Type; if only some fields are null, suspect the field names. Narrowing it down this way makes it easier to reach the cause.
@ModelAttribute: Bundling Multiple Form/Query Parameters
When you have multiple parameters and want to bundle them into a single object, as with a search form, @ModelAttribute is convenient.
@GetMapping("/search")
public String search(@ModelAttribute SearchForm form) {
return form.getKeyword() + " / " + form.getPage();
}
If SearchForm has keyword and page fields, ?keyword=spring&page=2 binds directly to them. In fact, complex-type arguments like DTOs are treated as @ModelAttribute by default, so the annotation itself can often be omitted.
The difference from @RequestBody is the format it targets. @RequestBody looks at the JSON body, while @ModelAttribute looks at form- or query-style parameters. Since trying to receive JSON with @ModelAttribute won’t populate the values, be careful not to confuse the two.
Behavior on Type Conversion Failure: 400 Bad Request and the Exceptions Raised
If a non-numeric value like ?id=abc arrives for @RequestParam Long id, Spring fails at type conversion. In this case, MethodArgumentTypeMismatchException is thrown, and by default a 400 Bad Request is returned. The same applies to @PathVariable.
On the other hand, if the JSON syntax is broken or the types don’t match with @RequestBody, an HttpMessageNotReadableException occurs, and this too defaults to a 400.
In other words, Spring rejects cases where the input format is incorrect as a 400 by default. If you want to shape the contents of the error response, you use @ExceptionHandler or @ControllerAdvice, but that’s a separate topic, so we won’t touch on it here.
Which Annotation to Use: Summary of the Decision Flow
Finally, let’s summarize how to choose in a single flow.
- If the value is part of the URL path (
/users/42) →@PathVariable - If it’s a single value in the query after
?→@RequestParam - If you’re turning an entire JSON body into an object →
@RequestBody - If you’re bundling multiple form/query parameters into a single object →
@ModelAttribute
When in doubt, go back to “where is the value located,” and you can almost always narrow it down to a single choice. Once you can receive the values, the next step is input validation to confirm that the received values are correct. Proceeding to How to Validate with @Valid will make the entry point of your Controller much more robust.