Introduction

In our REST Assured series, we’ve covered topics like Parameterized Tests, REST Assured with JUnit, and Request and Response Logging, building a robust toolkit for API testing. Now, we’ll explore POJO Mapping, a technique to map API responses to Plain Old Java Objects (POJOs) for easier validation and test maintainability. This guide demonstrates how to use REST Assured to deserialize JSON responses into POJOs, with practical examples and best practices. It’s designed for beginners and experienced developers, ensuring you can handle complex API data effectively.

Key Point: POJO Mapping in REST Assured simplifies API response validation by converting JSON or XML data into Java objects, enabling object-oriented assertions.

What is POJO Mapping?

A POJO (Plain Old Java Object) is a simple Java class with private fields, public getters, and setters, without complex dependencies. In REST Assured, POJO mapping involves deserializing API responses (typically JSON) into POJO instances using libraries like Jackson or Gson, which REST Assured supports out of the box. Benefits include:

  • Type Safety: Validate response data using Java’s type system.
  • Readability: Perform assertions on object properties instead of JSON paths.
  • Reusability: Reuse POJOs across multiple tests or projects.
  • Complex Data Handling: Easily manage nested objects and collections.

We’ll use https://jsonplaceholder.typicode.com for examples, mapping responses from endpoints like /posts and /comments to POJOs, including nested structures and lists.

Setting Up for POJO Mapping

We’ll set up a Maven project with REST Assured, JUnit 5, and Allure for reporting, consistent with previous posts. Jackson is included via REST Assured’s dependencies for JSON deserialization.

Here’s the pom.xml, styled with your preferred Blogger format for XML syntax highlighting:



    4.0.0
    com.example
    rest-assured-pojo
    1.0-SNAPSHOT

    
        11
        2.27.0
        1.9.22
    

    
        
            io.rest-assured
            rest-assured
            5.4.0
            test
        
        
            org.junit.jupiter
            junit-jupiter
            5.10.2
            test
        
        
            org.hamcrest
            hamcrest
            2.2
            test
        
        
            com.fasterxml.jackson.core
            jackson-databind
            2.15.2
            test
        
        
            io.qameta.allure
            allure-junit5
            ${allure.version}
            test
        
    

    
        
            
                org.apache.maven.plugins
                maven-surefire-plugin
                3.2.5
                
                    
                        -javaagent:"${settings.localRepository}/org/aspectj/aspectjweaver/${aspectj.version}/aspectjweaver-${aspectj.version}.jar"
                    
                
                
                    
                        org.aspectj
                        aspectjweaver
                        ${aspectj.version}
                    
                
            
            
                io.qameta.allure
                allure-maven
                2.12.0
                
                    ${allure.version}
                
            
        
    

No additional setup is required for https://jsonplaceholder.typicode.com. We’ll create POJO classes to match the API’s JSON structure.

Implementing POJO Mapping

Let’s explore how to map API responses to POJOs using REST Assured, covering simple objects, nested objects, and collections.

Example 1: Mapping a Single Object

Map a single post response to a POJO.

Create the Post POJO:


public class Post {
    private int id;
    private int userId;
    private String title;
    private String body;

    // Default constructor for Jackson
    public Post() {}

    // Getters and Setters
    public int getId() { return id; }
    public void setId(int id) { this.id = id; }
    public int getUserId() { return userId; }
    public void setUserId(int userId) { this.userId = userId; }
    public String getTitle() { return title; }
    public void setTitle(String title) { this.title = title; }
    public String getBody() { return body; }
    public void setBody(String body) { this.body = body; }
}

Test code:


import io.qameta.allure.Description;
import io.qameta.allure.Feature;
import io.restassured.RestAssured;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.DisplayName;
import org.junit.jupiter.api.Test;
import static io.restassured.RestAssured.*;
import static org.junit.jupiter.api.Assertions.*;

@Feature("POJO Mapping with REST Assured")
public class SingleObjectMappingTest {

    @BeforeEach
    public void setup() {
        RestAssured.baseURI = "https://jsonplaceholder.typicode.com";
    }

    @Test
    @DisplayName("Test mapping a single post to POJO")
    @Description("Test mapping a single post response to a POJO")
    public void testMapSinglePost() {
        Post post = given()
            .log().all()
        .when()
            .get("/posts/1")
        .then()
            .log().all()
            .statusCode(200)
            .extract().as(Post.class);

        assertEquals(1, post.getId(), "Post ID should be 1");
        assertTrue(post.getUserId() > 0, "User ID should be positive");
        assertNotNull(post.getTitle(), "Title should not be null");
        assertNotNull(post.getBody(), "Body should not be null");
    }
}

Explanation:

  • Post POJO: Matches the JSON structure of a post (e.g., {"id":1,"userId":1,"title":"...","body":"..."}).
  • extract().as(Post.class): Deserializes the JSON response into a Post object using Jackson.
  • JUnit assertions validate the POJO’s properties.
