POST, PUT, and PATCH all send a body. JSON is by far the dominant format on the wire, and Rest Assured gives you four sensible ways to attach a JSON body to a request: as a raw String, as a Java Map, as a POJO, or as a File. Each has a place — the right choice depends on how often the body changes, how much typing you can stand, and whether you want compile-time refactor safety. This lesson shows all four and the trade-offs that decide between them.
A raw String body
The simplest form. Use a Java text block (Java 13+) so you don't have to escape every quote:
@Test
public void createUserWithStringBody() {
String requestBody = """
{
"name": "Alice Smith",
"email": "alice@test.com",
"role": "admin",
"active": true
}
""";
given()
.contentType(ContentType.JSON)
.body(requestBody)
.when()
.post("/users")
.then()
.statusCode(201)
.body("name", equalTo("Alice Smith"));
}What it's good for: tiny one-off bodies, fixtures pasted from Postman, and tests that exercise specifically-malformed JSON (e.g., a trailing comma) where you want the literal bytes preserved.
What it costs: zero compile-time safety. A typo in "emial" won't be caught until the API rejects it. Field-level refactors (renaming role to userRole across 50 tests) become a find-and-replace.
A Map body
Pass a Map<String, Object> and Rest Assured serialises it via Jackson:
@Test
public void createUserWithMapBody() {
Map<String, Object> body = new HashMap<>();
body.put("name", "Alice Smith");
body.put("email", "alice@test.com");
body.put("role", "admin");
body.put("active", true);
given()
.contentType(ContentType.JSON)
.body(body)
.when()
.post("/users")
.then()
.statusCode(201)
.body("name", equalTo("Alice Smith"));
}The Map sweet spot: when you have field/value pairs from configuration, parameterised data, or a faker library. No string escaping, types preserved (true stays a boolean, 42 stays an int), and you can build the Map up dynamically with if statements before posting.
For simple immutable cases, Map.of(...) is even more compact:
Map<String, Object> body = Map.of(
"name", "Alice Smith",
"email", "alice@test.com",
"role", "admin"
);(Note: Map.of caps at ten entries and rejects null values. For larger or nullable Maps, use HashMap or LinkedHashMap.)
Nested objects in a Map
Real bodies usually aren't flat. A nested address:
Map<String, Object> address = new HashMap<>();
address.put("street", "123 Test Street");
address.put("city", "London");
address.put("postcode", "SW1A 1AA");
Map<String, Object> body = new HashMap<>();
body.put("name", "Alice");
body.put("email", "alice@test.com");
body.put("address", address);
given()
.contentType(ContentType.JSON)
.body(body)
.when()
.post("/users");Jackson serialises this to:
{
"name": "Alice",
"email": "alice@test.com",
"address": {
"street": "123 Test Street",
"city": "London",
"postcode": "SW1A 1AA"
}
}Lists of nested objects work the same way — body.put("orders", List.of(order1, order2)).
A POJO body — the durable option
When the same request shape is used by more than a couple of tests, define a POJO:
package com.mycompany.apitests.models;
public class User {
private String name;
private String email;
private String role;
private boolean active;
// Constructors, getters, setters — covered in detail in Chapter 5
public User(String name, String email, String role, boolean active) {
this.name = name;
this.email = email;
this.role = role;
this.active = active;
}
// getters omitted for brevity
}@Test
public void createUserWithPojoBody() {
User user = new User("Alice Smith", "alice@test.com", "admin", true);
given()
.contentType(ContentType.JSON)
.body(user) // Jackson serialises the POJO automatically
.when()
.post("/users")
.then()
.statusCode(201)
.body("name", equalTo("Alice Smith"));
}The wins here are real: IDE autocomplete on User.setEmail(...), compile-time errors when the API contract changes, and the same User class doubles as the response model when you extract().as(User.class). The cost is the upfront class definition — but once you have it, every test gets simpler. Chapter 5 covers POJOs and Lombok in depth.
A body from a file
When the same fixture is reused across tests — for instance, a known-good large-order.json — store it as a file under src/test/resources/testdata/:
import java.io.File;
@Test
public void createUserFromJsonFile() {
File jsonFile = new File("src/test/resources/testdata/valid-user.json");
given()
.contentType(ContentType.JSON)
.body(jsonFile)
.when()
.post("/users")
.then()
.statusCode(201);
}Files are great for fixtures large enough that putting them in code would clutter the test, and for known-shape payloads shared across tests (smoke fixtures, regression baselines).
The four approaches at a glance
Four ways to send a JSON body — and when to use each
String
Java text block ("""...""")
Zero compile-time safety
Refactors are find-and-replace
Best for: one-offs, malformed-JSON tests
Map
Map<String, Object>
Jackson serialises automatically
No string escaping; types preserved
Best for: dynamic / parameterised bodies
POJO
Typed Java class with getters/setters
Compile-time refactor safety
Reusable for response deserialisation
Best for: any shape used by 3+ tests
File
src/test/resources/testdata/*.json
body(new File("..."))
Out-of-band fixture
Best for: large or shared baselines
The honest rule: start with a Map, graduate to a POJO when the same shape appears in three or more tests. Strings and files are escape hatches for specific situations.
PUT vs PATCH bodies
The body shape differs between PUT and PATCH — a distinction the API Testing Masterclass covered:
// PUT — replace the whole resource. Body should contain every field.
User fullUser = new User("Alice", "alice@test.com", "admin", true);
given()
.contentType(ContentType.JSON)
.body(fullUser)
.when()
.put("/users/42");
// PATCH — partial update. Body has only the fields to change.
Map<String, String> patch = Map.of("role", "viewer");
given()
.contentType(ContentType.JSON)
.body(patch)
.when()
.patch("/users/42");A common bug: sending a PUT with a partial body. A strict API will clear the omitted fields. PATCH-with-partial is the right shape for partial updates; PUT is for replacing entire resources.
Validating what was actually sent
When a test fails because the body wasn't what you expected, log the request:
given()
.log().all() // logs method, URL, headers, AND body
.contentType(ContentType.JSON)
.body(payload)
.when()
.post("/users");Or globally, only on failure:
RestAssured.enableLoggingOfRequestAndResponseIfValidationFails();The serialised JSON dump is exactly what went on the wire — including any field your POJO accidentally renamed via Jackson annotations, or any field you forgot to set (which serialises as null).
⚠️ Common mistakes
- Forgetting
.contentType(ContentType.JSON)with a Map or POJO body. Without it, Rest Assured doesn't know to serialise via Jackson — you may get the body as a string representation of the object (User@4f3d3e2a) or no body at all. Always set the content type when sending a body. - Sending a partial body via PUT. PUT means replace. A PUT body missing the
emailfield will, on a strict API, set the email to null. Use PATCH for partial updates. - Hand-constructing JSON strings with
+."{\"name\":\"" + name + "\"}"is fragile (escape hell, no encoding) and fails the moment a value contains a quote or newline. Use a Map or POJO; let Jackson do the work.
🎯 Practice task
Write a body four different ways and feel which fits your workflow. 30 minutes against REQRES.
- Set
RestAssured.baseURI = "https://reqres.in". REQRES echoes back what you POST, so any of the four body shapes will produce a201with a generatedidandcreatedAt. - Write
createUserWithStringBody()using a text block. Assert the response includes the name you sent. - Write
createUserWithMapBody()usingMap.of(...). Same assertion. Compare readability. - Write
createUserWithPojoBody()— define a smallUserCreateRequestPOJO undersrc/test/java/.../models/with two fields (name,job). POST aUserCreateRequestinstance. Confirm the response echoes the right name and job. - Write
createUserFromJsonFile()— drop avalid-user.jsonundersrc/test/resources/testdata/and POST it. Same assertion. - PATCH the user. PUT vs PATCH
/api/users/2with a body that changes onlyjob. PUT should overwrite; PATCH should merge. (REQRES doesn't enforce semantics — but the request shape is what you'd write against a real API.) - Log the request. Add
.log().all()to one test. Run it and read the dump. Confirm your body went out as expected JSON. This is the same dump you'll rely on when debugging a400response in production. - Stretch: run a deliberate failure — POST a Map missing the
namefield to a real API. Read the response body — most APIs return a structured error like{"error": "name is required"}. Add a test that asserts on that error message; this is how you verify the validation contract.
Next lesson: file uploads and multipart requests — how to send a file alongside form fields.