Matching JSON
Describes the possible expectations for matching a JsonElement or a
JsonNode with an expected value, and the options to relax the
comparison.
| Expectation | Negated | Summary |
|---|---|---|
Matches | matches the expected value, additional properties allowed | |
MatchesExactly | matches the expected value exactly |
Objects
You can verify that a JsonElement matches an expected object. Matches ignores additional properties, while
MatchesExactly also fails for them:
JsonElement track = JsonDocument.Parse("""{"title": "Let It Be", "artist": "The Beatles"}""").RootElement;
await Expect.That(track).Matches(new { title = "Let It Be" });
await Expect.That(track).MatchesExactly(new { title = "Let It Be", artist = "The Beatles" });
Matches accepts the same values as MatchesExactly with the option IgnoringAdditionalProperties().
MatchesExactly(new { title = "Let It Be" }) would fail because of the additional artist property:
Expected that track
matches new { title = "Let It Be" } exactly,
but it differed as $.artist had unexpected "The Beatles"
Arrays
You can verify that a JsonElement matches an expected array:
JsonElement titles = JsonDocument.Parse("""["Let It Be", "Yesterday", "Hey Jude"]""").RootElement;
await Expect.That(titles).Matches(["Let It Be", "Yesterday", "Hey Jude"]);
await Expect.That(titles).MatchesExactly(["Let It Be", "Yesterday", "Hey Jude"]);
Unlike additional properties, additional array elements are not ignored by Matches.
Matches(["Let It Be", "Yesterday"]) would fail with:
Expected that titles
matches ["Let It Be", "Yesterday"],
but it differed as $[2] had unexpected "Hey Jude"
JSON options:
- ignore additional properties
Primitive values
You can also verify that a JsonElement matches a primitive value:
await Expect.That(JsonDocument.Parse("\"Let It Be\"").RootElement).Matches("Let It Be");
await Expect.That(JsonDocument.Parse("4.05").RootElement).Matches(4.05);
await Expect.That(JsonDocument.Parse("true").RootElement).Matches(true);
await Expect.That(JsonDocument.Parse("null").RootElement).Matches(null);
Options
The options are available wherever JsonOptions are accepted, e.g. Matches, MatchesExactly,
IsEqualTo(expected).AsJson(o => …), IsValidJsonMatching, IsObject,
IsArray and IsJsonSerializedAs.
Like the options of aweXpect, an option that switches
something on takes an optional bool:
| Option | Configures |
|---|---|
IgnoringAdditionalProperties() | ignores additional properties of objects |
TreatingNullAsAbsent() | a property with the value null counts as absent |
IgnoringAdditionalArrayElements() | ignores additional elements of arrays |
IgnoringArrayOrder() | the elements of arrays can be in any order |
IgnoringMember(name), Ignoring(path => …) | leaves values out of the comparison |
NumbersWithin(tolerance) | numbers match within an absolute or relative tolerance |
WithJsonOptions(…) | the JsonDocumentOptions used to parse JSON strings |
Treating null as absent
By default, a property with the value null differs from a missing property. With TreatingNullAsAbsent(), a
property with the value null counts as absent, in both directions:
JsonElement track = JsonDocument.Parse("""{"title": "Let It Be", "album": null}""").RootElement;
await Expect.That(track).MatchesExactly(new { title = "Let It Be" },
o => o.TreatingNullAsAbsent());
await Expect.That(track).MatchesExactly(new { title = "Let It Be", artist = (string?)null },
o => o.TreatingNullAsAbsent());
Without the option, MatchesExactly(new { title = "Let It Be" }) would fail with:
Expected that track
matches new { title = "Let It Be" } exactly,
but it differed as $.album had unexpected <null>
Elements of an array with the value null are not affected.
Additional array elements
With IgnoringAdditionalArrayElements(), the additional elements of all compared arrays, also nested ones, are
ignored:
JsonElement titles = JsonDocument.Parse("""["Let It Be", "Yesterday", "Hey Jude"]""").RootElement;
await Expect.That(titles).Matches(["Let It Be", "Yesterday"],
o => o.IgnoringAdditionalArrayElements());
Array order
By default, arrays are compared by index. With IgnoringArrayOrder(), the elements of all compared arrays, also
nested ones, can be in any order:
JsonElement titles = JsonDocument.Parse("""["Let It Be", "Yesterday", "Hey Jude"]""").RootElement;
await Expect.That(titles).MatchesExactly(["Hey Jude", "Let It Be", "Yesterday"],
o => o.IgnoringArrayOrder());
Each expected element is paired with the first matching element of the subject that is not paired yet. The expected
elements that could not be paired are reported as missing on the array, the remaining elements of the subject as
unexpected at their index. MatchesExactly(["Hey Jude", "Something", "Help!"], o => o.IgnoringArrayOrder()) would
fail with:
Expected that titles
matches ["Hey Jude", "Something", "Help!"] exactly,
but it differed as
$ had missing "Something" and
$ had missing "Help!" and
$[0] had unexpected "Let It Be" and
$[1] had unexpected "Yesterday"
JSON options:
- ignore array order
When exactly one element remains unpaired on each side, they are compared with each other instead, e.g.
$[1] was "Yesterday" instead of "Something". Combined with IgnoringAdditionalArrayElements(), the expected
elements only have to be contained in the subject in any order. The positional members of
IsArray (At, WithElements, WithArrays and WithObjects) still verify the
element at each index.
The pairing is greedy, so overlapping It.Is… expectations can fail, although another pairing would succeed.
Ignoring members
Generated values, e.g. ids, timestamps or ETags, can be left out of the comparison with IgnoringMember(name) or
Ignoring(path => …). An ignored value and everything below it is neither compared nor reported as missing or
unexpected:
JsonElement track = JsonDocument.Parse(
"""{"id": 42, "title": "Let It Be", "meta": {"createdAt": "2026-10-16", "etag": "abc"}}""").RootElement;
await Expect.That(track).MatchesExactly(new { id = 1, title = "Let It Be", meta = new { createdAt = "2026-01-01" } },
o => o.IgnoringMember("id").IgnoringMember("createdAt").Ignoring(path => path.EndsWith(".etag")));
The name is matched against the JSON path, as it is shown in failure messages, and must cover whole trailing segments
of it, ignoring the casing: "id" ignores $.id and $.items[0].id, but not $.parentId, "meta.createdAt" only
ignores createdAt within meta, and "$.id" only ignores the property of the root. "[0]" ignores the first
element of every array; with IgnoringArrayOrder(), an index refers to the element of the subject, which is then left
out of the pairing. Explicit expectations of IsObject and
IsArray, e.g. With("id"), Without("id") or At(0), still verify an
ignored value.
The members to ignore are listed in the "JSON options" context. When the comparison fails, each name that matched no
member is named below them, together with the member it most likely meant, as a misspelled name otherwise silently
ignores nothing. For a track {"title": "Let It Be", "createdAt": "2026-10-16"},
MatchesExactly(new { title = "Yesterday", createdAt = "2026-01-01" }, o => o.IgnoringMember("craetedAt")) would fail
with:
Expected that track
matches new { title = "Yesterday", createdAt = "2026-01-01" } exactly,
but it differed as
$.title was "Let It Be" instead of "Yesterday" and
$.createdAt was "2026-10-16" instead of "2026-01-01"
JSON options:
- ignore members: ["craetedAt"]
"craetedAt" matched no member (did you mean "createdAt"?)
Numbers within a tolerance
Calculated numbers rarely match an expected literal exactly. With NumbersWithin(tolerance), a number matches when it
differs from the expected number by at most the tolerance, and with NumbersWithin(tolerance).Percent() by at most the
percentage of the expected number. The option can be combined with the other options:
JsonElement invoice = JsonDocument.Parse("""{"total": 0.30000000000000004, "tax": 0.0575}""").RootElement;
await Expect.That(invoice).MatchesExactly(new { total = 0.3, tax = 0.057m },
o => o.NumbersWithin(0.001));
await Expect.That(invoice).MatchesExactly(new { tax = 0.057m },
o => o.NumbersWithin(1).Percent().IgnoringAdditionalProperties());
The tolerance applies to every number in the compared JSON, also to integers such as ids or counts. Like the
tolerance in aweXpect, a relative tolerance only matches
an equal number when the expected number is 0. The numbers are compared as double, so numbers outside its range are
compared by their text. For a product {"name": "Guitar", "price": 110},
MatchesExactly(new { name = "Guitar", price = 100 }, o => o.NumbersWithin(5).Percent()) would fail with:
Expected that product
matches new { name = "Guitar", price = 100 } exactly,
but it differed as $.price was 110 instead of 100 ± 5%
JSON options:
- compare numbers within ± 5%
Options in failure messages
The failure message lists the active options in a "JSON options" context, like in the examples above, and omits it
when no option is active. As Matches, IsValidJsonMatching, IsObject and
IsArray ignore additional properties by default, their failures list
ignore additional properties.