JsonAssertions.TUnit 0.6.1

Prefix Reserved
dotnet add package JsonAssertions.TUnit --version 0.6.1
                    
NuGet\Install-Package JsonAssertions.TUnit -Version 0.6.1
                    
This command is intended to be used within the Package Manager Console in Visual Studio, as it uses the NuGet module's version of Install-Package.
<PackageReference Include="JsonAssertions.TUnit" Version="0.6.1" />
                    
For projects that support PackageReference, copy this XML node into the project file to reference the package.
<PackageVersion Include="JsonAssertions.TUnit" Version="0.6.1" />
                    
Directory.Packages.props
<PackageReference Include="JsonAssertions.TUnit" />
                    
Project file
For projects that support Central Package Management (CPM), copy this XML node into the solution Directory.Packages.props file to version the package.
paket add JsonAssertions.TUnit --version 0.6.1
                    
#r "nuget: JsonAssertions.TUnit, 0.6.1"
                    
#r directive can be used in F# Interactive and Polyglot Notebooks. Copy this into the interactive tool or source code of the script to reference the package.
#:package JsonAssertions.TUnit@0.6.1
                    
#:package directive can be used in C# file-based apps starting in .NET 10 preview 4. Copy this into a .cs file before any lines of code to reference the package.
#addin nuget:?package=JsonAssertions.TUnit&version=0.6.1
                    
Install as a Cake Addin
#tool nuget:?package=JsonAssertions.TUnit&version=0.6.1
                    
Install as a Cake Tool

JsonAssertions.TUnit

NuGet Downloads License: MIT .NET

Scope: Test projects only. Not intended for production code.

Part of the DotNetAssertions family of assertion extensions for TUnit.

TUnit-native JSON assertions for .NET. Fluent entry points over TUnit's Assert.That(...) pipeline for asserting against System.Text.Json documents, HTTP response bodies (including RFC 7807 ProblemDetails), and the registration state of source-generated JsonSerializerContext instances. AOT-compatible, trimmable, no runtime reflection in the assertion path.

Full documentation, design notes, and roadmap: github.com/JohnVerheij/JsonAssertions.TUnit

Status

Each path / value / shape entry point is available over a JSON string, a System.Text.Json.JsonElement, and an HttpResponseMessage (whose body is read as the JSON document). HTTP-response and AOT-context assertions target their natural receiver type.

Entry point Behavior
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; the int / uint / long / ulong overloads match a number or a numeric string, so a string-encoded 64-bit integer such as Protobuf int64 / uint64 matches too).
HasJsonValueOneOf(path, T[]) Asserts the value at path is one of the given values (string[], double[], int[], uint[], long[], or ulong[]).
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.
IsEquivalentJsonTo(expected) / IsEquivalentJsonTo(expected, configure) over a JSON string or JsonElement Asserts the whole document is structurally equivalent to expected, independent of property order and number form (1 == 1.0). The configure callback sets IgnorePath and IgnoreArrayOrder.
ContainsJson(expectedSubset) / ContainsJson(expectedSubset, configure) over a JSON string, JsonElement, or HttpResponseMessage (v0.6.0+) Asserts the document contains the expected subset (extra actual properties ignored); the failure lists every missing or mismatched path. Same options as IsEquivalentJsonTo.
GetJsonValue<T>(path) / GetJsonString(path) / GetJsonElement(path) over a JSON string or JsonElement (and ...Async on HttpResponseMessage) (v0.6.0+) Reads a typed value out at path and returns it (not an assertion), for driving later requests or cross-checks; throws JsonExtractionException with path context on failure.
HasJsonResponse<T>(status, JsonTypeInfo<T>, T expected, ct) on HttpResponseMessage Asserts status + AOT-clean deserialization + structural equality in one chain.
MatchesProblemDetails(status, ..., ct) on HttpResponseMessage Asserts an RFC 7807 application/problem+json response with matching fields.
MatchesValidationProblemDetails(status, errors, ..., ct) on HttpResponseMessage Like MatchesProblemDetails plus the ASP.NET Core errors dictionary.
RoundtripsCleanlyVia<T>(JsonTypeInfo<T>) on any T Asserts serialize → deserialize → re-serialize is byte-identical via the supplied source-generated JsonTypeInfo<T>.
AsJsonContext().HasJsonTypeInfoFor<T>() on a JsonSerializerContext-typed source Asserts the supplied source-generated context registers a JsonTypeInfo<T> for T.
JsonRenderers.ReformatJson<T>(JsonTypeInfo<T>) (static factory) Returns Func<string, string> that canonicalizes a JSON string via the consumer's JsonSerializerContext; composes with SnapshotAssertions.TUnit's MatchesSnapshot(Func<>) at the consumer's call site.
JsonCanonicalizer.Canonicalize(json, opts) (static, v0.4.0+) Typeless structural canonical form (sorted keys, stable indent, all fields preserved) with [*]-aware ScrubPath scrubbing of volatile values; for pinning a whole response shape as a snapshot. Needs no JsonSerializerContext (unlike ReformatJson<T>) and keeps unknown fields.

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].entries[1].id, $[0] for a root-array first element. The [*] wildcard (v0.4.0+) matches every element of an array on HasJsonProperty and HasJsonValueMatching (items[*].id). See the path-syntax notes on GitHub for the full grammar.

