Request bodies, path variables and query parameters
The previous lesson introduced the three ways a controller reads input — path variables, query parameters, and the request body. This lesson goes deeper on each: the details that matter in real APIs, the type conversion Spring does, and how JSON becomes a Java object and back. These are verified against the running Dakiya API.
Path variables: identifying a resource
A path variable is part of the URL path, used to identify which resource you mean:
@GetMapping("/{id}")
public ParcelResponse getOne(@PathVariable Long id) { ... } // GET /api/parcels/42
{id} in the path binds to the id parameter. Spring converts the type for you — the URL segment
"42" (a string) becomes a Long (verified: /api/parcels/1 bound id = 1 and returned that parcel). If
the conversion fails (/api/parcels/abc), Spring returns a 400 automatically. You can have several:
@GetMapping("/{hubId}/parcels/{parcelId}") binds both. If the variable name differs from the parameter
name, be explicit: @PathVariable("id") Long parcelId. Use path variables for the identity of a resource —
the thing that says which one — not for filters or options.
Query parameters: filtering, sorting, options
A query parameter comes after the ? and is for modifying a request — filtering, sorting, paging,
optional flags:
@GetMapping
public List<ParcelResponse> list(
@RequestParam(required = false) String city, // ?city=Pune
@RequestParam(defaultValue = "0") int page) { // ?page=2, defaults to 0
...
}
@RequestParam binds a query-string value. Three options you use constantly:
required = false— the parameter is optional; absent meansnull(verified: omitting?cityreturned all parcels,?city=Punefiltered to Pune).defaultValue = "0"— a value used when absent (and it implies optional).- Type conversion again:
?page=2binds to anintautomatically.
The distinction from path variables matters for clean API design: path = which resource, query =
how/which subset. /api/parcels/42 is the parcel 42; /api/parcels?city=Pune&status=BOOKED is a
filtered collection. Putting a filter in the path, or an identity in the query, reads wrong and confuses
clients.
The request body: JSON in
For POST and PUT, the data comes in the request body as JSON, bound with @RequestBody:
@PostMapping
public ResponseEntity<ParcelResponse> create(@RequestBody CreateParcelRequest req) {
// req.recipientName(), req.destinationCity() — populated from the JSON
}
Spring deserialises the JSON body into your object using Jackson (from starter-web). Posting
{"recipientName":"Asha","destinationCity":"Pune"} populates a CreateParcelRequest with those fields
(verified: created a parcel from exactly that body). The binding is by field/record-component name — the
JSON key recipientName maps to the recipientName component. Jackson handles the types (numbers, booleans,
nested objects, lists). You almost never parse JSON by hand in Spring; you declare the shape you expect (a
DTO — next lesson) and @RequestBody fills it.
The response body: JSON out
Returning is the mirror image — Jackson serialises your returned object to JSON:
@GetMapping("/{id}")
public ParcelResponse getOne(@PathVariable Long id) {
return ParcelResponse.from(parcel); // → {"id":1,"trackingCode":"PKG-...","status":"BOOKED",...}
}
Verified: returning a ParcelResponse produced the JSON object shown, and returning a List<ParcelResponse>
produced a JSON array. The mapping is again by property name (a record's components, or a class's getters).
Jackson serialises nested objects and collections recursively, formats enums as their name (Status.BOOKED
→ "BOOKED"), and handles dates per your configuration. So the round trip is symmetric: @RequestBody
turns incoming JSON into your object; returning an object turns it back into JSON — and you work in Java
types throughout, never touching the raw JSON.
Headers, and content types
Occasionally you need a request header or must set a response header:
@GetMapping("/{id}")
public ParcelResponse getOne(@PathVariable Long id,
@RequestHeader(value = "X-Request-Id", required = false) String requestId) {
...
}
@RequestHeader binds a header value. For response headers (and status), use ResponseEntity (previous
lesson) — for example the Location header on a create. One content-type note: Spring reads and writes
JSON by default for @RestController, driven by the Content-Type (incoming) and Accept (outgoing)
headers; a client sending JSON should set Content-Type: application/json, which is why the verified curl
calls included that header. You rarely configure this — JSON is the default for REST APIs and Spring handles
the negotiation.
Check your work
Path variables. @PathVariable binds a {placeholder} in the URL, type-converted ("1" → Long);
for the identity of a resource. Verified: /api/parcels/1 → id = 1.
Query parameters. @RequestParam binds ?key=value, with required=false/defaultValue, type-
converted; for filtering/sorting/paging/options. Verified: ?city=Pune filtered, omitting it returned all.
Path versus query. Path = which resource (/parcels/42); query = how/which subset
(/parcels?city=Pune) — do not swap them.
Request body. @RequestBody deserialises the JSON payload into your object (Jackson), by field name;
for POST/PUT. Verified: a JSON body populated the DTO.
Response body. Returning an object serialises it to JSON (Jackson), by property name, recursively, enums as their name. Verified round trip; you work in Java types, never raw JSON.
Headers. @RequestHeader reads a header; ResponseEntity sets response headers/status; JSON is the
default content type for REST controllers.
Practice
- Add
@PathVariable Long id; request/api/parcels/abc(a non-number) and confirm Spring returns 400 on the failed conversion. - Add
@RequestParam(required=false) String cityand@RequestParam(defaultValue="0") int page; test with and without each. - Reason about (and try) a filter placed in the path vs the query — feel why
/parcels?status=BOOKEDreads better than/parcels/BOOKED. - POST a JSON body and confirm
@RequestBodypopulates your object; add a field to the JSON that the DTO does not have and observe it is ignored. - Return a nested object (a parcel containing a list) and confirm Jackson serialises the whole structure to JSON.
- Read a custom request header with
@RequestHeaderand echo it back in the response.
Official documentation
- Spring — @PathVariable — And the other handler arguments.
- Spring — @RequestBody and @ResponseBody — JSON binding in and out.
- Jackson (JSON) support in Spring — How serialisation works.
Next: DTOs, and why you should not expose entities.
Stuck on this lesson?
Being stuck is part of it — but being stuck alone for three days is not. Our internship programme pairs this curriculum with code review and one-to-one help from working developers, and it is free.
About the internship