Important: Ensure POJO field names match JSON keys, or use Jackson annotations (e.g., @JsonProperty) for mismatches.

Example 2: Mapping Nested Objects

Map a comment response with a nested post reference.

Create the Comment and PostReference POJOs:


public class Comment {
    private int id;
    private String name;
    private String email;
    private String body;
    private PostReference post;

    // Default constructor for Jackson
    public Comment() {}

    // Getters and Setters
    public int getId() { return id; }
    public void setId(int id) { this.id = id; }
    public String getName() { return name; }
    public void setName(String name) { this.name = name; }
    public String getEmail() { return email; }
    public void setEmail(String email) { this.email = email; }
    public String getBody() { return body; }
    public void setBody(String body) { this.body = body; }
    public PostReference getPost() { return post; }
    public void setPost(PostReference post) { this.post = post; }
}

public class PostReference {
    private int id;

    // Default constructor for Jackson
    public PostReference() {}

    // Getters and Setters
    public int getId() { return id; }
    public void setId(int id) { this.id = id; }
}

Test code:


import io.qameta.allure.Description;
import io.qameta.allure.Feature;
import io.restassured.RestAssured;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.DisplayName;
import org.junit.jupiter.api.Test;
import static io.restassured.RestAssured.*;
import static org.junit.jupiter.api.Assertions.*;

@Feature("POJO Mapping with REST Assured")
public class NestedObjectMappingTest {

    @BeforeEach
    public void setup() {
        RestAssured.baseURI = "https://jsonplaceholder.typicode.com";
    }

    @Test
    @DisplayName("Test mapping a comment with nested post to POJO")
    @Description("Test mapping a comment response with nested post to POJO")
    public void testMapCommentWithPost() {
        Comment comment = given()
            .log().all()
        .when()
            .get("/comments/1")
        .then()
            .log().all()
            .statusCode(200)
            .extract().as(Comment.class);

        assertEquals(1, comment.getId(), "Comment ID should be 1");
        assertNotNull(comment.getEmail(), "Email should not be null");
        assertNotNull(comment.getPost(), "Post reference should not be null");
        assertEquals(1, comment.getPost().getId(), "Post ID should be 1");
    }
}

Explanation:

  • Comment POJO: Includes a PostReference object to map the nested postId field.
  • Assertions validate both top-level and nested properties.
  • Jackson automatically maps nested JSON fields to corresponding POJO fields.

Example 3: Mapping a List of Objects

Map a list of posts to a collection of POJOs.

Use the Post POJO from Example 1.

Test code:


import io.qameta.allure.Description;
import io.qameta.allure.Feature;
import io.restassured.RestAssured;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.DisplayName;
import org.junit.jupiter.api.Test;
import java.util.List;
import static io.restassured.RestAssured.*;
import static org.junit.jupiter.api.Assertions.*;

@Feature("POJO Mapping with REST Assured")
public class ListMappingTest {

    @BeforeEach
    public void setup() {
        RestAssured.baseURI = "https://jsonplaceholder.typicode.com";
    }

    @Test
    @DisplayName("Test mapping a list of posts to POJOs")
    @Description("Test mapping a list of posts to a collection of POJOs")
    public void testMapListOfPosts() {
        List posts = given()
            .log().all()
        .when()
            .get("/posts")
        .then()
            .log().all()
            .statusCode(200)
            .extract().jsonPath().getList("", Post.class);

        assertEquals(100, posts.size(), "Should return 100 posts");
        assertTrue(posts.stream().allMatch(post -> post.getId() > 0), "All post IDs should be positive");
        assertTrue(posts.stream().allMatch(post -> post.getTitle() != null), "All titles should be non-null");
    }
}

Explanation:

  • getList("", Post.class): Deserializes the JSON array into a List.
  • Assertions use Java streams to validate properties across all posts.
  • The empty path "" indicates the root array in the response.
Pro Tip: Use jsonPath().getList() for collections, specifying the POJO class to map array elements.

Example 4: Serializing POJO for Requests

Serialize a POJO to JSON for a POST request.

Use the Post POJO from Example 1.

Test code:


import io.qameta.allure.Description;
import io.qameta.allure.Feature;
import io.restassured.RestAssured;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.DisplayName;
import org.junit.jupiter.api.Test;
import static io.restassured.RestAssured.*;
import static org.hamcrest.Matchers.*;

@Feature("POJO Mapping with REST Assured")
public class SerializePOJOTest {

    @BeforeEach
    public void setup() {
        RestAssured.baseURI = "https://jsonplaceholder.typicode.com";
    }