Two notes on [*]. It is a "for all" quantifier, so [*].id passes vacuously on an empty array, whereas [0].id fails on one; a naive [0] to [*] migration silently drops the implicit non-emptiness check, so pair the wildcard with HasNonEmptyJsonArray("items") when emptiness should fail the test. And [*] fits existence and genuinely-uniform value checks only: an element-specific check (for example "the element at index 2 has id 2", HasJsonValue("items[2].id", 2)) must stay index-scoped rather than become [*].

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.58.0 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);

Each integer overload (int / uint / long / ulong) matches the value whether the JSON encodes it as a number or a numeric string; long / ulong exist because protobuf serializes 64-bit ints as JSON strings (to avoid 53-bit precision loss) while System.Text.Json writes them as numbers. double matches a JSON number only. Use the L / UL suffix to assert a 64-bit value (HasJsonValue("guid.high", 123456789012345L)).

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.

Subset matching (ContainsJson, 0.6.0+)

One declarative assertion in place of a block of per-property checks. The expected fields must be present and equal; the response may carry any additional fields the test does not want to pin. The failure lists every wrong path, not just the first.

await Assert.That(response).ContainsJson("""
    { "status": "Healthy",
      "entries": [ { "name": "db", "status": "Healthy" } ] }
    """, ct);

Do not fold a volatile value into the expected subset. ContainsJson asserts equality for every field you put in it - "subset" constrains which fields are checked, not how strictly they are compared. A duration reading, a server-generated identifier, or a status that legitimately varies gets pinned to whatever the response said the day the test was written, which asserts more than the contract does and fails on the first run that differs. Collapsing a block of per-property checks feels mechanical, so a volatile field is easy to sweep in with the stable ones, turning a presence check into an equality check. Keep those fields on their own presence or predicate assertion, or exclude them with o => o.IgnorePath("...").

Namespaces

The package places its own types in two namespaces, the same shape as the rest of the assertion family, and the source generator emits the fluent entry points into TUnit's:

Type Namespace Auto-imported?
JsonPath, JsonPathResolution, JsonValueComparison, JsonShape, JsonRenderers, JsonFailureMessage (framework-agnostic core) JsonAssertions No (needs using JsonAssertions;)
Typed extraction (GetJsonValue<T>(), GetJsonString(), GetJsonElement() and the ...Async forms) JsonAssertions No (needs using JsonAssertions;)
IJsonContextAssertionSource + the AsJsonContext() bridge (the AOT-context assertions) JsonAssertions.TUnit No (needs using JsonAssertions.TUnit; when chaining .AsJsonContext())
Source-generated assertion entry points TUnit.Assertions.Extensions Yes (TUnit auto-imports)

