In REST Assured, an endpoint returns JSON as text/plain and as(ScaffoldInspection.class) fails — how do you fix it?
answer
- the response label picks the reader
- text/plain is neither json nor xml
- one content type versus every content type
- naming a mapper skips the question
- reset() clears the default parser
basics
~20 sREST Assured picks a deserializer from the response content type, and text/plain matches neither JSON nor XML, so as() throws IllegalStateException. Register the type with RestAssured.registerParser, set RestAssured.defaultParser, or name a mapper with as(Class, ObjectMapperType) to bypass the decision.
solid answer
~40 sREST Assured chooses a response deserializer from the response's own content type: `as(ScaffoldInspection.class)` looks for `json` or `xml` in it, and a body labelled `text/plain` matches neither, so the call throws `IllegalStateException: Cannot parse object because no supported Content-Type was specified in response`. There are three levers. `RestAssured.registerParser("text/plain", Parser.JSON)` maps that one content-type string onto the JSON parser and is the narrowest fix. `RestAssured.defaultParser = Parser.JSON` is a blanket fallback used for any content type the registry does not otherwise recognise, including a response carrying no content type at all. `extract().as(ScaffoldInspection.class, ObjectMapperType.JACKSON_3)` skips the content-type decision entirely and is per-call. Both statics are global — reset them in teardown with `RestAssured.reset()`, or use the `ResponseSpecBuilder` forms `registerParser(...)` and `setDefaultParser(...)`. Which you reach for is a blast-radius question, not a correctness one.
code
java · 19 linesimport io.restassured.parsing.Parser;
import static io.restassured.RestAssured.given;
import static io.restassured.RestAssured.registerParser;
// legacy endpoint answers JSON but labels it text/plain
registerParser("text/plain", Parser.JSON);
ScaffoldInspection inspection = given()
.pathParam("ref", "INSP-2024-0917")
.when()
.get("/inspections/{ref}")
.then()
.statusCode(200)
.extract()
.as(ScaffoldInspection.class);
// per-call alternative that consults no header at all:
// .extract().as(ScaffoldInspection.class, ObjectMapperType.JACKSON_3)go deeper
Remember that deserialization follows the response's Content-Type, not the Accept header you sent. When as() complains about an unsupported Content-Type, the body is usually fine and the label is the problem.
Explain the selection order: an explicitly named mapper wins, otherwise the resolved content type is tested for json or xml, otherwise the default parser supplies a fallback, otherwise IllegalStateException.
Show you would pick the narrowest lever that fixes the case, keep the static registry clean across a parallel suite, and raise the mislabelled Content-Type as a service defect rather than silently absorbing it in a base class.
Decide where workarounds for a misbehaving service live: a shared response specification, a single documented registration, or a rejected pull request that sends the fix upstream. Weigh how many such accommodations a suite can carry before it stops describing the contract.
A legacy endpoint on a scaffolding-inspection API answers `GET /inspections/INSP-2024-0917` with a perfectly good JSON body and the header `Content-Type: text/plain`. `extract().as(ScaffoldInspection.class)` blows up. Nothing is wrong with the body, the DTO or the classpath — the response label is what fails. ## How REST Assured picks a response reader Deserialization is driven by the **response's** content type, the mirror image of serialization, which is driven by the request's. When `as(Class)` runs, REST Assured resolves a content type and then makes a substring decision on it: - If the resolved content type contains `json`, it walks the JSON mappers on the classpath and uses the first one it finds. - If it contains `xml`, it does the same for the XML mappers. - Otherwise it falls back to the default parser's content type, if one was set. - If that too comes up empty, it throws `IllegalStateException: Cannot parse object because no supported Content-Type was specified in response. Content-Type was 'text/plain'.` The substring test explains a lot of otherwise puzzling behaviour. A vendor type such as `application/vnd.scaffold.inspection+json` needs no configuration at all, because it contains `json`. `text/plain` does not, and neither does `application/octet-stream` — and `Parser.TEXT` exists but has no object mapper behind it, so recognising a body as text is not the same as being able to map it. ## The three levers, and their blast radius | Lever | Scope | Effect | |---|---|---| | `RestAssured.registerParser("text/plain", Parser.JSON)` | that one content-type string | the registrar rewrites the chosen type to `application/json`, so a JSON mapper is picked | | `RestAssured.defaultParser = Parser.JSON` | every response the registry does not otherwise recognise | supplies a fallback content type, including for responses with no `Content-Type` at all | | `as(ScaffoldInspection.class, ObjectMapperType.JACKSON_3)` | this one call | no content type is passed to the mapping step, so the header is never consulted | A fourth exists for the awkward cases: `as(ScaffoldInspection.class, myObjectMapper)` takes an `ObjectMapper` instance and calls it directly, bypassing the registry and the selection logic together. Ranked by preference in a real suite: 1. `registerParser` when exactly one endpoint or one vendor type misbehaves — it says what is wrong in one line and leaves every other response alone. 2. The two-argument `as(...)` when the fix should not outlive the test that needs it. 3. `defaultParser` when the service is consistently sloppy about labelling and you have decided the whole suite should treat unknown types as JSON. ## Why the default parser is the blunt instrument `RestAssured.defaultParser` is often described as "for responses with no content type", and that is its headline use. But the registrar returns the default parser for *any* content type that has no registered entry, so setting it globally quietly colours far more than the one endpoint you were fixing. If a second endpoint later starts returning XML, a suite-wide `Parser.JSON` default is now in a position to make a confusing failure instead of an obvious one. Prefer the narrow lever until you have a reason not to. ## Test hygiene around the statics Both `registerParser` and `defaultParser` are static state on `RestAssured`, which means they survive the test that set them and leak into every test that runs afterwards in the same JVM. That matters in a parallel or long-running suite: - Reset in teardown: `RestAssured.reset()` clears the default parser along with `baseURI`, `port`, the request and response specifications, the filter list and the config. - Or `RestAssured.unregisterParser("text/plain")` to remove one registration without flattening everything else. - Or avoid the statics: `ResponseSpecBuilder` exposes `registerParser(String, Parser)` and `setDefaultParser(Parser)`, so the rule travels with a response specification you attach where you want it. - The validate side has the same two spellings as `then().parser(String, Parser)` and `then().defaultParser(Parser)`. ## Diagnosing it when you are handed the failure The exception text names the offending content type verbatim, so the first move is to read it rather than the DTO. From there: - Print the response headers — `then().log().headers()` — and confirm what the server actually sent, including any charset suffix. - Check whether the body assertions in the same test were also failing. If `body("verdict", equalTo("PASS"))` was fine but `as(...)` was not, you are looking at a mapping-side problem, not a parsing-side one. - Decide whether the real fix belongs in the test suite at all. A service labelling JSON as `text/plain` is a defect in the service; the parser registration is a workaround you are choosing to carry, and it is worth saying so in the pull request rather than burying it in a base class. That last judgment is what separates a senior answer from a correct one. All three levers work. Choosing the narrowest one, keeping the global state clean, and naming the underlying service bug is the part that does not come from the documentation.
- Does a vendor content type like application/vnd.scaffold.inspection+json need registerParser before as(...) works?No. The mapping decision is a substring test for `json` or `xml` on the resolved content type, and a `+json` vendor type contains `json`, so a JSON mapper is chosen with no configuration. `registerParser` is for types that carry neither substring — `text/plain`, `application/octet-stream`, or a bare vendor type with no `+json` suffix.
- What is the difference between RestAssured.defaultParser and RestAssured.registerParser?`registerParser(contentType, parser)` is keyed to one exact content-type string, charset stripped. `defaultParser` is a fallback the registrar returns for any content type that has no registered entry, and it also covers responses with no `Content-Type` header at all. The first is surgical; the second changes how the whole suite treats unrecognised labels.
- How do you keep a parser registration from leaking into other tests?Both levers are static state on `RestAssured`. Call `RestAssured.reset()` in teardown to clear the default parser along with the other statics, or `unregisterParser(contentType)` to drop just one registration. Better still, put the rule on a `ResponseSpecBuilder` via `registerParser(...)` or `setDefaultParser(...)` so it travels only with the tests that attach that specification.
saying these in an interview costs you the question
- Thinking given().accept(ContentType.JSON) changes how the response is deserialized
- Believing text/plain works because Parser.TEXT exists
- Assuming REST Assured sniffs the body and detects JSON on its own
- Setting RestAssured.defaultParser globally and never resetting it
- Claiming a mislabelled body needs a custom Filter to rewrite the header
- Treating defaultParser and registerParser as interchangeable