1. Diagnostics instead of magic
The URL vs. body fork has already ruled out half of the false hypotheses. But the pipeline does not end there: Spring may not find a handler, a URL parameter may not be converted, the request body may not be read, and the response body may not be written after return. That is why the main skill here is not memorizing yet another internal name, but quickly localizing the failure phase. Otherwise it is very easy to fix Jackson when the problem was in @PathVariable, or to debug a service when the controller method was never called at all.
What matters now is not a new mechanism, but a working approach: in 30–60 seconds, understand where it hurts, and only then dig deeper. For a typical REST API, this depth is already enough to stop treating MVC like a lottery.
The request processing chain
When you have one controller method, it seems that "the request reached the method, and that is it." But Spring MVC works like a pipeline: there are stages before entering the method, there is the method itself (and services), and there are stages after return, when the result must be turned into an HTTP response. If you do not see the whole pipeline, any error looks the same: "something is wrong."
Let's fix the practical (not academic) chain. I am deliberately not naming internal classes — you are not required to remember them by name, but you are required to understand the stages:
flowchart TD
A["HTTP request arrived"] --> B["Handler search: mapping"]
B --> C["Preparing method arguments"]
C --> D1["Path/Query conversion ConversionService"]
C --> D2["Body conversion HttpMessageConverter"]
D1 --> E["Entering the controller method"]
D2 --> E
E --> F["Calling service / business logic"]
F --> G["return value"]
G --> H["Writing HTTP response"]
H --> I["Response body conversion HttpMessageConverter"]
H --> J["Status / headers"]
I --> K["HTTP response sent to client"]
J --> K
There is an important detail that many people miss: reading the body and writing the body are independent. For example, you can read the request body (POST with @RequestBody), but not write a response body (return 204 No Content). Or vice versa: GET has no request body, but it does have a response body.
A tiny code example to feel this independence (imagine this is a fragment inside TaskController):
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
@PostMapping("/api/v1/tasks")
public ResponseEntity<Void> create(@RequestBody CreateTaskBody body) {
// IMPORTANT: here Spring must read the request body and build CreateTaskBody,
// otherwise the body parameter will simply not appear.
// But there will be no body in the response: we return 204 No Content.
return ResponseEntity.noContent().build();
}
Here the request body must be read, otherwise body will not appear. But the response body will not be written, because there is none.
2. Fast problem localization
Did the request reach the method
The first diagnostic question sounds almost rude: "Are you sure your method actually ran?" It is rude because sometimes the answer is "no." And that is normal: in many errors, the stages before entering the method are to blame. If you are trying to debug a service while Spring could not even parse the JSON, you are literally fixing a refrigerator when the power outlet is not plugged in.
The simplest and most honest way to check "reached / did not reach" is to set a breakpoint or, for learning purposes, temporarily use System.out.println. In production, of course, a logger is better, but the idea matters right now.
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
@PostMapping("/api/v1/tasks")
public ResponseEntity<Void> create(@RequestBody CreateTaskBody body) {
// Learning marker: if it is not in the console/debugger, the method was not reached.
// In production, use a logger instead of System.out.
System.out.println("TaskController#create called"); // TaskController#create called
return ResponseEntity.noContent().build();
}
If you send a request and do not see this output (or the breakpoint does not trigger), the problem is before the method. From there, you do not guess; you ask the next question: "Why did it not reach it?" — and that is where concrete variants begin.
Beginners very often confuse two situations. In one, Spring did not find a handler at all (the path/method did not match); in the other, a handler was found, but the arguments could not be prepared (the query was not converted, the JSON was not read). In both cases, "the method was not called," but the reasons are different — and the fixes are different too.
Data source: URL or body
The second diagnostic question usually sounds like this: "What exactly was Spring trying to convert?" The error "failed to read value" may refer to a query parameter, JSON body, or path variable. If you do not separate these sources, you will look for the problem in Jackson when it has nothing to do with JSON at all. And that is, at the very least, 20 extra minutes of your life (and one sad cup of tea).
Let's fix a simple rule: @PathVariable and @RequestParam are strings from the URL that must be converted to a type. @RequestBody is the message body, which must be read as a whole document (for example, JSON).
It is convenient to keep the comparison in mind as a table:
| Data source | What the "raw" input looks like | Which mechanism | Typical example |
|---|---|---|---|
| Path variable | a small piece of a string | ConversionService | /tasks/{taskId} |
| Query parameter | a small piece of a string | ConversionService | ?page=0&dueBefore=2026-03-21 |
| Request body | a whole document (JSON text) | HttpMessageConverter + Jackson | {"title":"Fix API"} |
| Response body | a whole document (JSON text) | HttpMessageConverter + Jackson | {"id":"...","title":"..."} |
The same Java type can appear in both worlds. For example, LocalDate can come from a query, or it can come from JSON. These are different pipelines:
import java.time.LocalDate;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestParam;
@GetMapping("/api/v1/tasks")
public String find(@RequestParam LocalDate dueBefore) {
// dueBefore comes from the query string, i.e. this is URL conversion through ConversionService.
return dueBefore.toString();
}
Here LocalDate is built from a query parameter string.
And here LocalDate is built from JSON:
import java.time.LocalDate;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
@PostMapping("/api/v1/tasks")
public void create(@RequestBody CreateTaskBody body) {
// body is built by Jackson from JSON (request body conversion).
LocalDate due = body.dueDate;
}
In practice, this means: if a date "does not parse," first ask yourself, "Did it come from the query or from JSON?" This immediately separates ConversionService errors from HttpMessageConverter/Jackson errors.
3. Typical body conversion failures
Body conversion most often breaks not because Spring "does not understand JSON," but because the input/output contract did not match expectations: wrong media type, broken JSON, or JSON that does not match the Java type. These are three different problems, but beginners often see only one thing: 400 Bad Request and panic. To avoid panicking, you need to learn how to recognize them by symptoms.
Below are three typical scenarios you will run into constantly. Let's agree right away: we are not building an ideal error model right now; we are simply learning to understand at which stage the request "broke." Even if Spring returns a not-very-friendly message, it usually still hints at the source of the problem.
Wrong Content-Type: "I send JSON, but the server does not believe me"
This scenario looks like this: you are really sending JSON text, but in the headers you set Content-Type to something other than application/json. For a human: "well, it is JSON anyway"; for a server, it is like coming to a bank and saying "I definitely have money, it is just in a chip bag." Maybe money, but the process is already suspicious.
A typical bad request example (an illustrative .http sample):
# Error: the body looks like JSON, but Content-Type says "this is plain text".
POST http://localhost:8080/api/v1/tasks
Content-Type: text/plain
{"title":"Fix API"}
If your endpoint expects JSON (@RequestBody + consumes = application/json or the default JSON stack), Spring will try to choose an HttpMessageConverter for text/plain and your Java type. Usually it will not find a suitable one, and you will get something like 415 Unsupported Media Type (or a similar story).
What should you check first? The Content-Type header in the request and, if you use it, consumes in the mapping annotation. If they do not agree, the request body will not be read.
Malformed JSON: "the text is there, but it is not JSON"
The second scenario is simpler, but very common: JSON is syntactically broken. The classic bug is an extra comma, an unclosed quote, or a missing brace. And yes, this bug can survive for 20 minutes because you are looking at Java code while the problem is one comma.
Example of broken JSON (an extra comma at the end):
# Error: JSON is syntactically invalid (extra comma at the end).
POST http://localhost:8080/api/v1/tasks
Content-Type: application/json
{"title":"Fix API",}
At this point Spring already understands that the format is JSON, chooses the JSON converter, passes the text to Jackson... and Jackson honestly says: "Sorry, I am not going to read this." The important part is that this happens before the controller method is entered. So you will not see a breakpoint or System.out.println.
This is the case where beginners often say, "My validation broke" or "the service crashed." No: the service did not even start. First, a Java object must appear, and it will not appear until the JSON is syntactically valid.
JSON does not match the Java type: "the JSON is valid, but not the right one"
The third scenario is the trickiest one, because the JSON looks nice, the format is correct, but the structure/types of the values do not match what your @RequestBody parameter expects. For example, you expect a number, but a string is sent. Or you expect an object, but an array is sent. Or the field name is different.
Suppose your request body looks roughly like this:
class CreateTaskBody {
// We expect a string — that is fine.
public String title;
// We expect a number. If a string ("a lot") arrives, Jackson will not be able to build the object.
public int estimateHours;
}
And the client sends:
POST http://localhost:8080/api/v1/tasks
Content-Type: application/json
{"title":"Fix API","estimateHours":"a lot"}
The JSON is syntactically correct, but "a lot" cannot honestly be turned into an int. This is again a body conversion stage error: Jackson cannot build the object. And again, the controller method may not run because the parameter could not be created.
At the diagnostic level, the key is not to argue with reality: if the types do not match, they do not match. Do not "fix Spring"; align the contract (either the Java type or the JSON) to one shared understanding.
4. Problems on the response side
Beginners often think: "If the method ran, the response will definitely go out." Not so fast. After return, Spring MVC still has to write the response body, and that is again the responsibility of HttpMessageConverter. So there are situations where you can see that the code executed (even the service worked), but the client still gets an error because writing the response failed.
The simplest example is a mismatch in the expected response format. For example, the method returns an object, but you declared (or the client requested via Accept) a format for which there is no converter.
For example, let's take the same simple TaskResponseBody with fields id and title.
A bad (and deliberately "broken") mapping example:
import org.springframework.http.MediaType;
import org.springframework.web.bind.annotation.GetMapping;
@GetMapping(path = "/api/v1/tasks/ping", produces = MediaType.TEXT_PLAIN_VALUE)
public TaskResponseBody ping() {
// Contract error: produces=text/plain, but we return an object.
// Spring can return text/plain only if the result is a string/bytes.
TaskResponseBody response = new TaskResponseBody();
response.id = "t-1";
response.title = "Fix API";
return response;
}
Here you require text/plain, but return an object. Spring knows how to write a string as text/plain, but not an object — it needs a JSON converter and application/json. In real life you would not do this on purpose, but as a diagnostic experiment it is useful: you immediately see that "the method ran, but the response was not written."
A more realistic variant is when the client sent Accept: application/xml, but your service does not support XML. Even if the method could return an object, Spring will not be able to choose a converter for the requested format. From a diagnostic point of view, the key idea is the same: there is conversion on the output side too.
5. Symptom-based diagnostic scheme
So that this lecture does not remain "smart theory," let's fix one practical scheme. You can keep it in your head, without paper and without memorizing all exception classes. The scheme is built on two questions we already discussed, plus the clarification "input or output."
First you determine the phase: the problem is before the method, inside the method, or after the method. Usually you can see this by whether the breakpoint/marker triggered and what the server returned (and at what point). Then you determine the data source: URL or body. And only then — the specific type of failure.
Below is a "symptom map" that is handy to reread when everything is broken and you want to blame the universe:
| Symptom (what it looks like) | Where it happened | What to check first |
|---|---|---|
| Controller method was not called, 404/405 | mapping | path, HTTP method, base /api/v1, annotations @GetMapping/@PostMapping |
| Controller method was not called, error about "cannot convert parameter" | URL conversion | @RequestParam/@PathVariable, date/number/enum format in the URL |
| Controller method was not called, error about reading JSON / parse error | request body conversion | Content-Type, JSON syntax, structure matching the Java type |
| The method ran, the service did its job, but the response did not go out | response body conversion | produces, Accept, return type, serialization capability |
| The method ran, but your code failed inside | your logic | now we debug the service/mapping/checks, not MVC mechanics |
Notice: this scheme does not include "urgently write your own HttpMessageConverter." And that is not a joke. At Junior level, almost all problems are solved by a correct Content-Type, valid JSON, and matching Java types and contract. Customization is useful, but later and when needed, not as the first reaction to pain.
6. Typical body conversion mistakes
Mistake #1: fixing the service and business logic when the request never reached the controller method.
This is a classic: you open TaskService, change "suspicious" spots, add checks, and the request still fails. The reason is usually simple: the problem is before entering the method (broken JSON, wrong Content-Type, query parameter does not parse). First make sure the method is actually executed.
Mistake #2: mixing up conversion from URL and conversion from body.
When LocalDate does not parse, beginners often automatically think "Jackson." But if the date came from the query string, Jackson was not involved at all. And vice versa: if the date is inside JSON, then ConversionService is not to blame. The first thing you need to do with any conversion error is determine the data source.
Mistake #3: thinking that "I have JSON in the body" matters more than the Content-Type header.
From the HTTP point of view, the contract is not only the body, but also the headers. If you sent JSON and forgot Content-Type: application/json, the server has every right not to guess your intention. Build the habit of checking Content-Type as automatically as you check the request path.
Mistake #4: thinking that the response body "writes itself" if the method returned an object.
After return, a converter still has to be found to write the response, and it depends on produces, Accept, and the return type. So situations where "the method ran, but the client got an error" are entirely real. It is especially useful to remember this when you are sure that "everything succeeded."
Mistake #5: trying to dive into Spring MVC internals before learning how to localize the phase of the problem.
It is very tempting to start Googling "how to replace the converter," "how to configure ObjectMapper," "how to make a custom binder." But most often that is premature complexity. Until you can clearly say "this failed on body conversion" or "this failed on query conversion," any fine-tuning turns into a magical ritual. And we are trying to drive magic out of the project here — politely, but firmly.
GO TO FULL VERSION