skip to content

Object Mapping

Two directions of one piece of machinery: a Java object written into the request payload, and a response read back into a Java type. Asked because the library that does it is chosen implicitly.

part ofREST Assuredoverview, primer and where to startread it →
on this pageshow

explore

questions

7

In REST Assured, how do you deserialize a JSON array response into a List<ScaffoldInspection>?

level: middleimportance: must knowfreq 58%

answer

  1. a Class cannot carry an element type
  2. anonymous subclass, not a bare constructor
  3. io.restassured.common.mapper, not io.restassured.mapper
  4. raw maps become ClassCastException later
  5. no as(TypeRef, ObjectMapperType) overload

basics

~20 s

REST Assured reads a JSON array into a typed list with as(new TypeRef<List<ScaffoldInspection>>() {}); the anonymous subclass carries the element type past erasure. as(List.class) cannot, and hands back raw maps that fail on first use. Import it from io.restassured.common.mapper.

solid answer

~50 s

A JSON array of inspections cannot be read with `extract().as(List.class)`. `List.class` is a plain `Class` with nowhere to record an element type, so the mapper is told only to build a list and fills it with one `Map` per array element; the first typed access then throws `ClassCastException`. REST Assured's answer is `TypeRef`: `extract().as(new TypeRef<List<ScaffoldInspection>>() {})`. The trailing `{}` is load-bearing — `TypeRef` is abstract, and its constructor reads the actual type argument off the anonymous subclass's generic superclass; without one it throws `IllegalArgumentException("No type info in TypeRef")`. Import it from `io.restassured.common.mapper.TypeRef` in the `rest-assured-common` module — the `io.restassured.mapper` spelling that appears in older docs is stale. The same trick covers any generic container, such as `Map<String, List<Defect>>`. Note there is no `as(TypeRef, ObjectMapperType)` overload: to name a mapper as well, pass `typeRef.getType()` into `as(Type, ObjectMapperType)`.

code

java · 18 lines
java
import io.restassured.common.mapper.TypeRef;
import java.util.List;

import static io.restassured.RestAssured.given;
import static org.hamcrest.Matchers.greaterThan;

List<ScaffoldInspection> found = given()
        .queryParam("site", "SITE-4417")
        .when()
        .get("/inspections")
        .then()
        .statusCode(200)
        .body("size()", greaterThan(0))
        .extract()
        .as(new TypeRef<List<ScaffoldInspection>>() {});

// element type survived: no raw Map, no ClassCastException
String verdict = found.get(0).getVerdict();

go deeper

for a junior

Learn the shape of the call before the theory: a single object is as(ScaffoldInspection.class) and a JSON array is as(new TypeRef<List<ScaffoldInspection>>() {}). Remember the trailing braces and the io.restassured.common.mapper import.

for a middle

Be ready to explain why a Class literal cannot describe List<Foo>, how the anonymous subclass lets TypeRef recover the argument through getGenericSuperclass, and why the resulting ClassCastException lands far from the as() call.

for a senior

Show judgment about which spelling belongs in a suite: an array Class, a wrapper DTO with an items field, or TypeRef. Point out that a response that will grow a pagination envelope should get the DTO now, not TypeRef.

for a principal

Own the convention. Decide whether the suite maps to hand-written DTOs at all or asserts on paths, and weigh the maintenance cost of DTOs that must track a changing payload against the readability they buy across hundreds of tests.

