One controller method returns "Hello", and the browser shows those words instead of rendering a page. Another method sends the same greeting as JSON. The difference is not the greeting itself: it is @ResponseBody and the return type.
A returned String becomes the response body
The diagram follows two return values through @ResponseBody. A string takes the text response path; an object takes a JSON conversion path when a suitable converter is configured.
Without @ResponseBody, Spring MVC may interpret a controller's returned String as a view name. With it, Spring writes the return value to the HTTP body through a message converter. Compare two methods returning the same greeting in different types:
@Controller
class ApiController {
@GetMapping("/api/greeting/text")
@ResponseBody
String text() {
return "Hello";
}
@GetMapping("/api/greeting/json")
@ResponseBody
Greeting json() {
return new Greeting("Hello");
}
}
record Greeting(String message) {}The /api/greeting/text body is Hello. If the method was meant to select an HTML view, @ResponseBody is the wrong annotation. For a controller that serves only API responses, @RestController applies the same response-body behavior at the class level. The Spring MVC @ResponseBody reference describes the converter path and @RestController relationship.
Message converters handle object responses
For an object, Spring considers the client's Accept header and the available HTTP message converters. In a typical Spring Boot web app with JSON support, a client that accepts JSON receives {"message":"Hello"} from the second endpoint.
| Request | Return type | Response body | Typical Content-Type |
|---|---|---|---|
/api/greeting/text | String | Hello | text/plain |
/api/greeting/json | Greeting | {"message":"Hello"} | application/json |
Both responses may look like text in a browser window. Inspect both the body and Content-Type. If you expected JSON but receive a 406 or conversion error, check the request's Accept header, return type, and configured JSON converter separately.
Automatic serialization does not make every object safe to expose. Its fields become part of your API contract. Returning a persistence entity or internal exception directly can leak fields or lock internal structure into public responses.
Treat the response type as an external contract
A response-specific type such as Greeting makes the exposed fields visible in code. When adding a field, decide deliberately whether clients should receive it. For a missing resource, specify a status such as 404 rather than returning null as an ambiguous success response. Avoid exposing internal paths or full exception details in error bodies.
Test Content-Type and body together
When message-converter configuration changes, assertions about both fields reveal the contract change:
mockMvc.perform(get("/api/greeting/text"))
.andExpect(status().isOk())
.andExpect(content().contentTypeCompatibleWith("text/plain"))
.andExpect(content().string("Hello"));
mockMvc.perform(get("/api/greeting/json").accept("application/json"))
.andExpect(status().isOk())
.andExpect(content().contentTypeCompatibleWith("application/json"))
.andExpect(jsonPath("$.message").value("Hello"));The first response is text; the second is an object represented as JSON. A test that checks only a successful status would miss that difference.
Key takeaways
@ResponseBody writes a return value to the HTTP body instead of selecting a view. A String usually becomes text, while a response object usually becomes JSON when the appropriate converter is available. Make the external response type explicit and test status, Content-Type, and body together.

