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.
As always, the example code can be found over on GitHub.