JsonAssertions.TUnit
0.2.0
Prefix Reserved
dotnet add package JsonAssertions.TUnit --version 0.2.0
NuGet\Install-Package JsonAssertions.TUnit -Version 0.2.0
<PackageReference Include="JsonAssertions.TUnit" Version="0.2.0" />
<PackageVersion Include="JsonAssertions.TUnit" Version="0.2.0" />
<PackageReference Include="JsonAssertions.TUnit" />
paket add JsonAssertions.TUnit --version 0.2.0
#r "nuget: JsonAssertions.TUnit, 0.2.0"
#:package JsonAssertions.TUnit@0.2.0
#addin nuget:?package=JsonAssertions.TUnit&version=0.2.0
#tool nuget:?package=JsonAssertions.TUnit&version=0.2.0
JsonAssertions.TUnit
Scope: Test projects only. Not intended for production code.
TUnit-native JSON assertions for .NET. Fluent entry points over TUnit's Assert.That(...) pipeline for asserting against System.Text.Json documents and HTTP response bodies. AOT-compatible, trimmable, no runtime reflection in the assertion path.
Full documentation, design notes, and roadmap: github.com/JohnVerheij/JsonAssertions.TUnit
Status: v0.2.0
Each entry point is available over a JSON string, a System.Text.Json.JsonElement, and an HttpResponseMessage (whose body is read as the JSON document).
| Entry point | Behaviour |
|---|---|
HasJsonProperty(path) |
Asserts a property exists at the path. |
DoesNotHaveJsonProperty(path) |
Asserts no property exists at the path. |
HasJsonValue(path, expected) |
Asserts the value at path equals expected (a string, bool, or number). |
HasJsonValueOneOf(path, T[]) |
Asserts the value at path is one of the given strings or numbers. |
HasJsonValueMatching(path, predicate) |
Asserts the value at path satisfies Func<JsonElement, bool>. |
HasJsonValueParsableAs<T>(path) |
Asserts the value at path is a JSON string parseable as T (where T : IParsable<T>). |
HasJsonValueKind(path, kind) |
Asserts the value at path is of the given JsonValueKind. |
HasJsonBoolean(path) |
Asserts the value at path is a JSON boolean (true or false). |
HasNonEmptyJsonString(path) |
Asserts the value at path is a non-empty JSON string. |
HasJsonArrayLength(path, length) |
Asserts the value at path is a JSON array of the given length. |
HasNonEmptyJsonArray(path) / HasEmptyJsonArray(path) |
Asserts the value at path is a non-empty / empty JSON array. |
The path is a dot-separated property navigation with optional [N] zero-based bracket indices and an optional leading $ JSONPath root reference: user.name, items[0].id, objects[0].planData[1].pickPlanId, $[0] for a root-array first element. See the path-syntax notes on GitHub for the full grammar.
The point over a hand-rolled TryGetProperty(...).IsTrue() helper is the failure message: every assertion renders a path-context block saying where resolution stopped, not merely that it did.
Install
dotnet add package JsonAssertions.TUnit
Requirements: TUnit 1.44.39 or later, .NET 10. System.Text.Json is in-box on .NET 10, so the package carries no runtime dependency beyond TUnit.
Quick start
using System.Text.Json;
[Test]
public async Task ResponseBodyHasExpectedShape(CancellationToken ct)
{
string json = """{"user":{"name":"alice","age":30},"roles":["admin"]}""";
await Assert.That(json).HasJsonProperty("user.name");
await Assert.That(json).HasJsonValue("user.age", 30);
await Assert.That(json).HasJsonArrayLength("roles", 1);
}
The fluent entry points auto-import via TUnit.Assertions.Extensions. The same entry points work on a JsonElement, and directly on an HttpResponseMessage:
// Reads the response body and asserts against it. The cancellation token flows to the read.
await Assert.That(response).HasJsonProperty("user.name", ct);
await Assert.That(response).HasJsonValue("user.age", 30, ct);
When an assertion fails, the message names the failure point:
to have a JSON property at path "user.address.city"
resolved as far as: user.address
no property "city" on "user.address"
A response body or string that is not valid JSON fails the assertion with an explained message rather than throwing a raw JsonException.
Two namespaces
The single package places types in two namespaces, the same shape as the rest of the assertion family:
| Type | Namespace | Auto-imported? |
|---|---|---|
JsonPath, JsonPathResolution, JsonValueComparison, JsonShape (framework-agnostic core) |
JsonAssertions |
No (needs using JsonAssertions;) |
| Source-generated assertion entry points | TUnit.Assertions.Extensions |
Yes (TUnit auto-imports) |
Roadmap
- Deserialise-then-predicate assertions.
- Semantic JSON equality and subset / fragment matching.
Family
Part of an assertion family for TUnit:
License
MIT. Copyright (c) 2026 John Verheij.
| Product | Versions Compatible and additional computed target framework versions. |
|---|---|
| .NET | net10.0 is compatible. net10.0-android was computed. net10.0-browser was computed. net10.0-ios was computed. net10.0-maccatalyst was computed. net10.0-macos was computed. net10.0-tvos was computed. net10.0-windows was computed. |
-
net10.0
- TUnit.Assertions (>= 1.44.39)
- TUnit.Core (>= 1.44.39)
NuGet packages
This package is not used by any NuGet packages.
GitHub repositories
This package is not used by any popular GitHub repositories.
View the rendered release notes: https://github.com/JohnVerheij/JsonAssertions.TUnit/releases/tag/v0.2.0
### Added
- **Array-indexed path segments.** `JsonPath.Resolve(element, "items[0].name")` navigates `items` (object property) then index 0 (array element) then `name` (object property). Indices are zero-based, non-negative integers in `[N]` brackets. Mixed property + index segments compose freely (`objects[0].planData[1].pickPlanId`). Closes the #1 friction surfaced by the v0.1.0 adoption survey.
- **`$` JSONPath root-self.** `JsonPath.Resolve(element, "$")` resolves to the supplied element itself. `$.user.name` is equivalent to `user.name`; `$[0]` is equivalent to `[0]`. A bare `[0]` against a root array also works without the `$` prefix. Closes the "no path to assert root-array shape" gap surfaced by the v0.1.0 adoption survey.
- **`HasNonEmptyJsonString(path)`** on `string` and `JsonElement` (and `HttpResponseMessage` for the async HTTP entry point). Asserts the value at `path` is a JSON string of non-zero length. A non-string kind or an empty `""` string fails.
- **`HasJsonBoolean(path)`** on `string`, `JsonElement`, and `HttpResponseMessage`. Asserts the value at `path` is a JSON boolean (either `true` or `false`). `JsonValueKind.True` and `.False` are distinct kinds, so this is the discoverable form of "this field is a bool, either value" that `HasJsonValueKind` alone cannot express.
- **`HasJsonValueMatching(path, Func<JsonElement, bool> predicate)`** on `string`, `JsonElement`, and `HttpResponseMessage`. Asserts the value at `path` satisfies a consumer-supplied predicate. Covers the ~¼ of value assertions that are not exact-equality (numeric inequalities, complex shape checks).
- **`HasJsonValueOneOf(path, string[])`** and **`HasJsonValueOneOf(path, double[])`** on `string`, `JsonElement`, and `HttpResponseMessage`. The discoverable form for "value is one of {Healthy, Degraded, Unhealthy}" or "code is one of {200, 503, 504}". Callers pass a C# 12 collection-expression literal: `HasJsonValueOneOf("status", ["Healthy", "Degraded"])`.
- **`HasJsonValueParsableAs<T>(path) where T : IParsable<T>`** on `string`, `JsonElement`, and `HttpResponseMessage`. Asserts the value at `path` is a JSON string whose text parses as `T` via `T.TryParse(value, CultureInfo.InvariantCulture, out _)`. Covers the "value parses as `Guid` / `DateTimeOffset` / `Uri`" pattern without committing to a particular parser per call site.
- **`JsonShape.IsNonEmptyString(JsonElement)`** and **`JsonShape.IsBoolean(JsonElement)`** framework-agnostic predicates in the `JsonAssertions` core, matching the family pattern (core predicate + TUnit-adapter assertion).
- **`JsonValueComparison.MatchesAny(JsonElement, string[])`** and **`JsonValueComparison.MatchesAny(JsonElement, double[])`** framework-agnostic comparison primitives in the `JsonAssertions` core.
- **`Directory.Build.targets` auto-extracts the per-version section from `CHANGELOG.md` at pack time** and feeds it into `<PackageReleaseNotes>`, so the Release Notes tab on the nuget.org package page shows the per-version body verbatim rather than a literal placeholder. Affects releases from this version onward; nupkgs already on nuget.org are immutable.
- **Prepended `View the rendered release notes: <url>` line** on the extracted body, pointing at the matching GitHub Release. nuget.org renders the Release Notes tab as plaintext with preserved line breaks rather than rendered markdown ([NuGet/NuGetGallery#8889](https://github.com/NuGet/NuGetGallery/issues/8889) is the open feature request); the prepended URL gives consumers a one-click route to the rendered-markdown version on GitHub.
### Changed
- **`JsonPath.Resolve` failure-point context for array failures.** An out-of-range index on an array, or an index access on a non-array, now surfaces in `FailedSegment` as `[N]` (matching the resolved-prefix syntax) and renders a tailored reason line in the failure message: `no element at index [N] on "items"` for an array out-of-range; `cannot index [N]: "user" is an Object, not an array` for an index access on a non-array.
- **`<PackageReleaseNotes>` fallback** in `JsonAssertions.TUnit.csproj` is now `$(RepositoryUrl)/releases/tag/v$(Version)` rather than the literal text "See CHANGELOG.md". The URL is auto-linked by nuget.org, so the no-match case still gives consumers a one-click route to the matching GitHub Release notes.
- **`CONVENTIONS.md` updated to v0.5.** Adds a `CHANGELOG conventions` section (Keep a Changelog 1.1.0 standard headers, user-facing-only content, header order, stylistic rules) and a `PackageReleaseNotes` auto-extract convention. Supersedes the v0.4 bump that added `JsonAssertions.TUnit` to the family roster; the v0.5 file remains copied identically across all five family repos.
- **`CODE_OF_CONDUCT.md` upgraded to Contributor Covenant v3.0** from v2.1. The maintainer contact link is now the GitHub profile URL (https://github.com/JohnVerheij) rather than a private email address, since GitHub keeps the primary email private.
- **`PackageValidationBaselineVersion` set to `0.1.0`.** ApiCompat now validates the additive v0.2.0 surface against the v0.1.0 baseline; `CompatibilitySuppressions.xml` is regenerated to capture the accepted additive differences from v0.1.0.
- **Package description** revised to drop the v0.0.1 / v0.1.0 sequencing narrative and describe the current shipped surface verbatim.