Only the source-generated entry points auto-import. The other three rows need two using directives between them (using JsonAssertions; covers both of its rows), which a single GlobalUsings.cs puts in front of every test file:

global using JsonAssertions;       // core types + GetJsonValue<T>()
global using JsonAssertions.TUnit; // .AsJsonContext()

The extraction methods are the one part of the package that does not hang off Assert.That(...): they extend the receiver, because they return a value rather than asserting one.

using JsonAssertions;   // the extraction methods live here, and do not auto-import

// RIGHT: the receiver, not the assertion
var orderId = await response.GetJsonValueAsync<int>("orderId", ct);

// WRONG: ValueAssertion<HttpResponseMessage> has no such method
// await Assert.That(response).GetJsonValueAsync<int>("orderId", ct);

Roadmap

  • Semantic JSON equality already ships as IsEquivalentJsonTo (0.5.0); subset matching (ContainsJson) and typed extraction (GetJsonValue<T> / GetJsonString / GetJsonElement) in 0.6.0. Further increments are demand-driven.

Family

Part of an assertion family for TUnit:

License

MIT. Copyright (c) 2026 John Verheij.

Product 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. 
Compatible target framework(s)
Included target framework(s) (in package)
Learn more about Target Frameworks and .NET Standard.

NuGet packages

This package is not used by any NuGet packages.

GitHub repositories

This package is not used by any popular GitHub repositories.

Version Downloads Last Updated
0.6.1 828 7/12/2026
0.6.0 1,435 7/11/2026
0.5.0 408 6/14/2026
0.4.2 409 6/5/2026
0.4.1 249 6/4/2026
0.4.0 161 6/3/2026
0.2.0 296 5/15/2026
0.1.0 155 5/14/2026
0.0.1 145 5/14/2026

View the rendered release notes: https://github.com/JohnVerheij/JsonAssertions.TUnit/releases/tag/v0.6.1

Patch release. Documentation only; no API or behavioral change. Everything here comes from migrating a real suite onto 0.6.0, where the two new features landed cleanly but three shapes in the surface misled a reader who was pattern-matching from the rest of the package.

### Changed

- **`ContainsJson`** documentation now carries an explicit warning against folding a volatile value into the expected subset. `ContainsJson` asserts *equality* for every field placed in it: "subset" constrains which fields are checked, not how strictly they are compared. Pinning a duration reading, a server-generated identifier, or a legitimately-varying status therefore asserts more than the contract does, and the test fails on the first run that differs. This is the failure mode a migration onto 0.6.0 walks into, because collapsing a block of per-property checks feels mechanical enough to sweep a volatile field in with the stable ones, turning a presence check into an equality check.
- **`GetJsonValue<T>` / `GetJsonString` / `GetJsonElement`** are now documented as extending the **receiver** rather than the assertion chain, and as living in the `JsonAssertions` namespace, so they need `using JsonAssertions;`. Every other feature auto-imports through `TUnit.Assertions.Extensions`, which trains a reader to reach for `Assert.That(response).GetJsonValueAsync<T>(...)` first: that does not compile, and the resulting `CS1061` never names the missing namespace. The namespace table, the entry-point list, and the cookbook pattern now all show the receiver form.
- **`HasJsonArrayLength`** and the other shape and value assertions now document that only the `HttpResponseMessage` forms take a `CancellationToken` (it flows to the body read; a `string` or `JsonElement` source has nothing to cancel). Passing one on a `JsonElement` reports `CS1929` naming `HttpResponseMessage` as the required receiver, which points at the receiver when the fix is to drop the token.
- **`HasJsonTypeInfoFor<T>`** is now documented as the one assertion in the package that guards a failure class the rest of a suite can miss: a type absent from the `JsonSerializerContext` is not a compile error, and with a reflection fallback in the resolver chain a JIT test run serializes it happily while the published Native-AOT binary throws at runtime.
- **`MatchesProblemDetails`** now documents that it asserts the media type as well as the fields, so a separate content-type test becomes redundant on migration (which a strict analyzer set will flag as duplicate method bodies).