eBook – Guide Spring Cloud – NPI EA (cat=Spring Cloud)
announcement - icon

Let's get started with a Microservice Architecture with Spring Cloud:

>> Join Pro and download the eBook

eBook – Mockito – NPI EA (tag = Mockito)
announcement - icon

Mocking is an essential part of unit testing, and the Mockito library makes it easy to write clean and intuitive unit tests for your Java code.

Get started with mocking and improve your application tests using our Mockito guide:

Download the eBook

eBook – Java Concurrency – NPI EA (cat=Java Concurrency)
announcement - icon

Handling concurrency in an application can be a tricky process with many potential pitfalls. A solid grasp of the fundamentals will go a long way to help minimize these issues.

Get started with understanding multi-threaded applications with our Java Concurrency guide:

>> Download the eBook

eBook – Reactive – NPI EA (cat=Reactive)
announcement - icon

Spring 5 added support for reactive programming with the Spring WebFlux module, which has been improved upon ever since. Get started with the Reactor project basics and reactive programming in Spring Boot:

>> Join Pro and download the eBook

eBook – Java Streams – NPI EA (cat=Java Streams)
announcement - icon

Since its introduction in Java 8, the Stream API has become a staple of Java development. The basic operations like iterating, filtering, mapping sequences of elements are deceptively simple to use.

But these can also be overused and fall into some common pitfalls.

To get a better understanding on how Streams work and how to combine them with other language features, check out our guide to Java Streams:

>> Join Pro and download the eBook

eBook – Jackson – NPI EA (cat=Jackson)
announcement - icon

Do JSON right with Jackson

Download the E-book

eBook – HTTP Client – NPI EA (cat=Http Client-Side)
announcement - icon

Get the most out of the Apache HTTP Client

Download the E-book

eBook – Maven – NPI EA (cat = Maven)
announcement - icon

Get Started with Apache Maven:

Download the E-book

eBook – Persistence – NPI EA (cat=Persistence)
announcement - icon

Working on getting your persistence layer right with Spring?

Explore the eBook

eBook – RwS – NPI EA (cat=Spring MVC)
announcement - icon

Building a REST API with Spring?

Download the E-book

Course – LS – NPI EA (cat=Jackson)
announcement - icon

Get started with Spring and Spring Boot, through the Learn Spring course:

>> LEARN SPRING
Course – RWSB – NPI EA (cat=REST)
announcement - icon

Explore Spring Boot 3 and Spring 6 in-depth through building a full REST API with the framework:

>> The New “REST With Spring Boot”

Course – LSS – NPI EA (cat=Spring Security)
announcement - icon

Yes, Spring Security can be complex, from the more advanced functionality within the Core to the deep OAuth support in the framework.

I built the security material as two full courses - Core and OAuth, to get practical with these more complex scenarios. We explore when and how to use each feature and code through it on the backing project.

You can explore the course here:

>> Learn Spring Security

Course – LSD – NPI EA (tag=Spring Data JPA)
announcement - icon

Spring Data JPA is a great way to handle the complexity of JPA with the powerful simplicity of Spring Boot.

Get started with Spring Data JPA through the guided reference course:

>> CHECK OUT THE COURSE

Partner – Moderne – NPI EA (cat=Spring Boot)
announcement - icon

Refactor Java code safely — and automatically — with OpenRewrite.

Refactoring big codebases by hand is slow, risky, and easy to put off. That’s where OpenRewrite comes in. The open-source framework for large-scale, automated code transformations helps teams modernize safely and consistently.

Each month, the creators and maintainers of OpenRewrite at Moderne run live, hands-on training sessions — one for newcomers and one for experienced users. You’ll see how recipes work, how to apply them across projects, and how to modernize code with confidence.

Join the next session, bring your questions, and learn how to automate the kind of work that usually eats your sprint time.

Course – LJB – NPI EA (cat = Core Java)
announcement - icon

Code your way through and build up a solid, practical foundation of Java:

>> Learn Java Basics

Partner – LambdaTest – NPI EA (cat= Testing)
announcement - icon

Distributed systems often come with complex challenges such as service-to-service communication, state management, asynchronous messaging, security, and more.

Dapr (Distributed Application Runtime) provides a set of APIs and building blocks to address these challenges, abstracting away infrastructure so we can focus on business logic.