    @Test
    @DisplayName("Test serializing POJO for POST request")
    @Description("Test serializing a POJO to JSON for a POST request")
    public void testSerializePost() {
        Post post = new Post();
        post.setTitle("Test Post");
        post.setBody("This is a test");
        post.setUserId(1);

        given()
            .header("Content-Type", "application/json")
            .body(post)
            .log().all()
        .when()
            .post("/posts")
        .then()
            .log().all()
            .statusCode(201)
            .body("title", equalTo("Test Post"))
            .body("userId", equalTo(1));
    }
}

Explanation:

  • body(post): Serializes the Post POJO to JSON using Jackson.
  • Verifies the response echoes the sent data (as per jsonplaceholder.typicode.com behavior).
  • POJOs simplify request construction for complex payloads.

Example 5: POJO Mapping with Allure

Integrate POJO mapping with Allure reporting.


import io.qameta.allure.Allure;
import io.qameta.allure.Description;
import io.qameta.allure.Feature;
import io.restassured.RestAssured;
import com.fasterxml.jackson.databind.ObjectMapper;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.DisplayName;
import org.junit.jupiter.api.Test;
import java.io.IOException;
import static io.restassured.RestAssured.*;
import static org.junit.jupiter.api.Assertions.*;

@Feature("POJO Mapping with REST Assured")
public class AllurePOJOTest {

    @BeforeEach
    public void setup() {
        RestAssured.baseURI = "https://jsonplaceholder.typicode.com";
    }

    @Test
    @DisplayName("Test mapping post to POJO with Allure")
    @Description("Test mapping a post to POJO with Allure reporting")
    public void testMapPostWithAllure() throws IOException {
        Post post = given()
            .log().all()
        .when()
            .get("/posts/1")
        .then()
            .log().all()
            .statusCode(200)
            .extract().as(Post.class);

        assertEquals(1, post.getId(), "Post ID should be 1");
        assertNotNull(post.getTitle(), "Title should not be null");

        // Attach POJO as JSON to Allure report
        ObjectMapper mapper = new ObjectMapper();
        String postJson = mapper.writeValueAsString(post);
        Allure.addAttachment("Post POJO", "application/json", postJson, ".json");
    }
}

Explanation:

  • ObjectMapper: Converts the POJO back to JSON for Allure attachment.
  • Allure.addAttachment: Attaches the serialized POJO to the report.
  • Run mvn clean test and mvn allure:serve to view the report.

Integrating with CI/CD

Add POJO mapping tests to a GitHub Actions pipeline for automation.

Create or update .github/workflows/ci.yml:


name: REST Assured POJO CI Pipeline

on:
  push:
    branches:
      - main
  pull_request:
    branches:
      - main

jobs:
  build-and-test:
    runs-on: ubuntu-latest

    steps:
      - name: Checkout code
        uses: actions/checkout@v4

      - name: Set up JDK 11
        uses: actions/setup-java@v4
        with:
          java-version: '11'
          distribution: 'temurin'

      - name: Cache Maven dependencies
        uses: actions/cache@v4
        with:
          path: ~/.m2/repository
          key: ${{ runner.os }}-maven-${{ hashFiles('**/pom.xml') }}
          restore-keys: ${{ runner.os }}-maven-

      - name: Run tests
        run: mvn clean test

      - name: Generate Allure report
        run: mvn allure:report

      - name: Upload Allure results
        if: always()
        uses: actions/upload-artifact@v4
        with:
          name: allure-results
          path: target/allure-results

      - name: Publish Allure report
        if: always()
        uses: simple-elf/allure-report-action@v1.7
        with:
          allure_results: target/allure-results
          gh_pages: gh-pages
          allure_report: allure-report

      - name: Deploy report to GitHub Pages
        if: always()
        uses: peaceiris/actions-gh-pages@v4
        with:
          github_token: ${{ secrets.GITHUB_TOKEN }}
          publish_dir: allure-report

Explanation:

  • Runs POJO mapping tests with mvn clean test.
  • Publishes Allure reports to GitHub Pages for visibility.

Tips for Beginners

  • Match JSON Structure: Ensure POJO fields align with API response keys, using @JsonProperty for mismatches.
  • Use Lombok: Consider Lombok to reduce boilerplate code for getters, setters, and constructors.
  • Validate Selectively: Assert only critical POJO properties to keep tests focused.
  • Debug with Logging: Enable log().all() to inspect JSON when mapping fails.
Troubleshooting Tip: If POJO mapping fails, verify the JSON structure with log().all() and check for missing fields or type mismatches in the POJO.

What’s Next?

This post enhances our REST Assured series by introducing POJO Mapping, a powerful technique for handling API responses. To continue your learning, consider exploring:

  • GraphQL Testing: Testing GraphQL APIs with REST Assured.
  • Advanced Allure Reporting: Customizing reports with environments and categories.
  • End-to-End Testing: Combining REST Assured with Selenium for UI and API testing.
Stay tuned for more testing tutorials!