REST Assured's `as(...)` family hands a response body to an object mapper and gives you a Java object back. For a single resource that is uneventful — `extract().as(ScaffoldInspection.class)`. For a collection endpoint on a scaffolding-inspection API it is not, and the obstacle is the Java type system rather than the library. ## Why `as(List.class)` cannot work `GET /inspections?site=SITE-4417` answers with a top-level JSON array: ```json [ {"ref": "INSP-2024-0917", "verdict": "PASS", "tieCount": 42}, {"ref": "INSP-2024-0918", "verdict": "ADVISORY", "tieCount": 39} ] ``` `extract().as(List.class)` compiles, runs, and then betrays you: - `List.class` is a `Class<List>` — a runtime handle with nowhere to record an element type. - The declaration `List<ScaffoldInspection> found = ... .as(List.class);` raises only an unchecked-conversion warning, never a compile error. - The mapper is told to build "a list" and nothing more, so it fills it with its own generic node type: one `Map` per array element, not one `ScaffoldInspection`. - The failure surfaces at the first typed access as a `ClassCastException`, and the stack trace points at your assertion rather than at the `as(...)` call that caused it. That last point is why this is a mid-level question rather than a trivia question: the symptom and the cause are several lines apart. ## What `TypeRef` does about it `io.restassured.common.mapper.TypeRef<T>` is an abstract class with a `protected` no-argument constructor. That constructor calls `getClass().getGenericSuperclass()` and pulls the first actual type argument out of it, then exposes it through `getType()` as a `java.lang.reflect.Type`. The resolved `Type` — not a bare `Class` — is what REST Assured passes down to the mapper. ```java List<ScaffoldInspection> found = given() .queryParam("site", "SITE-4417") .when().get("/inspections") .then().statusCode(200) .extract().as(new TypeRef<List<ScaffoldInspection>>() {}); ``` Three details are easy to get wrong: 1. The trailing `{}` is not decoration. It creates an anonymous subclass, and that subclass is the only reason `getGenericSuperclass()` is a `ParameterizedType` at runtime. 2. `TypeRef` is abstract, so a bare `new TypeRef<...>()` does not compile at all; a construction that loses the argument throws `IllegalArgumentException("No type info in TypeRef")`. 3. The package is `io.restassured.common.mapper`, in the `rest-assured-common` artifact. Documentation and blog posts that say `io.restassured.mapper.TypeRef` are stale and the import will not resolve. ## The four call forms, side by side | Call | Reader chosen by | Element type | |---|---|---| | `as(ScaffoldInspection.class)` | the response `Content-Type` | not applicable | | `as(ScaffoldInspection.class, ObjectMapperType.GSON)` | the named mapper | not applicable | | `as(new TypeRef<List<ScaffoldInspection>>() {})` | the response `Content-Type` | preserved | | `as(typeRef.getType(), ObjectMapperType.JACKSON_3)` | the named mapper | preserved | Note what the third row does *not* change: `as(TypeRef)` still resolves its reader from the response's content type, exactly like `as(Class)` does. `TypeRef` solves erasure, not reader selection. And there is no `as(TypeRef, ObjectMapperType)` overload — if you need a generic container *and* a named mapper, pass `new TypeRef<List<ScaffoldInspection>>() {}.getType()` into `as(Type, ObjectMapperType)`. ## Failure signatures worth recognising - `ClassCastException` naming a `Map` implementation where you expected your own type — you used `as(List.class)` or an equivalent raw call. - `IllegalArgumentException: No type info in TypeRef` — the anonymous subclass braces went missing, or the reference was built through a helper that erased the argument. - A compile error on `import io.restassured.mapper.TypeRef` — wrong package; use `io.restassured.common.mapper`. - A mapper-side error about an unrecognised field — the element type resolved correctly and the problem has moved on to the DTO, which is progress. ## When not to reach for TypeRef `TypeRef` earns its keep on genuinely generic shapes, but it is not always the shortest correct answer: - `as(ScaffoldInspection[].class)` also works, because an array `Class` carries its component type. It is often the tidier spelling when you only intend to iterate. - `extract().jsonPath().getList("ref", String.class)` is the right tool when you want one field across the array rather than whole objects. - A wrapper DTO — an `InspectionPage` type with a `List<ScaffoldInspection> items` field — removes the problem entirely, and is what you want the moment the endpoint grows a pagination envelope around the array. - Reserve `TypeRef` for shapes an array cannot express: `Map<String, List<Defect>>`, `List<Map<String, Object>>`, and nested containers generally. The judgment an interviewer is listening for is that this is a resolved-`Type` problem, not a REST Assured quirk: the library needs a `java.lang.reflect.Type` to hand the mapper, a `Class` literal can only supply one for non-generic and array shapes, and `TypeRef` is the standard trick for manufacturing one for everything else.

  • Why do the trailing braces in new TypeRef<List<ScaffoldInspection>>() {} matter?
    They create an anonymous subclass. `TypeRef` is abstract and its constructor reads the actual type argument from `getClass().getGenericSuperclass()`, which is a `ParameterizedType` only because of that subclass. Without a type argument the constructor throws `IllegalArgumentException("No type info in TypeRef")`, and without the braces the code does not compile at all.
  • How would you deserialize into a generic container and still name the object mapper explicitly?
    There is no `as(TypeRef, ObjectMapperType)` overload, so you cannot do it in one literal call. Resolve the type yourself and use the `Type` overload: `as(new TypeRef<List<ScaffoldInspection>>() {}.getType(), ObjectMapperType.JACKSON_3)`. `TypeRef.getType()` returns the resolved `java.lang.reflect.Type` that REST Assured would have passed down anyway.
  • Does as(TypeRef) change how REST Assured picks which object mapper reads the body?
    No. `as(TypeRef)` delegates straight to the `as(Type)` path, so the reader is still chosen from the response's content type exactly as it is for `as(Class)`. `TypeRef` solves type erasure only. If the content type is unusable you still need `registerParser`, `defaultParser`, or the two-argument form that names a mapper.