In this tutorial, we'll focus on Dapr's pub/sub API for message brokering. Using its Spring Boot integration, we'll simplify the creation of a loosely coupled, portable, and easily testable pub/sub messaging system:

>> Flexible Pub/Sub Messaging With Spring Boot and Dapr

1. Overview

When we’re testing HTTP endpoints that return JSON we want to be able to check the contents of the response body. Often we want to capture examples of this JSON and store it in formatted example files to compare against responses.

However, we may encounter problems if some fields in the JSON returned are in a different order than our example, or if some fields contain values that change from one response to the next.

We can use REST-assured to write our test assertions, but it doesn’t solve all of the above problems by default. In this tutorial, we’ll look at how to assert JSON bodies with REST-assured, and how to use JSONAssert, JsonUnit, and ModelAssert to make it easier to handle fields that vary, or expected JSON that’s formatted differently to the precise response from the server.

2. Example Project Setup

We can use REST-assured to test any type of HTTP server. It’s commonly used with Spring Boot and Micronaut tests.

For our example, let’s use WireMock to simulate the server we’re testing.

2.1. Set up WireMock

Let’s add the dependency for WireMock to our pom.xml:

<dependency>
    <groupId>org.wiremock</groupId>
    <artifactId>wiremock-standalone</artifactId>
    <version>3.9.1</version>
    <scope>test</scope>
</dependency>

Now we can build our test to use the WireMockTest JUnit 5 extension:

@WireMockTest
class WireMockTest {
    @BeforeEach
    void beforeEach(WireMockRuntimeInfo wmRuntimeInfo) {
        // set up wiremock
    }
}

2.2. Add Example Endpoints

Inside our beforeEach() method we tell WireMock to simulate one endpoint that returns consistent static data on every request to /static:

stubFor(get("/static").willReturn(
  aResponse()
    .withStatus(200)
    .withHeader("content-type", "application/json")
    .withBody("{\"name\":\"baeldung\",\"type\":\"website\",\"text\":{\"language\":\"english\",\"code\":\"java\"}}")));

Then we add a /build endpoint which also adds some runtime data that changes on every request:

stubFor(get("/build").willReturn(
  aResponse()
    .withStatus(200)
    .withHeader("content-type", "application/json")
    .withBody("{\"build\":\"" + 
      UUID.randomUUID() + 
      "\",\"timestamp\":\"" + 
      LocalDateTime.now() + 
      "\",\"name\":\"baeldung\",\"type\":\"website\",\"text\":{\"language\":\"english\",\"code\":\"java\"}}")));

Here our build and timestamp fields are a UUID and a date stamp, respectively.

2.3. Capture JSON Bodies

It’s common at this point to capture the actual output of our endpoints and put them in a JSON file to use as an expected response.

Here are our static endpoint outputs:

{
  "name": "baeldung",
  "type": "website",
  "text": {
    "language": "english",
    "code": "java"
  }
}

And here’s the output of the /build endpoint:

{
  "build": "360dac90-38bc-4430-bbc3-a46091aea135",
  "timestamp": "2024-09-09T22:33:46.691667",
  "name": "baeldung",
  "type": "website",
  "text": {
    "language": "english",
    "code": "java"
  }
}

2.4. Setup REST-assured

Let’s add REST-assured to our pom.xml:

<dependency>
    <groupId>io.rest-assured</groupId>
    <artifactId>rest-assured</artifactId>
    <version>5.5.0</version>
    <scope>test</scope>
</dependency>

We can configure the REST-assured client to use the port exposed by WireMock within the beforeAll() of our test class:

@BeforeEach
void beforeEach(WireMockRuntimeInfo wmRuntimeInfo) {
    RestAssured.port = wmRuntimeInfo.getHttpPort();
}

Now we’re ready to write some assertions.

3. Using REST-assured out of the Box

REST-assured provides a given()/then() structure for setting up and asserting HTTP requests. This includes the ability to assert expected values in the response headers, status code, or body. It also lets us extract the body for deeper assertions.

Let’s start by seeing how to check JSON responses, using built-in features of REST-assured.

3.1. Asserting Individual Fields With REST-assured

By convention, we can use the REST-assured body() method to assert the value of an individual field in our response:

given()
  .get("/static")
  .then()
  .body("name", equalTo("baeldung"));

This uses a JSON path expression as the first parameter of body(), followed by a Hamcrest matcher to indicate the expected value.

While this is very precise for testing individual fields, it becomes long-winded when there’s a whole JSON object to assert:

