Your REST Assured suite sets ObjectMapperConfig.defaultObjectMapper and body(dive, ObjectMapperType.GSON) is ignored — why?
answer
- config is consulted before the call site
- instance mapper beats named type
- defaultObjectMapperType is only a fallback
- absent mapper throws, never falls back
basics
~20 sREST 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.
solid answer
~40 sREST Assured's serialization entry point tests things in a fixed order. First `ObjectMapperConfig.hasDefaultObjectMapper()`: if you installed an `ObjectMapper` instance through `defaultObjectMapper(...)`, that object serializes everything and the routine returns straight away. Only afterwards does it look at a type — the one passed to `body(pojo, ObjectMapperType.X)`, or `defaultObjectMapperType(...)` when the call site named none. Last comes the content-type-driven classpath scan. So `defaultObjectMapper` outranks `ObjectMapperType.GSON`, while `defaultObjectMapperType` does not: a call-site type beats *that* one. The single escape hatch is the `body(pojo, ObjectMapper)` overload, which serializes through the instance you pass and never enters the config-aware path. Two further edges: naming a mapper absent from the classpath throws `IllegalArgumentException` rather than falling back, and `defaultObjectMapper(...)` rebuilds the whole `ObjectMapperConfig`, discarding factory overrides set on it earlier.
code
java · 16 linesimport static io.restassured.config.ObjectMapperConfig.objectMapperConfig;
import io.restassured.config.RestAssuredConfig;
import io.restassured.mapper.ObjectMapperType;
// AuditingDiveMapper implements io.restassured.mapper.ObjectMapper
RestAssured.config = RestAssuredConfig.config()
.objectMapperConfig(objectMapperConfig()
.defaultObjectMapper(new AuditingDiveMapper()));
// GSON is ignored: defaultObjectMapper is checked before any named type
given().contentType(JSON).body(dive, ObjectMapperType.GSON)
.post("/clubs/kraken-divers/dives");
// this overload never reaches ObjectMapperConfig at all
given().contentType(JSON).body(dive, new GsonDiveMapper())
.post("/clubs/kraken-divers/dives");go deeper
You are unlikely to be asked this, but know that REST Assured has a configuration layer above the call site, so a mapper set once in test setup can affect every request in the suite.
Be able to walk the ladder in order — default mapper instance, then a named type, then the content-type scan — and to say which of the two ObjectMapperConfig settings the call site can override.
Demonstrate the diagnosis: a payload that is subtly wrong with no mapper named anywhere in the failing test means you look at shared configuration before you look at the classpath.
Take a position on whether suites should pin a serializer centrally at all, given that a central default silently overrides deliberate per-test choices and no tooling surfaces the conflict.
## The four-step decision REST Assured's serialization entry point runs one ordered sequence of checks and returns from the first one that matches. Written out, the ladder is: 1. Is an `ObjectMapper` instance installed as `ObjectMapperConfig.defaultObjectMapper(...)`? If so, that object serializes the payload and the routine returns immediately. 2. Otherwise, was a type named — either at the call site through `body(pojo, ObjectMapperType.X)`, or as `ObjectMapperConfig.defaultObjectMapperType(...)`? If so, that mapper is used, with the call-site value winning over the configured one. 3. Otherwise, fall back to the content-type-driven classpath scan. 4. If nothing matches, throw. Step 1 is the answer to the question. A named `ObjectMapperType` never gets a chance, because the `defaultObjectMapper` check sits above it and returns. There is no warning, no log line and no exception — your test compiles, passes, and quietly serializes through the wrong writer. ## `defaultObjectMapper` versus `defaultObjectMapperType` The two settings on `ObjectMapperConfig` look symmetrical and are not. One outranks the call site and one is outranked by it: | setting | takes | position in the ladder | beaten by a call-site type? | |---|---|---|---| | `defaultObjectMapper(ObjectMapper)` | your own instance | checked first, above everything | no | | `defaultObjectMapperType(ObjectMapperType)` | one of the eight constants | checked second, as a fallback | yes | So `defaultObjectMapperType(ObjectMapperType.GSON)` is a genuine default that any `body(pojo, ObjectMapperType.JACKSON_2)` overrides, while `defaultObjectMapper(myMapper)` is closer to a hard override. Conflating the two is the most common way to reason about this wrongly. ## The one overload that skips the configuration There is an escape hatch, and it is a different overload rather than a different setting: - `body(pojo, ObjectMapper)` builds a serialization context by hand and calls `serialize(...)` on the instance you passed. It never enters the config-aware routine, so it is unaffected by `defaultObjectMapper`. - `body(pojo, ObjectMapperType)` does enter that routine, which is precisely why it loses. `ObjectMapper` is a two-method interface — `serialize(ObjectMapperSerializationContext)` and `deserialize(ObjectMapperDeserializationContext)`, both returning `Object` — so it cannot be written as a lambda; you need a class. From the serialization context you can read the object, the content type and the charset REST Assured resolved for the request. ## Naming an absent mapper throws The type-driven step is not tolerant. Each constant is paired with a classpath probe, and a mismatch falls through to a throw rather than to the next candidate: - `body(dive, ObjectMapperType.JOHNZON)` with no Johnzon present raises `IllegalArgumentException` saying that johnzon does not exist in the classpath. It does not fall back to Jackson. - `ObjectMapperType.JAXB` probes `javax.xml.bind.Binder` and `ObjectMapperType.JAKARTA_EE` probes `jakarta.xml.bind.Binder`, so a project that has moved to the Jakarta namespace throws on `JAXB` and succeeds on `JAKARTA_EE`. - `ObjectMapperType.JSONB` is backed specifically by Eclipse Yasson, so another JSON-B provider does not satisfy it. This is deliberate: a named mapper is an assertion about the payload format, and silently using a different library would produce a different document. ## What the ladder means for a shared suite The practical reading of the ordering is that the two `ObjectMapperConfig` settings sit at different strengths, and you should pick the one that matches your intent: - Reach for `defaultObjectMapperType(...)` when you want a house default that an individual test may still override for a good reason. - Reach for `defaultObjectMapper(...)` only when you genuinely want no test to be able to choose — for instance because every outgoing payload must pass through one instrumented writer. - Leave both unset when the content type should decide, which is the case for most suites. Because the strong form is silent, a suite that installs `defaultObjectMapper(...)` in shared setup should say so where a reader of an individual test will see it, not only in a configuration class nobody opens. ## Diagnosing it in a real suite The failure mode is a payload that is subtly wrong — a differently named field, a date rendered in another form, a null included where you expected it dropped — with no evidence pointing at the mapper. Work it in this order: 1. Log the outgoing request body and read the bytes, since mapper choice shows up as formatting and naming, never as a line naming the library. 2. Check whether anything in the suite's setup calls `defaultObjectMapper(...)`; it outranks everything a test can say at the call site. 3. Only then look at the classpath, because the scan is the last step and is reached only when neither configuration default nor a named type applies. One more sharp edge belongs on the checklist. `defaultObjectMapper(...)` does not copy the config it is called on: it constructs a fresh `ObjectMapperConfig` from the single-argument constructor, which resets `defaultObjectMapperType` to null and rebuilds every mapper factory from defaults. Any `jackson2ObjectMapperFactory(...)` customisation you chained earlier on that same config object is discarded, so order your configuration calls with that in mind.
- How would you confirm which mapper actually wrote the payload during a failing run?Log the outgoing request body and read the bytes: mapper choice shows up as field naming and formatting, never as a line naming the library. Then check the suite's setup for a `defaultObjectMapper(...)` call, since it outranks anything a test names, and only after that bisect the classpath by excluding one candidate mapper and rerunning.
- Why does ObjectMapperType.JAXB throw on a Jakarta-namespace project while JAKARTA_EE works?The two constants probe different marker classes: `JAXB` requires `javax.xml.bind.Binder` on the classpath and `JAKARTA_EE` requires `jakarta.xml.bind.Binder`. A project that has moved to the Jakarta namespace has only the latter, so naming `JAXB` falls through to the throw and raises `IllegalArgumentException` saying jaxb is not in the classpath.
saying these in an interview costs you the question
- Assumes the call site always overrides global configuration
- Treats defaultObjectMapper and defaultObjectMapperType as interchangeable
- Expects ObjectMapperType.JOHNZON to fall back to Jackson when Johnzon is missing
- Thinks body(pojo, myMapper) still consults ObjectMapperConfig
- Believes defaultObjectMapper(...) preserves factory settings already on that config