A Class literal is a shipping label that says only "crate". A TypeRef is the packing list taped to the outside: same crate, but now the receiver knows what shape everything inside should be.

saying these in an interview costs you the question

  • Claiming as(List.class) returns a typed list because generics survive at runtime
  • Importing TypeRef from io.restassured.mapper instead of io.restassured.common.mapper
  • Writing new TypeRef<List<Foo>>() without the anonymous-subclass braces
  • Believing TypeRef also changes which object mapper reads the body
  • Passing a Jackson TypeReference straight into as(...) as if it were a TypeRef
  • Assuming a top-level JSON array needs a custom deserializer in REST Assured
open as a page

In REST Assured, an endpoint returns JSON as text/plain and as(ScaffoldInspection.class) fails — how do you fix it?

level: seniorimportance: must knowfreq 46%

basics

~20 s

REST 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.

open as a page

In REST Assured, which library serializes the object you hand to given().body(diveEntry)?

level: juniorimportance: should knowfreq 58%

basics

~20 s

REST Assured's body(Object) picks its writer from the request Content-Type, not the object's class. A JSON type takes the first of Jackson 3, Jackson 2, Jackson 1, Gson, Johnzon, Yasson on the classpath. An XML type takes Jakarta EE, then JAXB.

open as a page

In REST Assured, what does as(ScaffoldInspection.class, ObjectMapperType.GSON) do that as(ScaffoldInspection.class) does not?

level: juniorimportance: nice to knowfreq 34%

basics

~20 s

REST Assured's two-argument as() names the reader outright, so the response Content-Type is never consulted; the one-argument form derives the mapper from that header instead. A named mapper missing from the classpath throws IllegalArgumentException rather than falling back.

open as a page

In REST Assured, why must given().contentType(...) come before body(diveEntry) in the chain?

level: middleimportance: nice to knowfreq 42%

basics

~20 s

REST Assured's body(Object) serializes immediately, reading the Content-Type already on the specification. Call it first and that value is still null, so the no-content-type scan runs. A later contentType(...) then only rewrites the header, because the bytes are already fixed.

open as a page

In REST Assured, why does body(entry) throw for content type application/vnd.divelog.v3, and what fixes it?

level: seniorimportance: nice to knowfreq 27%

basics

~20 s

REST Assured chooses a writer by looking for the substring json or xml inside the request content type. application/vnd.divelog.v3 contains neither, so body(Object) throws IllegalArgumentException before anything is sent. EncoderConfig.encodeContentTypeAs maps that type to ContentType.JSON, and the JSON scan then runs.

open as a page

Your REST Assured suite sets ObjectMapperConfig.defaultObjectMapper and body(dive, ObjectMapperType.GSON) is ignored — why?

level: seniorimportance: nice to knowfreq 31%

basics

~20 s

REST Assured checks ObjectMapperConfig.defaultObjectMapper first, before any type you name at the call site, so it wins over body(dive, ObjectMapperType.GSON). Only the body(Object, ObjectMapper) overload beats it, because that overload never consults the object-mapper configuration. Nothing warns you.

open as a page