given()
  .get("/static")
  .then()
  .body("name", equalTo("baeldung"))
  .body("type", equalTo("website"))
  .body("text.code", equalTo("java"))
  .body("text.language", equalTo("english"));

3.2. Asserting a Whole JSON Body as a String

REST-assured allows us to extract the whole body and assert it after REST-assured has finished its checks:

String body = given()
  .get("/static")
  .then()
  .extract()
  .body()
  .asString();

assertThat(body)
  .isEqualTo("{\"name\":\"baeldung\",\"type\":\"website\",\"text\":{\"language\":\"english\",\"code\":\"java\"}}");

Here we’ve used an assertThat() assertion from AssertJ to check the result. We should note that the body() function can use a Hamcrest matcher as its sole argument to assert the whole body. We’ll be looking into that option later.

The problem with asserting the whole body as a String is that it is easily affected by the order of fields, or by format.

3.3. Asserting the Whole Response Using a POJO

If the domain object returned by the service is already modeled in our codebase, we may find it easier to test using those domain classes. In our example, maybe we have a WebsitePojo class:

public class WebsitePojo {
    public static class WebsiteText {
        private String language;
        private String code;

        // getters, setters, equals, hashcode and constructors
    }

    private String name;
    private String type;
    private WebsiteText text;

    // getters, setters, equals, hashcode and constructors
}

With these classes, we can write a test that uses REST-assured’s extract() method to convert to a POJO for us:

WebsitePojo body = given()
  .get("/static")
  .then()
  .extract()
  .body()
  .as(WebsitePojo.class);

assertThat(body)
  .isEqualTo(new WebsitePojo("baeldung", "website", new WebsiteText("english", "java")));

Here the extract() method takes the body, parses it, and uses as() to convert it to our WebsitePojo type. We can construct an object with the expected values to compare, using AssertJ.

4. Asserting With JSONAssert

JSONAssert is one of the longest-standing JSON comparison tools. It allows for customization, allowing us to handle small differences between formats, along with handling unpredictable values.

4.1. Comparing Response Body With a String

Let’s use JSON Assert’s assertEquals() to compare the response body with an expected String:

String body = given()
  .get("/static")
  .then()
  .extract()
  .body()
  .asString();

JSONAssert.assertEquals("{\"name\":\"baeldung\",\"type\":\"website\",\"text\":{\"language\":\"english\",\"code\":\"java\"}}", body, JSONCompareMode.STRICT);

We can use STRICT mode here since the /static endpoint returns entirely predictable results.

We should note that JSON Assert’s methods throw JSONException on error, so our test method needs a throws on it:

@Test
void whenGetBody_thenCanCompareByJsonAssertAgainstFile() throws Exception {
}

4.2. Comparing Response Body With a File

If we have a convenient way of loading a file, we can use our example JSON file with the assertion:

JSONAssert.assertEquals(Files.contentOf(new File("src/test/resources/expected-website.json"), "UTF-8"), body, JSONCompareMode.STRICT);

As we have AssertJ, we can use the contentOf() function to load our test data file as a String. The fact that our JSON file is formatted is ignored by JSONAssert, which checks for semantic equivalence, rather than character-by-character.

4.3. Comparing a Response With Extra Fields

One solution to the unpredictable fields is to ignore them. We could compare the response from /build to the subset of values found in /static:

JSONAssert.assertEquals(Files.contentOf(new File("src/test/resources/expected-website.json"), "UTF-8"), body, JSONCompareMode.LENIENT)

While this prevents the test from going wrong, it would be better if we could assert the unpredictable fields in some way.

4.4. Using a Custom Comparator

As well as STRICT and LENIENT modes, JSONAssert provides customization options. While they have limitations, they work well in this situation:

String body = given()
  .get("/build")
  .then()
  .extract()
  .body()
  .asString();

JSONAssert.assertEquals(Files.contentOf(new File("src/test/resources/expected-build.json"), "UTF-8"), body,
  new CustomComparator(JSONCompareMode.STRICT,
    new Customization("build",
      new RegularExpressionValueMatcher<>("[0-9a-f-]+")),
    new Customization("timestamp",
      new RegularExpressionValueMatcher<>(".+"))));

Here we’ve added a Customization on the build field to match a regular expression with only UUID characters in it, followed by a customization for timestamp to match any non-blank string.

