When you hear API and web client together you think REST
, at least I do. This is because it is still one of the most popular architectural paradigms for building APIs. Another approach about which I hear from time to time is GraphQL
. There is one common part for these two - they talk JSON. Both specifications REST
and GraphQL
does not restrict only to this format but due to its simplicity, it is a very common choice. Every healthy software contains tests on different layers, from unit up to the end-to-end tests. So, how could we verify responses from our APIs?
In java
world, especiality in Spring
ecosystem when it comes to testing APIs you probably saw one of these:
this.mockMvc.perform(get("/hello?name=John"))
.andExpect(status().isOk())
.andExpect(jsonPath("$.name", is("John")));
jsonPath
static method in the above snippet is a wrapper around great library JsonPath. Together with Hamcrest it just makes writing test against JSON a true pleasure.
Note: JsonPath does not require Spring
it just need some JSON.
I have created simple playground for testing different JsonPath expressions. jsonPath
static method is provided which expects expression, matcher and a JSON string against which expression will be executed.
Let us assume that json
argument in the following examples will have this structure:
{
"_id": "5cdaff5fce90bc3ddd98475b",
"guid": "57cc1cbb-52ba-49ff-af66-4b9dde85a207",
"isActive": false,
"a": {
"b": {
"c": {
"d": {
"e": {
"f": "end"
}
}
}
}
},
"f": "other end"
}
Working with JSON objects
Every JsonPath expression starts with $
symbol which represents root element (no matter if the JSON structure is an object or array). When you want to access the specific property you have two choices - dot or bracket notation.
//dot notation
jsonPath("$.guid", is("57cc1cbb-52ba-49ff-af66-4b9dde85a207")).runAgainst(json);
jsonPath("$.isActive", isA(Boolean.class)).runAgainst(json);
jsonPath("$.age", instanceOf(Number.class)).runAgainst(json);
jsonPath("$.nonExisting", nullValue());
jsonPath("$.name.first", is("Opal")).runAgainst(json);
//bracket notation
jsonPath("$['guid']", is("57cc1cbb-52ba-49ff-af66-4b9dde85a207")).runAgainst(json);
jsonPath("$['isActive']", isA(Boolean.class)).runAgainst(json);
jsonPath("$['name']['first']", is("Opal")).runAgainst(json);
Although the second approach is noisier it is useful in situation when property name contains special characters or starts with a character other than represented by this regular expression /[a-zA-Z_]/
.
We can also do a deep scan to access property in very nested object. To acccess f
property in our sample we could:
jsonPath("$..f", hasItems("end", "other-end")).runAgainst(json);
// or using standard dot notation
jsonPath("$.a.b.c.d.e.f", is("end")).runAgainst(json);
Notice that deep scann returns array of values because depending on JSON structure it can find more properties with the name f
. We can narrow down what path will be part of a deep scan by pointing to spefici property from which deep scan starts:
jsonPath("$.a..f", hasItems("end")).runAgainst(json);
Working with JSON arrays
Let us assume that json
argument in the following examples will have this structure:
{
"friends": [
{
"id": 0,
"name": "Hester Dallai",
"age": 31
},
{
"id": 1,
"name": "Lucinda Goff",
"age": 27
},
{
"id": 2,
"name": "Ella Day",
"age": 17
}
],
"adult": 18
}
To access the single property from an object inside array you use square brackets just like in java
.
jsonPath("$.friends[0].id", is(0)).runAgainst(json);
JsonPath expressions support wildcards (*
) which selects all elements in both object and arrays.
To verify the age of our friends we can:
jsonPath("$.friends[*].age", hasItems(31, 27, 17)).runAgainst(json);
When you have ever worked with python
you probably came across slice notation, which in some part is supported in JsonPath.
[start:stop] # from start to stop - 1
[start:] # from start to the end of array
[:n] # selects first n - 1 elements
[-n:] # selects last n elements
Knowing this slice expressions would like like this:
jsonPath("$.friends[:3].age", hasItems(31, 27)).runAgainst(json);
jsonPath("$.friends[1:2].age", hasItems(27)).runAgainst(json);
jsonPath("$.friends[:2].age", hasItems(31, 27)).runAgainst(json);
jsonPath("$.friends[-2:].age", hasItems(17, 27)).runAgainst(json);
Filter expression
Next powerful feature of JsonPath are filter expression [?(<expression>)]
which make selection of elements more dynamic. Complete list of supported operators can be found at https://github.com/json-path/JsonPath#filter-operators.
jsonPath("$.friends[?(@.age >= 18)].id", hasItems(0, 1)).runAgainst(json);
jsonPath("$.friends[?(@.id == 1)].age", hasItems(27)).runAgainst(json);
jsonPath("$.friends[?(@.id != 1)].age", hasItems(31, 17)).runAgainst(json);
jsonPath("$.friends[?(@.age in [31, 17])].id", hasItems(0, 2)).runAgainst(json);
jsonPath("$.friends[?(@.name =~ /^.*Da.*$/i)].id", hasItems(0, 2)).runAgainst(json);
@
represent the current node. In the above example this will represent friend node and for each of our friends ($.friends[]
), given property will be returned when the expression evaluates to true.
We can be even more dynamic by referencing to other properties:
jsonPath("$.friends[?(@.age >= 18)].id", hasItems(0, 1)).runAgainst(json);
jsonPath("$.friends[?(@.age >= $.adult)].id", hasItems(0, 1)).runAgainst(json);
Functions
JsonPath provides functions that can be added at the end of our expression. The rule is simple - output of expression is input to the function. Complete list of build in functions can be found at https://github.com/json-path/JsonPath#functions.
Let us assume that json
argument in the following examples will have this structure:
{
"range": [
0,
1,
2,
3,
4,
5,
6,
7,
8,
9
]
}
then we can run functions that expects arrays as input
jsonPath("$.range.avg()", is(4.5)).runAgainst(json);
jsonPath("$.range.min()", is(0.0)).runAgainst(json);
jsonPath("$.range.max()", is(9.0)).runAgainst(json);
jsonPath("$.range.stddev()", is(2.8722813232690143)).runAgainst(json);
jsonPath("$.range.length()", is(10)).runAgainst(json);
Conclusion
As you saw JsonPath expressions are very powerful, they should be able to extract whatever you want from a JSON structure. All used examples and more can be found on github.