Skip to content

JSON Assertions

Axiom.Json is the optional Axiom package for deterministic JSON assertions.

It focuses on:

  • structural JSON equivalency
  • simple JSON path existence checks
  • simple scalar value-at-path checks
  • simple object and array shape checks at a path

It does not add HTTP helpers, ASP.NET helpers, direct Newtonsoft.Json support, or a full JSONPath engine.

If your subject is HttpResponseMessage rather than raw JSON content, use HTTP and API assertions. Axiom.Http reuses Axiom.Json internally for response-body JSON checks.

Install

dotnet add package Axiom.Json

Use it alongside Axiom.Assertions:

using Axiom.Assertions;
using Axiom.Json;

Supported Inputs

The package supports:

  • raw JSON string
  • JsonDocument
  • JsonElement

Examples:

using System.Text.Json;
using Axiom.Assertions;
using Axiom.Json;

var actualJson = """
    { "id": 1, "name": "Bob", "roles": ["admin", "author"] }
    """;
var expectedJson = """
    { "roles": ["admin", "author"], "name": "Bob", "id": 1.0 }
    """;

actualJson.Should().BeJsonEquivalentTo(expectedJson);

using var document = JsonDocument.Parse(actualJson);
document.Should().HaveJsonPath("$.roles[1]");
document.RootElement.Should().HaveJsonStringAtPath("$.name", "Bob");

Structural Equivalency

Use BeJsonEquivalentTo(...) and NotBeJsonEquivalentTo(...) when you want JSON-aware structural comparison instead of plain string equality.

using Axiom.Assertions;
using Axiom.Json;

var actualJson = """
    {
      "customer": {
        "id": 7,
        "active": true,
        "roles": ["admin", "author"]
      }
    }
    """;

var expectedJson = """
    {
      "customer": {
        "roles": ["admin", "author"],
        "active": true,
        "id": 7.0
      }
    }
    """;

actualJson.Should().BeJsonEquivalentTo(expectedJson);
actualJson.Should().NotBeJsonEquivalentTo("""
    { "customer": { "id": 9, "active": true, "roles": ["admin", "author"] } }
    """);

Current semantics:

  • object property order does not matter
  • array order does matter
  • missing properties and extra properties fail explicitly
  • value-kind mismatches fail explicitly
  • numeric comparison uses normalized JSON numeric values, so 1, 1.0, and 1e0 are treated as equivalent

JSON Path Assertions

Axiom.Json uses a deliberately small path syntax:

  • optional root marker: $
  • object-member traversal with .
  • array-index traversal with [index]

Examples:

  • $.customer.id
  • $.customer.roles[0]
  • items[1].sku

This is not a full JSONPath implementation. Property names that need escaping are out of scope for the current path model.

using Axiom.Assertions;
using Axiom.Json;

var json = """
    {
      "customer": {
        "id": 7,
        "name": "Bob",
        "active": true,
        "roles": ["admin", "author"],
        "deletedAt": null
      }
    }
    """;

json.Should().HaveJsonPath("$.customer.roles[1]");
json.Should().NotHaveJsonPath("$.customer.email");
json.Should().HaveJsonObjectAtPath("$.customer");
json.Should().HaveJsonArrayAtPath("$.customer.roles");
json.Should().HaveJsonArrayLengthAtPath("$.customer.roles", 2);
json.Should().HaveJsonPropertyCountAtPath("$.customer", 5);
json.Should().HaveJsonStringAtPath("$.customer.name", "Bob");
json.Should().HaveJsonNumberAtPath("$.customer.id", 7m);
json.Should().HaveJsonBooleanAtPath("$.customer.active", true);
json.Should().HaveJsonNullAtPath("$.customer.deletedAt");

Use HaveJsonObjectAtPath(...), HaveJsonArrayAtPath(...), and the count checks when the shape of the JSON at a path matters before checking scalar values inside it.

Contract Assertions

Use contract assertions when the JSON shape matters but full schema validation would be too much. These checks stay method-based and use the same simple path syntax as the rest of Axiom.Json.

using Axiom.Assertions;
using Axiom.Json;

var responseJson = """
    {
      "id": "evt_123",
      "type": "order.created",
      "status": "queued",
      "customer": {
        "id": "cus_123",
        "name": "Bob"
      },
      "items": [
        { "id": "ord_1", "status": "queued" },
        { "id": "ord_2", "status": "processing" }
      ],
      "statuses": ["queued", "processing"]
    }
    """;

responseJson.Should().BeValidJson();
responseJson.Should().HaveJsonProperties("id", "type", "status", "customer", "items");
responseJson.Should().HaveJsonPropertiesAtPath("$.customer", "id", "name");
responseJson.Should().HaveAllowedValueAtPath("$.status", "queued", "processing", "complete");
responseJson.Should().HaveJsonObjectItemsWithPropertiesAtPath("$.items", "id", "status");
responseJson.Should().HaveJsonObjectItemsWithOnlyPropertiesAtPath("$.items", "id", "status");
responseJson.Should().HaveAllowedValuesAtPath("$.statuses", "queued", "processing", "complete");

For shared allowed-value sets, pass a collection:

using Axiom.Assertions;
using Axiom.Json;

var responseJson = """{ "status": "queued" }""";
var allowedStatuses = new[] { "queued", "processing", "complete" };
responseJson.Should().HaveAllowedValueAtPath("$.status", allowedStatuses);

Invalid JSON

For raw JSON string subjects, invalid JSON fails clearly with an invalid JSON parse location in the failure message.

Invalid expected JSON passed as a raw string is treated as an invalid assertion argument and throws ArgumentException.

Current Limits

Axiom.Json is intentionally narrow:

  • System.Text.Json inputs plus raw JSON strings only
  • no direct Newtonsoft.Json support
  • no HTTP or API-response helpers in this package; use Axiom.Http for HttpResponseMessage
  • no full JSONPath language
  • no wildcard selection; array-wide checks operate on paths that resolve to arrays, such as $.items
  • array order is significant

For the full method list, see the Assertion Reference.