5. Comparison Using JsonUnit

JsonUnit is a younger JSON assertion library, influenced by AssertJ, designed for fluent assertions.

5.1. Adding JsonUnit

For fluent assertions, we add the JsonUnit AssertJ dependency:

<dependency>
    <groupId>net.javacrumbs.json-unit</groupId>
    <artifactId>json-unit-assertj</artifactId>
    <version>3.4.1</version>
    <scope>test</scope>
</dependency>

5.2. Comparing Response Body With a File

We use assertThatJson() to start a JSON assertion:

assertThatJson(body)
  .isEqualTo(Files.contentOf(new File("src/test/resources/expected-website.json"), "UTF-8"));

This can handle responses in different formats with the fields in any order.

5.3. Using Regular Expressions on Unpredictable Field Values

We can provide expected output for JsonUnit with special placeholders in it that indicate to match against a regular expression:

String body = given()
  .get("/build")
  .then()
  .extract()
  .body()
  .asString();

assertThatJson(body)
  .isEqualTo("{\"build\":\"${json-unit.regex}[0-9a-f-]+\",\"timestamp\":\"${json-unit.any-string}\",\"type\":\"website\",\"name\":\"baeldung\",\"text\":{\"language\":\"english\",\"code\":\"java\"}}");

Here the placeholder ${json-unit-regex} prefixes our UUID pattern. The ${json-unit.any-string} placeholder matches successfully against any string value.

The disadvantage of these placeholders is that they pollute the expected values with control commands to the assertion.

6. Comparison With Model Assert

ModelAssert has a similar set of features to both JSON Assert and JsonUnit. By default, it’s sensitive to the order of keys in the response.

6.1. Adding Model Assert

To use ModelAssert we add it to the pom.xml:

<dependency>
    <groupId>uk.org.webcompere</groupId>
    <artifactId>model-assert</artifactId>
    <version>1.0.3</version>
    <scope>test</scope>
</dependency>

6.2. Comparing the JSON Response Body With a File

We use assertJson() to compare a string with an expected value, which can be a File:

String body = given()
  .get("/static")
  .then()
  .extract()
  .body()
  .asString();

assertJson(body)
  .where()
  .keysInAnyOrder()
  .isEqualTo(new File("src/test/resources/expected-website-different-field-order.json"));

We don’t need to use a file reading utility as ModelAssert can read files. In this example, the expected JSON is deliberately in a different order, so where().keysInAnyOrder() has been added to the assertion before isEqualTo() is called.

6.3. Ignoring Extra Fields

Model Assert can also compare a subset of fields to a larger object:

assertJson(body)
  .where()
  .objectContains()
  .isEqualTo("{\"type\":\"website\",\"name\":\"baeldung\",\"text\":{\"language\":\"english\",\"code\":\"java\"}}");

The objectContains() rule makes ModelAssert ignore any fields not present in the expected, but present in the actual.

6.4. Adding Rules for Unpredictable Fields

However, it’s better to customize ModelAssert to assert the fields that are present, even if we can’t predict their exact values:

String body = given()
  .get("/build")
  .then()
  .extract()
  .body()
  .asString();

assertJson(body)
  .where()
  .keysInAnyOrder()
  .path("build").matches("[0-9a-f-]+")
  .path("timestamp").matches("[0-9:T.-]+")
  .isEqualTo(new File("src/test/resources/expected-build.json"));

Here the two path() rules add a regular expression match for the build and timestamp fields.

7. Tighter Integration

As we saw earlier, REST-assured is an assertion library supporting Hamcrest matchers in its body() method. To use it with the other JSON assertion libraries, we’ve had to extract the response body. Each of the libraries can be used as a Hamcrest matcher. Depending on our use case, this may make our test code easier to read.

7.1. JSON Assert Hamcrest

For this we need an extra dependency produced by a different contributor:

<dependency>
    <groupId>uk.co.datumedge</groupId>
    <artifactId>hamcrest-json</artifactId>
    <version>0.2</version>
</dependency>

This handles simple use cases well:

given()
  .get("/build")
  .then()
  .body(sameJSONAs(Files.contentOf(new File("src/test/resources/expected-website.json"), "UTF-8")).allowingExtraUnexpectedFields());

The sameJSONAs builds a Hamcrest matcher using JSON Assert as the engine. However, it only has limited customization options. In this case, we can only use allowExtraUnexpectedFields().

