How does Spring select which HttpMessageConverter to use for a given request or response? Explain canRead/canWrite, ordering, and content negotiation.
answer
- first-match-wins, ordered list
- read: Content-Type -> 415 if none
- write: Accept -> producible ∩ acceptable -> 406 if none
- producible = union of getSupportedMediaTypes where canWrite
- String converter */* pre-empts Jackson
basics
~20 sSpring iterates the ordered converter list and picks the first whose canRead/canWrite returns true for both the Java type and the media type. For writing, content negotiation (the Accept header) determines the target media type; for reading it's the request's Content-Type.
solid answer
~40 sSelection is order-sensitive and two-dimensional (Java type + media type). For reading, Spring takes the request's Content-Type and walks the registered converter list in order, calling canRead(targetType, contentType); the first true wins and read() runs. If none match, it throws HttpMediaTypeNotSupportedException (415). For writing, content negotiation first resolves the client's acceptable media types (from the Accept header, or a produces= constraint, or extension/param strategies). Spring computes the producible media types as the union of getSupportedMediaTypes of converters that canWrite the return type, intersects with the requested list, sorts by specificity/quality, then for each acceptable type finds the first converter whose canWrite matches and calls write(). If nothing matches, it throws HttpMediaTypeNotAcceptableException (406). Because it's first-match-wins, converter ORDER decides ties — e.g., StringHttpMessageConverter (supports */*) can pre-empt Jackson for a String return.
code
java · 21 lines// Selection depends on Accept header + return type + converter order.
@RestController
class NegotiationDemo {
// Accept: application/json -> MappingJackson2HttpMessageConverter
// Accept: application/xml -> XML converter IF jackson-dataformat-xml present,
// else 406 Not Acceptable
@GetMapping("/report")
public Report report() { return Report.sample(); }
// Returns String: StringHttpMessageConverter supports */*, so with a
// lenient Accept this yields text/plain "hi" (no JSON quotes),
// whereas Jackson would have written "\"hi\"".
@GetMapping("/hello")
public String hello() { return "hi"; }
// Force JSON regardless of the raw String by constraining producible type.
@GetMapping(value = "/hello-json", produces = MediaType.APPLICATION_JSON_VALUE)
public String helloJson() { return "\"hi\""; }
}go deeper
Know Spring tries converters until one matches the type and media type.
Explain read uses Content-Type and write uses the Accept header, and name 415 vs 406.
Detail the producible-vs-acceptable intersection, q-value sorting, and how first-match-wins makes ordering decisive.
Reason about content-negotiation strategies (ContentNegotiationManager), the canWrite(type, null) producible-discovery pass, and how reordering converters changes API contracts.
## The two-dimensional match Every selection asks two questions of each converter, in list order: 1. Does it support the **Java type**? (the `@RequestBody` param type, or the return type) 2. Does it support the **media type**? (`canRead` uses the request `Content-Type`; `canWrite` uses a negotiated response media type) Both must be true. **First match wins** — the list is ordered and iteration stops at the first converter that says yes. ## Reading (request → object) Handled by `RequestResponseBodyMethodProcessor` / `AbstractMessageConverterMethodArgumentResolver`: 1. Determine the request `Content-Type` (defaults to `application/octet-stream` if absent). 2. For each converter in order, call `canRead(paramType, contentType)`. 3. First `true` → call `read(...)`, done. 4. None → `HttpMediaTypeNotSupportedException` → **HTTP 415 Unsupported Media Type**. 5. If the body itself is malformed for the chosen converter (e.g. bad JSON) → `HttpMessageNotReadableException` → **HTTP 400**. ## Writing (object → response) and content negotiation Handled by `AbstractMessageConverterMethodProcessor.writeWithMessageConverters`: 1. **Content negotiation** resolves the list of requested media types via `ContentNegotiationManager`. Default strategy = the `Accept` header (`HeaderContentNegotiationStrategy`); other strategies include path extension (deprecated-ish) and a query parameter (`format=json`). A method-level `produces` in `@RequestMapping`/`@GetMapping` further constrains this. 2. Compute **producible** media types: iterate converters, and for those whose `canWrite(returnType, null)` is true, collect `getSupportedMediaTypes()`. 3. **Intersect** requested vs producible, and sort by specificity and q-value (quality weight). 4. For each acceptable media type in that sorted order, walk the converter list again and use the first converter whose `canWrite(returnType, mediaType)` is true; call `write()`. 5. None acceptable → `HttpMediaTypeNotAcceptableException` → **HTTP 406 Not Acceptable**. 6. Serialization failure → `HttpMessageNotWritableException` → **HTTP 500**. ## Why ordering matters — the classic trap Because both `StringHttpMessageConverter` and `MappingJackson2HttpMessageConverter` can be candidates, order decides ties. `StringHttpMessageConverter` supports `*/*`, so a controller returning a raw `String` with a lenient/absent `Accept` will get `text/plain`, not JSON — even in a JSON API. If you reorder converters (e.g., put Jackson first) or forget that Jackson `canWrite` a `String` and would serialize it as a quoted JSON string, behavior changes. This is why you should be deliberate when reordering: the same object can be handled differently depending on which converter appears first for the negotiated media type. ## `canWrite(type, null)` nuance During the producible-types computation, Spring calls `canWrite` with a `null` media type to ask 'can you write this type at all?'. Converters implement this leniently. That's how the set of producible types is discovered before a specific media type is chosen. ## Edge cases - **No Accept header** → treated as `*/*`, so the first converter that can write the type wins → ordering dominates. - **produces mismatch** → if the method declares `produces = "application/json"` but the client sends `Accept: application/xml`, you get 406 before any converter writes. - **@JsonView / Jackson config** → applied inside `MappingJackson2HttpMessageConverter` via the configured `ObjectMapper`, not a separate selection step.
- What HTTP status results when no converter canRead the request body's Content-Type, versus none canWrite for the Accept header?No readable converter -> HttpMediaTypeNotSupportedException -> 415 Unsupported Media Type. No writable/acceptable converter -> HttpMediaTypeNotAcceptableException -> 406 Not Acceptable. A malformed-but-supported body -> HttpMessageNotReadableException -> 400.
- How does a method-level produces attribute interact with converter selection?produces narrows the producible media types before selection. If the client's Accept can't intersect with produces, Spring returns 406 without invoking any converter; otherwise the negotiated intersection drives which converter's canWrite is tried.
saying these in an interview costs you the question
- Saying the converter is chosen purely by return type, ignoring media-type negotiation.
- Claiming order doesn't matter (first-match-wins means it does).
- Confusing 415 (unreadable request media type) with 406 (unacceptable response media type).