7.2. JsonUnit Hamcrest

We need to add an extra dependency from the JsonUnit project to use the Hamcrest matcher:

<dependency>
    <groupId>net.javacrumbs.json-unit</groupId>
    <artifactId>json-unit</artifactId>
    <version>3.4.1</version>
    <scope>test</scope>
</dependency>

Then we can write an asserting matcher inside the body() function of REST-assured:

given()
  .get("/build")
  .then()
  .body(jsonEquals(Files.contentOf(new File("src/test/resources/expected-website.json"), "UTF-8")).when(Option.IGNORING_EXTRA_FIELDS));

Here the jsonEquals defines the matcher, customized by the when() function.

7.3. ModelAssert Hamcrest

ModelAssert was built to be both a standalone assertion and a Hamcrest matcher. We use the json() method to create a Hamcrest matcher:

given()
  .get("/build")
  .then()
  .body(json().where()
    .keysInAnyOrder()
    .path("build").matches("[0-9a-f-]+")
    .path("timestamp").matches("[0-9:T.-]+")
    .isEqualTo(new File("src/test/resources/expected-build.json")));

All the customization options from earlier are available in the same way.

8. Comparison of Libraries

JSONAssert is the most well-established library, but its complex customization along with its use of checked exceptions makes it a little fiddly to use.

JsonUnit is a growing library with a lot of users and a lot of customization options.

ModelAssert has more explicit support for programmatic customization and comparison against expected results in files. It’s a less well-known and less mature library.

9. Conclusion

In this article, we looked at how to compare JSON bodies returned from testing REST endpoints with expected JSON data that we might wish to store in files.

We looked at the challenges of field values that cannot be predicted and looked at how we could perform assertions natively with REST-assured as well as three sophisticated JSON comparison assertion libraries.

Finally, we looked at how to bring the assertions into the REST-assured syntax via the use of hamcrest matchers.

The code backing this article is available on GitHub. Once you're logged in as a Baeldung Pro Member, start learning and coding on the project.
Baeldung Pro – NPI EA (cat = Baeldung)
announcement - icon

Baeldung Pro comes with both absolutely No-Ads as well as finally with Dark Mode, for a clean learning experience:

>> Explore a clean Baeldung

Once the early-adopter seats are all used, the price will go up and stay at $33/year.

eBook – HTTP Client – NPI EA (cat=HTTP Client-Side)
announcement - icon

The Apache HTTP Client is a very robust library, suitable for both simple and advanced use cases when testing HTTP endpoints. Check out our guide covering basic request and response handling, as well as security, cookies, timeouts, and more:

>> Download the eBook

eBook – Java Concurrency – NPI EA (cat=Java Concurrency)
announcement - icon

Handling concurrency in an application can be a tricky process with many potential pitfalls. A solid grasp of the fundamentals will go a long way to help minimize these issues.

Get started with understanding multi-threaded applications with our Java Concurrency guide:

>> Download the eBook

eBook – Java Streams – NPI EA (cat=Java Streams)
announcement - icon

Since its introduction in Java 8, the Stream API has become a staple of Java development. The basic operations like iterating, filtering, mapping sequences of elements are deceptively simple to use.

But these can also be overused and fall into some common pitfalls.

To get a better understanding on how Streams work and how to combine them with other language features, check out our guide to Java Streams:

>> Join Pro and download the eBook

eBook – Persistence – NPI EA (cat=Persistence)
announcement - icon

Working on getting your persistence layer right with Spring?

Explore the eBook

Course – LS – NPI EA (cat=REST)

announcement - icon

Get started with Spring Boot and with core Spring, through the Learn Spring course:

>> CHECK OUT THE COURSE

Partner – Moderne – NPI EA (tag=Refactoring)
announcement - icon

Modern Java teams move fast — but codebases don’t always keep up. Frameworks change, dependencies drift, and tech debt builds until it starts to drag on delivery. OpenRewrite was built to fix that: an open-source refactoring engine that automates repetitive code changes while keeping developer intent intact.

The monthly training series, led by the creators and maintainers of OpenRewrite at Moderne, walks through real-world migrations and modernization patterns. Whether you’re new to recipes or ready to write your own, you’ll learn practical ways to refactor safely and at scale.

If you’ve ever wished refactoring felt as natural — and as fast — as writing code, this is a good place to start.

eBook Jackson – NPI EA – 3 (cat = Jackson)