From 699cea25fc0a075f6dafec9588a565f550d0cd68 Mon Sep 17 00:00:00 2001
From: Tom Longhurst <30480171+thomhurst@users.noreply.github.com>
Date: Sun, 28 Dec 2025 11:51:06 +0000
Subject: [PATCH 01/10] feat(assertions): add JsonDiffHelper for
path-to-difference error messages
---
.../Conditions/Json/JsonDiffHelper.cs | 133 ++++++++++++++++++
TUnit.Assertions/TUnit.Assertions.csproj | 1 +
2 files changed, 134 insertions(+)
create mode 100644 TUnit.Assertions/Conditions/Json/JsonDiffHelper.cs
diff --git a/TUnit.Assertions/Conditions/Json/JsonDiffHelper.cs b/TUnit.Assertions/Conditions/Json/JsonDiffHelper.cs
new file mode 100644
index 0000000000..6816e7ffd9
--- /dev/null
+++ b/TUnit.Assertions/Conditions/Json/JsonDiffHelper.cs
@@ -0,0 +1,133 @@
+using System.Text.Json;
+
+namespace TUnit.Assertions.Conditions.Json;
+
+///
+/// Helper class for comparing JSON elements and identifying differences.
+///
+internal static class JsonDiffHelper
+{
+ ///
+ /// Represents the result of a JSON comparison, including the path where a difference was found
+ /// and the expected and actual values at that location.
+ ///
+ /// The JSON path where the difference was found (e.g., "$.person.name").
+ /// The expected value at this path.
+ /// The actual value at this path.
+ /// Whether a difference was found. Defaults to true.
+ public readonly record struct DiffResult(string Path, string Expected, string Actual, bool HasDifference = true);
+
+ ///
+ /// Finds the first difference between two JSON elements.
+ ///
+ /// The actual JSON element to compare.
+ /// The expected JSON element to compare against.
+ /// A containing information about the first difference found,
+ /// or a result with set to false if the elements are identical.
+ public static DiffResult FindFirstDifference(JsonElement actual, JsonElement expected)
+ {
+ return FindDiff(actual, expected, "$");
+ }
+
+ private static DiffResult FindDiff(JsonElement actual, JsonElement expected, string path)
+ {
+ if (actual.ValueKind != expected.ValueKind)
+ {
+ return new DiffResult(path, expected.ValueKind.ToString(), actual.ValueKind.ToString());
+ }
+
+ return actual.ValueKind switch
+ {
+ JsonValueKind.Object => CompareObjects(actual, expected, path),
+ JsonValueKind.Array => CompareArrays(actual, expected, path),
+ _ => ComparePrimitives(actual, expected, path)
+ };
+ }
+
+ private static DiffResult CompareObjects(JsonElement actual, JsonElement expected, string path)
+ {
+ // Check for missing properties in actual that exist in expected
+ foreach (var prop in expected.EnumerateObject())
+ {
+ var propPath = $"{path}.{prop.Name}";
+ if (!actual.TryGetProperty(prop.Name, out var actualProp))
+ {
+ return new DiffResult(propPath, FormatValue(prop.Value), "(missing)");
+ }
+
+ var diff = FindDiff(actualProp, prop.Value, propPath);
+ if (diff.HasDifference)
+ {
+ return diff;
+ }
+ }
+
+ // Check for extra properties in actual that don't exist in expected
+ foreach (var prop in actual.EnumerateObject())
+ {
+ var propPath = $"{path}.{prop.Name}";
+ if (!expected.TryGetProperty(prop.Name, out _))
+ {
+ return new DiffResult(propPath, "(missing)", FormatValue(prop.Value));
+ }
+ }
+
+ return new DiffResult(path, "", "", HasDifference: false);
+ }
+
+ private static DiffResult CompareArrays(JsonElement actual, JsonElement expected, string path)
+ {
+ var actualLength = actual.GetArrayLength();
+ var expectedLength = expected.GetArrayLength();
+
+ if (actualLength != expectedLength)
+ {
+ return new DiffResult($"{path}.Length", expectedLength.ToString(), actualLength.ToString());
+ }
+
+ var actualEnumerator = actual.EnumerateArray();
+ var expectedEnumerator = expected.EnumerateArray();
+ var index = 0;
+
+ while (actualEnumerator.MoveNext() && expectedEnumerator.MoveNext())
+ {
+ var itemPath = $"{path}[{index}]";
+ var diff = FindDiff(actualEnumerator.Current, expectedEnumerator.Current, itemPath);
+ if (diff.HasDifference)
+ {
+ return diff;
+ }
+ index++;
+ }
+
+ return new DiffResult(path, "", "", HasDifference: false);
+ }
+
+ private static DiffResult ComparePrimitives(JsonElement actual, JsonElement expected, string path)
+ {
+ var actualText = FormatValue(actual);
+ var expectedText = FormatValue(expected);
+
+ if (actualText != expectedText)
+ {
+ return new DiffResult(path, expectedText, actualText);
+ }
+
+ return new DiffResult(path, "", "", HasDifference: false);
+ }
+
+ private static string FormatValue(JsonElement element)
+ {
+ return element.ValueKind switch
+ {
+ JsonValueKind.String => $"\"{element.GetString()}\"",
+ JsonValueKind.Number => element.GetRawText(),
+ JsonValueKind.True => "true",
+ JsonValueKind.False => "false",
+ JsonValueKind.Null => "null",
+ JsonValueKind.Object => "{...}",
+ JsonValueKind.Array => "[...]",
+ _ => element.GetRawText()
+ };
+ }
+}
diff --git a/TUnit.Assertions/TUnit.Assertions.csproj b/TUnit.Assertions/TUnit.Assertions.csproj
index faebe00897..e2fb098ee5 100644
--- a/TUnit.Assertions/TUnit.Assertions.csproj
+++ b/TUnit.Assertions/TUnit.Assertions.csproj
@@ -54,6 +54,7 @@
+
From 876a6c0b40dafe400ba69b7ef84a598ac14f3278 Mon Sep 17 00:00:00 2001
From: Tom Longhurst <30480171+thomhurst@users.noreply.github.com>
Date: Sun, 28 Dec 2025 12:10:29 +0000
Subject: [PATCH 02/10] feat(assertions): add JsonElement type checking
assertions
---
.../JsonElementAssertionTests.cs | 112 ++
.../Json/JsonElementAssertionExtensions.cs | 38 +
.../2025-12-28-json-assertions-design.md | 230 ++++
docs/plans/2025-12-28-json-assertions.md | 991 ++++++++++++++++++
4 files changed, 1371 insertions(+)
create mode 100644 TUnit.Assertions.Tests/JsonElementAssertionTests.cs
create mode 100644 TUnit.Assertions/Conditions/Json/JsonElementAssertionExtensions.cs
create mode 100644 docs/plans/2025-12-28-json-assertions-design.md
create mode 100644 docs/plans/2025-12-28-json-assertions.md
diff --git a/TUnit.Assertions.Tests/JsonElementAssertionTests.cs b/TUnit.Assertions.Tests/JsonElementAssertionTests.cs
new file mode 100644
index 0000000000..871873c112
--- /dev/null
+++ b/TUnit.Assertions.Tests/JsonElementAssertionTests.cs
@@ -0,0 +1,112 @@
+using System.Text.Json;
+using TUnit.Assertions.Extensions;
+
+namespace TUnit.Assertions.Tests;
+
+public class JsonElementAssertionTests
+{
+ [Test]
+ public async Task IsObject_WithObject_Passes()
+ {
+ using var doc = JsonDocument.Parse("{\"name\":\"test\"}");
+ await Assert.That(doc.RootElement).IsObject();
+ }
+
+ [Test]
+ public async Task IsObject_WithArray_Fails()
+ {
+ using var doc = JsonDocument.Parse("[1,2,3]");
+ await Assert.ThrowsAsync(
+ async () => await Assert.That(doc.RootElement).IsObject());
+ }
+
+ [Test]
+ public async Task IsArray_WithArray_Passes()
+ {
+ using var doc = JsonDocument.Parse("[1,2,3]");
+ await Assert.That(doc.RootElement).IsArray();
+ }
+
+ [Test]
+ public async Task IsArray_WithNonArray_Fails()
+ {
+ using var doc = JsonDocument.Parse("{\"key\":\"value\"}");
+ await Assert.ThrowsAsync(
+ async () => await Assert.That(doc.RootElement).IsArray());
+ }
+
+ [Test]
+ public async Task IsString_WithString_Passes()
+ {
+ using var doc = JsonDocument.Parse("\"hello\"");
+ await Assert.That(doc.RootElement).IsString();
+ }
+
+ [Test]
+ public async Task IsString_WithNonString_Fails()
+ {
+ using var doc = JsonDocument.Parse("42");
+ await Assert.ThrowsAsync(
+ async () => await Assert.That(doc.RootElement).IsString());
+ }
+
+ [Test]
+ public async Task IsNumber_WithNumber_Passes()
+ {
+ using var doc = JsonDocument.Parse("42");
+ await Assert.That(doc.RootElement).IsNumber();
+ }
+
+ [Test]
+ public async Task IsNumber_WithNonNumber_Fails()
+ {
+ using var doc = JsonDocument.Parse("\"text\"");
+ await Assert.ThrowsAsync(
+ async () => await Assert.That(doc.RootElement).IsNumber());
+ }
+
+ [Test]
+ public async Task IsBoolean_WithTrue_Passes()
+ {
+ using var doc = JsonDocument.Parse("true");
+ await Assert.That(doc.RootElement).IsBoolean();
+ }
+
+ [Test]
+ public async Task IsBoolean_WithNonBoolean_Fails()
+ {
+ using var doc = JsonDocument.Parse("null");
+ await Assert.ThrowsAsync(
+ async () => await Assert.That(doc.RootElement).IsBoolean());
+ }
+
+ [Test]
+ public async Task IsNull_WithNull_Passes()
+ {
+ using var doc = JsonDocument.Parse("null");
+ await Assert.That(doc.RootElement).IsNull();
+ }
+
+ [Test]
+ public async Task IsNull_WithNonNull_Fails()
+ {
+ using var doc = JsonDocument.Parse("{}");
+ await Assert.ThrowsAsync(
+ async () => await Assert.That(doc.RootElement).IsNull());
+ }
+
+ [Test]
+ public async Task IsNotNull_WithObject_Passes()
+ {
+ using var doc = JsonDocument.Parse("{}");
+ await Assert.That(doc.RootElement).IsNotNull();
+ }
+
+ [Test]
+ public async Task IsNotNull_WithNull_Fails()
+ {
+ using var doc = JsonDocument.Parse("null");
+ await Assert.ThrowsAsync(
+ async () => await Assert.That(doc.RootElement).IsNotNull());
+ }
+}
diff --git a/TUnit.Assertions/Conditions/Json/JsonElementAssertionExtensions.cs b/TUnit.Assertions/Conditions/Json/JsonElementAssertionExtensions.cs
new file mode 100644
index 0000000000..6b4ba1d018
--- /dev/null
+++ b/TUnit.Assertions/Conditions/Json/JsonElementAssertionExtensions.cs
@@ -0,0 +1,38 @@
+using System.Text.Json;
+using TUnit.Assertions.Attributes;
+
+namespace TUnit.Assertions.Conditions.Json;
+
+///
+/// Source-generated assertions for JsonElement type checking.
+///
+file static partial class JsonElementAssertionExtensions
+{
+ [GenerateAssertion(ExpectationMessage = "to be a JSON object", InlineMethodBody = true)]
+ public static bool IsObject(this JsonElement value)
+ => value.ValueKind == JsonValueKind.Object;
+
+ [GenerateAssertion(ExpectationMessage = "to be a JSON array", InlineMethodBody = true)]
+ public static bool IsArray(this JsonElement value)
+ => value.ValueKind == JsonValueKind.Array;
+
+ [GenerateAssertion(ExpectationMessage = "to be a JSON string", InlineMethodBody = true)]
+ public static bool IsString(this JsonElement value)
+ => value.ValueKind == JsonValueKind.String;
+
+ [GenerateAssertion(ExpectationMessage = "to be a JSON number", InlineMethodBody = true)]
+ public static bool IsNumber(this JsonElement value)
+ => value.ValueKind == JsonValueKind.Number;
+
+ [GenerateAssertion(ExpectationMessage = "to be a JSON boolean", InlineMethodBody = true)]
+ public static bool IsBoolean(this JsonElement value)
+ => value.ValueKind == JsonValueKind.True || value.ValueKind == JsonValueKind.False;
+
+ [GenerateAssertion(ExpectationMessage = "to be JSON null", InlineMethodBody = true)]
+ public static bool IsNull(this JsonElement value)
+ => value.ValueKind == JsonValueKind.Null;
+
+ [GenerateAssertion(ExpectationMessage = "to not be JSON null", InlineMethodBody = true)]
+ public static bool IsNotNull(this JsonElement value)
+ => value.ValueKind != JsonValueKind.Null;
+}
diff --git a/docs/plans/2025-12-28-json-assertions-design.md b/docs/plans/2025-12-28-json-assertions-design.md
new file mode 100644
index 0000000000..40f3b62377
--- /dev/null
+++ b/docs/plans/2025-12-28-json-assertions-design.md
@@ -0,0 +1,230 @@
+# JSON Assertions Design
+
+**Issue**: [#4178 - JsonElement assertions](https://github.com/thomhurst/TUnit/issues/4178)
+**Date**: 2025-12-28
+**Status**: Approved Design
+
+## Summary
+
+Add comprehensive JSON assertions to TUnit supporting both `JsonElement` and `JsonNode` type hierarchies, with runtime-appropriate features via conditional compilation.
+
+## Problem Statement
+
+Currently, `Assert.That(json1).IsEqualTo(json2)` performs string comparison on JSON, which:
+- Fails on semantically equivalent JSON with different whitespace
+- Provides unhelpful error messages that don't indicate where differences occur
+
+Users need:
+- Semantic JSON comparison (ignoring formatting)
+- Detailed error messages with paths to differences (e.g., "differs at $.abc.def")
+
+## Design Decisions
+
+| Decision | Choice | Rationale |
+|----------|--------|-----------|
+| Target Types | Both JsonElement and JsonNode | Complete coverage for all System.Text.Json users |
+| Equality Logic | Built-in `DeepEquals` | Leverage .NET's tested implementation |
+| Runtime Support | Per-runtime via `#if` | Provide what's available without polyfills |
+| Assertion Categories | Equality, Validity, Property, Type, Array | Comprehensive without external dependencies |
+| Path Queries | Deferred | JSONPath not built-in, avoid dependencies |
+| Error Messages | Path-to-difference | Addresses core issue request |
+| API Entry Point | `Assert.That()` extensions | Consistent with TUnit patterns |
+| Package Location | Inside TUnit.Assertions | No extra package for users |
+| Implementation | `[GenerateAssertion]` | Simplified code, source generator handles infrastructure |
+
+## Runtime Availability Matrix
+
+| Assertion | .NET 6-7 | .NET 8 | .NET 9+ |
+|-----------|----------|--------|---------|
+| `IsEqualTo` (JsonNode) | - | Y | Y |
+| `IsEqualTo` (JsonElement) | - | - | Y |
+| `IsValidJson` (string) | Y | Y | Y |
+| `HasProperty` | Y | Y | Y |
+| Type checks (`IsObject`, etc.) | Y | Y | Y |
+| Array assertions | Y | Y | Y |
+
+## File Structure
+
+```
+TUnit.Assertions/Conditions/Json/
+├── JsonElementAssertionExtensions.cs
+├── JsonNodeAssertionExtensions.cs
+├── JsonStringAssertionExtensions.cs
+└── JsonDiffHelper.cs
+```
+
+## Assertion Inventory
+
+### JsonElement Assertions
+
+```csharp
+// Type checking (all runtimes)
+IsObject()
+IsArray()
+IsString()
+IsNumber()
+IsBoolean()
+IsNull()
+IsNotNull()
+
+// Property access (all runtimes)
+HasProperty(string propertyName)
+DoesNotHaveProperty(string propertyName)
+
+// Equality (.NET 9+ only)
+#if NET9_0_OR_GREATER
+IsEqualTo(JsonElement expected)
+IsNotEqualTo(JsonElement expected)
+#endif
+```
+
+### JsonNode Assertions
+
+```csharp
+// Type checking (all runtimes)
+IsObject()
+IsArray()
+IsValue()
+
+// Property access (all runtimes)
+HasProperty(string propertyName)
+DoesNotHaveProperty(string propertyName)
+
+// Equality (.NET 8+ only)
+#if NET8_0_OR_GREATER
+IsEqualTo(JsonNode? expected)
+IsNotEqualTo(JsonNode? expected)
+#endif
+```
+
+### JsonArray Assertions
+
+```csharp
+// All runtimes
+IsEmpty()
+IsNotEmpty()
+HasCount(int expected)
+```
+
+### String (Raw JSON) Assertions
+
+```csharp
+// All runtimes
+IsValidJson()
+IsNotValidJson()
+IsValidJsonObject()
+IsValidJsonArray()
+```
+
+## Implementation Pattern
+
+Using `[GenerateAssertion]` for simplified code:
+
+```csharp
+using System.Text.Json;
+using TUnit.Assertions.Attributes;
+using TUnit.Assertions.Core;
+
+namespace TUnit.Assertions.Conditions.Json;
+
+file static partial class JsonElementAssertionExtensions
+{
+ [GenerateAssertion(ExpectationMessage = "to be a JSON object", InlineMethodBody = true)]
+ public static bool IsObject(this JsonElement value)
+ => value.ValueKind == JsonValueKind.Object;
+
+ [GenerateAssertion(ExpectationMessage = "to have property \"{propertyName}\"")]
+ public static bool HasProperty(this JsonElement value, string propertyName)
+ => value.TryGetProperty(propertyName, out _);
+
+#if NET9_0_OR_GREATER
+ [GenerateAssertion(ExpectationMessage = "to be equal to {expected}")]
+ public static AssertionResult IsEqualTo(this JsonElement value, JsonElement expected)
+ {
+ if (JsonElement.DeepEquals(value, expected))
+ return AssertionResult.Passed;
+
+ var diff = JsonDiffHelper.FindFirstDifference(value, expected);
+ return AssertionResult.Failed(
+ $"differs at {diff.Path}: expected {diff.Expected} but found {diff.Actual}");
+ }
+#endif
+}
+```
+
+## JsonDiffHelper
+
+Provides path-to-difference for error messages:
+
+```csharp
+internal static class JsonDiffHelper
+{
+ public readonly record struct DiffResult(string Path, string Expected, string Actual);
+
+ public static DiffResult FindFirstDifference(JsonElement left, JsonElement right)
+ {
+ return FindDiff(left, right, "$");
+ }
+
+ private static DiffResult FindDiff(JsonElement left, JsonElement right, string path)
+ {
+ if (left.ValueKind != right.ValueKind)
+ return new DiffResult(path, right.ValueKind.ToString(), left.ValueKind.ToString());
+
+ return left.ValueKind switch
+ {
+ JsonValueKind.Object => CompareObjects(left, right, path),
+ JsonValueKind.Array => CompareArrays(left, right, path),
+ _ => ComparePrimitives(left, right, path)
+ };
+ }
+ // ... implementation details
+}
+```
+
+## Error Message Examples
+
+```
+Expected JSON to be equal to {"name":"Alice","age":31}
+but differs at $.age: expected 31 but found 30
+
+Expected JSON to be equal to {"users":[{"id":1}]}
+but differs at $.users[0].id: expected 1 but found 2
+
+Expected JSON to be equal to {"active":true}
+but differs at $.active: expected True but found False
+```
+
+## Usage Examples
+
+```csharp
+// Type checking
+await Assert.That(element).IsObject();
+await Assert.That(node).IsArray();
+
+// Property access
+await Assert.That(element).HasProperty("name");
+await Assert.That(element).DoesNotHaveProperty("deleted");
+
+// Equality (.NET 8+/9+)
+await Assert.That(element).IsEqualTo(expectedElement);
+await Assert.That(node).IsEqualTo(expectedNode);
+
+// String validation
+await Assert.That(jsonString).IsValidJson();
+await Assert.That(jsonString).IsValidJsonObject();
+```
+
+## Testing Strategy
+
+- Unit tests in `TUnit.Assertions.Tests` with `#if` for runtime-specific tests
+- Test both pass and fail scenarios for each assertion
+- Verify error message format includes correct JSON paths
+- Multi-target test project to validate behavior on each runtime
+
+## Future Considerations
+
+- JSONPath support via optional dependency or built-in simple path syntax
+- `HasPropertyWithValue(name, value)` combined assertion
+- JSON Schema validation
+- Partial matching / subset assertions
diff --git a/docs/plans/2025-12-28-json-assertions.md b/docs/plans/2025-12-28-json-assertions.md
new file mode 100644
index 0000000000..7c728655a2
--- /dev/null
+++ b/docs/plans/2025-12-28-json-assertions.md
@@ -0,0 +1,991 @@
+# JSON Assertions Implementation Plan
+
+> **For Claude:** REQUIRED SUB-SKILL: Use superpowers:executing-plans to implement this plan task-by-task.
+
+**Goal:** Add comprehensive JSON assertions for JsonElement, JsonNode, and string types with semantic equality and path-to-difference error messages.
+
+**Architecture:** Use `[GenerateAssertion]` attribute pattern for all assertions. Conditional compilation (`#if NET8_0_OR_GREATER`, `#if NET9_0_OR_GREATER`) enables runtime-specific features. JsonDiffHelper provides detailed error messages showing where JSON structures differ.
+
+**Tech Stack:** System.Text.Json (built-in), TUnit.Assertions source generator, C# 12
+
+---
+
+## Task 1: Create JsonDiffHelper
+
+**Files:**
+- Create: `TUnit.Assertions/Conditions/Json/JsonDiffHelper.cs`
+
+**Step 1: Create the Json directory**
+
+```bash
+mkdir TUnit.Assertions/Conditions/Json
+```
+
+**Step 2: Write the JsonDiffHelper**
+
+Create `TUnit.Assertions/Conditions/Json/JsonDiffHelper.cs`:
+
+```csharp
+using System.Text.Json;
+
+namespace TUnit.Assertions.Conditions.Json;
+
+internal static class JsonDiffHelper
+{
+ public readonly record struct DiffResult(string Path, string Expected, string Actual);
+
+ public static DiffResult FindFirstDifference(JsonElement left, JsonElement right)
+ {
+ return FindDiff(left, right, "$");
+ }
+
+ private static DiffResult FindDiff(JsonElement left, JsonElement right, string path)
+ {
+ if (left.ValueKind != right.ValueKind)
+ {
+ return new DiffResult(path, right.ValueKind.ToString(), left.ValueKind.ToString());
+ }
+
+ return left.ValueKind switch
+ {
+ JsonValueKind.Object => CompareObjects(left, right, path),
+ JsonValueKind.Array => CompareArrays(left, right, path),
+ _ => ComparePrimitives(left, right, path)
+ };
+ }
+
+ private static DiffResult CompareObjects(JsonElement left, JsonElement right, string path)
+ {
+ // Check for missing properties in left that exist in right
+ foreach (var prop in right.EnumerateObject())
+ {
+ var propPath = $"{path}.{prop.Name}";
+ if (!left.TryGetProperty(prop.Name, out var leftProp))
+ {
+ return new DiffResult(propPath, FormatValue(prop.Value), "(missing)");
+ }
+
+ var diff = FindDiff(leftProp, prop.Value, propPath);
+ if (!string.IsNullOrEmpty(diff.Expected) || !string.IsNullOrEmpty(diff.Actual))
+ {
+ return diff;
+ }
+ }
+
+ // Check for extra properties in left that don't exist in right
+ foreach (var prop in left.EnumerateObject())
+ {
+ var propPath = $"{path}.{prop.Name}";
+ if (!right.TryGetProperty(prop.Name, out _))
+ {
+ return new DiffResult(propPath, "(missing)", FormatValue(prop.Value));
+ }
+ }
+
+ return default;
+ }
+
+ private static DiffResult CompareArrays(JsonElement left, JsonElement right, string path)
+ {
+ var leftLength = left.GetArrayLength();
+ var rightLength = right.GetArrayLength();
+
+ if (leftLength != rightLength)
+ {
+ return new DiffResult($"{path}.Length", rightLength.ToString(), leftLength.ToString());
+ }
+
+ var leftEnumerator = left.EnumerateArray();
+ var rightEnumerator = right.EnumerateArray();
+ var index = 0;
+
+ while (leftEnumerator.MoveNext() && rightEnumerator.MoveNext())
+ {
+ var itemPath = $"{path}[{index}]";
+ var diff = FindDiff(leftEnumerator.Current, rightEnumerator.Current, itemPath);
+ if (!string.IsNullOrEmpty(diff.Expected) || !string.IsNullOrEmpty(diff.Actual))
+ {
+ return diff;
+ }
+ index++;
+ }
+
+ return default;
+ }
+
+ private static DiffResult ComparePrimitives(JsonElement left, JsonElement right, string path)
+ {
+ var leftText = FormatValue(left);
+ var rightText = FormatValue(right);
+
+ if (leftText != rightText)
+ {
+ return new DiffResult(path, rightText, leftText);
+ }
+
+ return default;
+ }
+
+ private static string FormatValue(JsonElement element)
+ {
+ return element.ValueKind switch
+ {
+ JsonValueKind.String => $"\"{element.GetString()}\"",
+ JsonValueKind.Number => element.GetRawText(),
+ JsonValueKind.True => "true",
+ JsonValueKind.False => "false",
+ JsonValueKind.Null => "null",
+ JsonValueKind.Object => "{...}",
+ JsonValueKind.Array => "[...]",
+ _ => element.GetRawText()
+ };
+ }
+}
+```
+
+**Step 3: Verify it compiles**
+
+```bash
+cd C:/git/TUnit && dotnet build TUnit.Assertions/TUnit.Assertions.csproj --no-restore -v q
+```
+
+Expected: Build succeeded
+
+**Step 4: Commit**
+
+```bash
+git add TUnit.Assertions/Conditions/Json/JsonDiffHelper.cs
+git commit -m "feat(assertions): add JsonDiffHelper for path-to-difference error messages"
+```
+
+---
+
+## Task 2: Create JsonElement Type Assertions
+
+**Files:**
+- Create: `TUnit.Assertions/Conditions/Json/JsonElementAssertionExtensions.cs`
+- Test: `TUnit.Assertions.Tests/JsonElementAssertionTests.cs`
+
+**Step 1: Write the failing test**
+
+Create `TUnit.Assertions.Tests/JsonElementAssertionTests.cs`:
+
+```csharp
+using System.Text.Json;
+using TUnit.Assertions.Extensions;
+
+namespace TUnit.Assertions.Tests;
+
+public class JsonElementAssertionTests
+{
+ [Test]
+ public async Task IsObject_WithObject_Passes()
+ {
+ using var doc = JsonDocument.Parse("{\"name\":\"test\"}");
+ await Assert.That(doc.RootElement).IsObject();
+ }
+
+ [Test]
+ public async Task IsObject_WithArray_Fails()
+ {
+ using var doc = JsonDocument.Parse("[1,2,3]");
+ await Assert.ThrowsAsync(
+ async () => await Assert.That(doc.RootElement).IsObject());
+ }
+
+ [Test]
+ public async Task IsArray_WithArray_Passes()
+ {
+ using var doc = JsonDocument.Parse("[1,2,3]");
+ await Assert.That(doc.RootElement).IsArray();
+ }
+
+ [Test]
+ public async Task IsString_WithString_Passes()
+ {
+ using var doc = JsonDocument.Parse("\"hello\"");
+ await Assert.That(doc.RootElement).IsString();
+ }
+
+ [Test]
+ public async Task IsNumber_WithNumber_Passes()
+ {
+ using var doc = JsonDocument.Parse("42");
+ await Assert.That(doc.RootElement).IsNumber();
+ }
+
+ [Test]
+ public async Task IsBoolean_WithTrue_Passes()
+ {
+ using var doc = JsonDocument.Parse("true");
+ await Assert.That(doc.RootElement).IsBoolean();
+ }
+
+ [Test]
+ public async Task IsNull_WithNull_Passes()
+ {
+ using var doc = JsonDocument.Parse("null");
+ await Assert.That(doc.RootElement).IsNull();
+ }
+
+ [Test]
+ public async Task IsNotNull_WithObject_Passes()
+ {
+ using var doc = JsonDocument.Parse("{}");
+ await Assert.That(doc.RootElement).IsNotNull();
+ }
+}
+```
+
+**Step 2: Run test to verify it fails**
+
+```bash
+cd C:/git/TUnit && dotnet test TUnit.Assertions.Tests/TUnit.Assertions.Tests.csproj --filter "FullyQualifiedName~JsonElementAssertionTests" --no-build
+```
+
+Expected: FAIL - IsObject method not found
+
+**Step 3: Write the JsonElement type assertions**
+
+Create `TUnit.Assertions/Conditions/Json/JsonElementAssertionExtensions.cs`:
+
+```csharp
+using System.Text.Json;
+using TUnit.Assertions.Attributes;
+
+namespace TUnit.Assertions.Conditions.Json;
+
+///
+/// Source-generated assertions for JsonElement type checking.
+///
+file static partial class JsonElementAssertionExtensions
+{
+ [GenerateAssertion(ExpectationMessage = "to be a JSON object", InlineMethodBody = true)]
+ public static bool IsObject(this JsonElement value)
+ => value.ValueKind == JsonValueKind.Object;
+
+ [GenerateAssertion(ExpectationMessage = "to be a JSON array", InlineMethodBody = true)]
+ public static bool IsArray(this JsonElement value)
+ => value.ValueKind == JsonValueKind.Array;
+
+ [GenerateAssertion(ExpectationMessage = "to be a JSON string", InlineMethodBody = true)]
+ public static bool IsString(this JsonElement value)
+ => value.ValueKind == JsonValueKind.String;
+
+ [GenerateAssertion(ExpectationMessage = "to be a JSON number", InlineMethodBody = true)]
+ public static bool IsNumber(this JsonElement value)
+ => value.ValueKind == JsonValueKind.Number;
+
+ [GenerateAssertion(ExpectationMessage = "to be a JSON boolean", InlineMethodBody = true)]
+ public static bool IsBoolean(this JsonElement value)
+ => value.ValueKind == JsonValueKind.True || value.ValueKind == JsonValueKind.False;
+
+ [GenerateAssertion(ExpectationMessage = "to be JSON null", InlineMethodBody = true)]
+ public static bool IsNull(this JsonElement value)
+ => value.ValueKind == JsonValueKind.Null;
+
+ [GenerateAssertion(ExpectationMessage = "to not be JSON null", InlineMethodBody = true)]
+ public static bool IsNotNull(this JsonElement value)
+ => value.ValueKind != JsonValueKind.Null;
+}
+```
+
+**Step 4: Build to generate extension methods**
+
+```bash
+cd C:/git/TUnit && dotnet build TUnit.Assertions/TUnit.Assertions.csproj -v q
+```
+
+Expected: Build succeeded
+
+**Step 5: Run tests to verify they pass**
+
+```bash
+cd C:/git/TUnit && dotnet test TUnit.Assertions.Tests/TUnit.Assertions.Tests.csproj --filter "FullyQualifiedName~JsonElementAssertionTests"
+```
+
+Expected: All tests pass
+
+**Step 6: Commit**
+
+```bash
+git add TUnit.Assertions/Conditions/Json/JsonElementAssertionExtensions.cs TUnit.Assertions.Tests/JsonElementAssertionTests.cs
+git commit -m "feat(assertions): add JsonElement type checking assertions"
+```
+
+---
+
+## Task 3: Add JsonElement Property Assertions
+
+**Files:**
+- Modify: `TUnit.Assertions/Conditions/Json/JsonElementAssertionExtensions.cs`
+- Modify: `TUnit.Assertions.Tests/JsonElementAssertionTests.cs`
+
+**Step 1: Write the failing tests**
+
+Add to `TUnit.Assertions.Tests/JsonElementAssertionTests.cs`:
+
+```csharp
+ [Test]
+ public async Task HasProperty_WhenPropertyExists_Passes()
+ {
+ using var doc = JsonDocument.Parse("{\"name\":\"Alice\",\"age\":30}");
+ await Assert.That(doc.RootElement).HasProperty("name");
+ }
+
+ [Test]
+ public async Task HasProperty_WhenPropertyMissing_Fails()
+ {
+ using var doc = JsonDocument.Parse("{\"name\":\"Alice\"}");
+ await Assert.ThrowsAsync(
+ async () => await Assert.That(doc.RootElement).HasProperty("missing"));
+ }
+
+ [Test]
+ public async Task DoesNotHaveProperty_WhenPropertyMissing_Passes()
+ {
+ using var doc = JsonDocument.Parse("{\"name\":\"Alice\"}");
+ await Assert.That(doc.RootElement).DoesNotHaveProperty("missing");
+ }
+
+ [Test]
+ public async Task DoesNotHaveProperty_WhenPropertyExists_Fails()
+ {
+ using var doc = JsonDocument.Parse("{\"name\":\"Alice\"}");
+ await Assert.ThrowsAsync(
+ async () => await Assert.That(doc.RootElement).DoesNotHaveProperty("name"));
+ }
+```
+
+**Step 2: Run tests to verify they fail**
+
+```bash
+cd C:/git/TUnit && dotnet test TUnit.Assertions.Tests/TUnit.Assertions.Tests.csproj --filter "FullyQualifiedName~JsonElementAssertionTests.HasProperty" --no-build
+```
+
+Expected: FAIL - HasProperty method not found
+
+**Step 3: Add property assertions**
+
+Add to `TUnit.Assertions/Conditions/Json/JsonElementAssertionExtensions.cs`:
+
+```csharp
+ [GenerateAssertion(ExpectationMessage = "to have property \"{propertyName}\"", InlineMethodBody = true)]
+ public static bool HasProperty(this JsonElement value, string propertyName)
+ => value.ValueKind == JsonValueKind.Object && value.TryGetProperty(propertyName, out _);
+
+ [GenerateAssertion(ExpectationMessage = "to not have property \"{propertyName}\"", InlineMethodBody = true)]
+ public static bool DoesNotHaveProperty(this JsonElement value, string propertyName)
+ => value.ValueKind != JsonValueKind.Object || !value.TryGetProperty(propertyName, out _);
+```
+
+**Step 4: Run tests to verify they pass**
+
+```bash
+cd C:/git/TUnit && dotnet test TUnit.Assertions.Tests/TUnit.Assertions.Tests.csproj --filter "FullyQualifiedName~JsonElementAssertionTests"
+```
+
+Expected: All tests pass
+
+**Step 5: Commit**
+
+```bash
+git add TUnit.Assertions/Conditions/Json/JsonElementAssertionExtensions.cs TUnit.Assertions.Tests/JsonElementAssertionTests.cs
+git commit -m "feat(assertions): add JsonElement property assertions"
+```
+
+---
+
+## Task 4: Add JsonElement Equality Assertions (.NET 9+)
+
+**Files:**
+- Modify: `TUnit.Assertions/Conditions/Json/JsonElementAssertionExtensions.cs`
+- Modify: `TUnit.Assertions.Tests/JsonElementAssertionTests.cs`
+
+**Step 1: Write the failing tests**
+
+Add to `TUnit.Assertions.Tests/JsonElementAssertionTests.cs`:
+
+```csharp
+#if NET9_0_OR_GREATER
+ [Test]
+ public async Task IsEqualTo_WithIdenticalJson_Passes()
+ {
+ using var doc1 = JsonDocument.Parse("{\"name\":\"Alice\",\"age\":30}");
+ using var doc2 = JsonDocument.Parse("{\"name\":\"Alice\",\"age\":30}");
+ await Assert.That(doc1.RootElement).IsEqualTo(doc2.RootElement);
+ }
+
+ [Test]
+ public async Task IsEqualTo_WithDifferentWhitespace_Passes()
+ {
+ using var doc1 = JsonDocument.Parse("{ \"name\" : \"Alice\" }");
+ using var doc2 = JsonDocument.Parse("{\"name\":\"Alice\"}");
+ await Assert.That(doc1.RootElement).IsEqualTo(doc2.RootElement);
+ }
+
+ [Test]
+ public async Task IsEqualTo_WithDifferentValues_FailsWithPath()
+ {
+ using var doc1 = JsonDocument.Parse("{\"name\":\"Alice\",\"age\":30}");
+ using var doc2 = JsonDocument.Parse("{\"name\":\"Alice\",\"age\":31}");
+
+ var exception = await Assert.ThrowsAsync(
+ async () => await Assert.That(doc1.RootElement).IsEqualTo(doc2.RootElement));
+
+ await Assert.That(exception.Message).Contains("$.age");
+ }
+
+ [Test]
+ public async Task IsNotEqualTo_WithDifferentJson_Passes()
+ {
+ using var doc1 = JsonDocument.Parse("{\"name\":\"Alice\"}");
+ using var doc2 = JsonDocument.Parse("{\"name\":\"Bob\"}");
+ await Assert.That(doc1.RootElement).IsNotEqualTo(doc2.RootElement);
+ }
+#endif
+```
+
+**Step 2: Run tests to verify they fail**
+
+```bash
+cd C:/git/TUnit && dotnet test TUnit.Assertions.Tests/TUnit.Assertions.Tests.csproj --filter "FullyQualifiedName~JsonElementAssertionTests.IsEqualTo" --framework net9.0 --no-build
+```
+
+Expected: FAIL - IsEqualTo method not found
+
+**Step 3: Add equality assertions**
+
+Add to `TUnit.Assertions/Conditions/Json/JsonElementAssertionExtensions.cs`:
+
+```csharp
+#if NET9_0_OR_GREATER
+ [GenerateAssertion(ExpectationMessage = "to be equal to {expected}")]
+ public static TUnit.Assertions.Core.AssertionResult IsEqualTo(this JsonElement value, JsonElement expected)
+ {
+ if (JsonElement.DeepEquals(value, expected))
+ {
+ return TUnit.Assertions.Core.AssertionResult.Passed;
+ }
+
+ var diff = JsonDiffHelper.FindFirstDifference(value, expected);
+ return TUnit.Assertions.Core.AssertionResult.Failed(
+ $"differs at {diff.Path}: expected {diff.Expected} but found {diff.Actual}");
+ }
+
+ [GenerateAssertion(ExpectationMessage = "to not be equal to {expected}", InlineMethodBody = true)]
+ public static bool IsNotEqualTo(this JsonElement value, JsonElement expected)
+ => !JsonElement.DeepEquals(value, expected);
+#endif
+```
+
+**Step 4: Run tests to verify they pass**
+
+```bash
+cd C:/git/TUnit && dotnet test TUnit.Assertions.Tests/TUnit.Assertions.Tests.csproj --filter "FullyQualifiedName~JsonElementAssertionTests" --framework net9.0
+```
+
+Expected: All tests pass
+
+**Step 5: Commit**
+
+```bash
+git add TUnit.Assertions/Conditions/Json/JsonElementAssertionExtensions.cs TUnit.Assertions.Tests/JsonElementAssertionTests.cs
+git commit -m "feat(assertions): add JsonElement equality assertions for .NET 9+"
+```
+
+---
+
+## Task 5: Create JsonNode Assertions
+
+**Files:**
+- Create: `TUnit.Assertions/Conditions/Json/JsonNodeAssertionExtensions.cs`
+- Create: `TUnit.Assertions.Tests/JsonNodeAssertionTests.cs`
+
+**Step 1: Write the failing test**
+
+Create `TUnit.Assertions.Tests/JsonNodeAssertionTests.cs`:
+
+```csharp
+using System.Text.Json.Nodes;
+using TUnit.Assertions.Extensions;
+
+namespace TUnit.Assertions.Tests;
+
+public class JsonNodeAssertionTests
+{
+ [Test]
+ public async Task IsObject_WithJsonObject_Passes()
+ {
+ JsonNode? node = JsonNode.Parse("{\"name\":\"test\"}");
+ await Assert.That(node).IsJsonObject();
+ }
+
+ [Test]
+ public async Task IsArray_WithJsonArray_Passes()
+ {
+ JsonNode? node = JsonNode.Parse("[1,2,3]");
+ await Assert.That(node).IsJsonArray();
+ }
+
+ [Test]
+ public async Task IsValue_WithJsonValue_Passes()
+ {
+ JsonNode? node = JsonNode.Parse("42");
+ await Assert.That(node).IsJsonValue();
+ }
+
+ [Test]
+ public async Task HasProperty_WhenPropertyExists_Passes()
+ {
+ JsonNode? node = JsonNode.Parse("{\"name\":\"Alice\"}");
+ await Assert.That(node).HasJsonProperty("name");
+ }
+
+ [Test]
+ public async Task DoesNotHaveProperty_WhenPropertyMissing_Passes()
+ {
+ JsonNode? node = JsonNode.Parse("{\"name\":\"Alice\"}");
+ await Assert.That(node).DoesNotHaveJsonProperty("missing");
+ }
+
+#if NET8_0_OR_GREATER
+ [Test]
+ public async Task IsEqualTo_WithIdenticalJson_Passes()
+ {
+ JsonNode? node1 = JsonNode.Parse("{\"name\":\"Alice\",\"age\":30}");
+ JsonNode? node2 = JsonNode.Parse("{\"name\":\"Alice\",\"age\":30}");
+ await Assert.That(node1).IsEqualTo(node2);
+ }
+
+ [Test]
+ public async Task IsNotEqualTo_WithDifferentJson_Passes()
+ {
+ JsonNode? node1 = JsonNode.Parse("{\"name\":\"Alice\"}");
+ JsonNode? node2 = JsonNode.Parse("{\"name\":\"Bob\"}");
+ await Assert.That(node1).IsNotEqualTo(node2);
+ }
+#endif
+}
+```
+
+**Step 2: Run tests to verify they fail**
+
+```bash
+cd C:/git/TUnit && dotnet test TUnit.Assertions.Tests/TUnit.Assertions.Tests.csproj --filter "FullyQualifiedName~JsonNodeAssertionTests" --no-build
+```
+
+Expected: FAIL - methods not found
+
+**Step 3: Write the JsonNode assertions**
+
+Create `TUnit.Assertions/Conditions/Json/JsonNodeAssertionExtensions.cs`:
+
+```csharp
+using System.Text.Json.Nodes;
+using TUnit.Assertions.Attributes;
+
+namespace TUnit.Assertions.Conditions.Json;
+
+///
+/// Source-generated assertions for JsonNode types.
+///
+file static partial class JsonNodeAssertionExtensions
+{
+ [GenerateAssertion(ExpectationMessage = "to be a JsonObject", InlineMethodBody = true)]
+ public static bool IsJsonObject(this JsonNode? value)
+ => value is JsonObject;
+
+ [GenerateAssertion(ExpectationMessage = "to be a JsonArray", InlineMethodBody = true)]
+ public static bool IsJsonArray(this JsonNode? value)
+ => value is JsonArray;
+
+ [GenerateAssertion(ExpectationMessage = "to be a JsonValue", InlineMethodBody = true)]
+ public static bool IsJsonValue(this JsonNode? value)
+ => value is JsonValue;
+
+ [GenerateAssertion(ExpectationMessage = "to have property \"{propertyName}\"", InlineMethodBody = true)]
+ public static bool HasJsonProperty(this JsonNode? value, string propertyName)
+ => value is JsonObject obj && obj.ContainsKey(propertyName);
+
+ [GenerateAssertion(ExpectationMessage = "to not have property \"{propertyName}\"", InlineMethodBody = true)]
+ public static bool DoesNotHaveJsonProperty(this JsonNode? value, string propertyName)
+ => value is not JsonObject obj || !obj.ContainsKey(propertyName);
+
+#if NET8_0_OR_GREATER
+ [GenerateAssertion(ExpectationMessage = "to be equal to {expected}", InlineMethodBody = true)]
+ public static bool IsEqualTo(this JsonNode? value, JsonNode? expected)
+ => JsonNode.DeepEquals(value, expected);
+
+ [GenerateAssertion(ExpectationMessage = "to not be equal to {expected}", InlineMethodBody = true)]
+ public static bool IsNotEqualTo(this JsonNode? value, JsonNode? expected)
+ => !JsonNode.DeepEquals(value, expected);
+#endif
+}
+```
+
+**Step 4: Run tests to verify they pass**
+
+```bash
+cd C:/git/TUnit && dotnet test TUnit.Assertions.Tests/TUnit.Assertions.Tests.csproj --filter "FullyQualifiedName~JsonNodeAssertionTests"
+```
+
+Expected: All tests pass
+
+**Step 5: Commit**
+
+```bash
+git add TUnit.Assertions/Conditions/Json/JsonNodeAssertionExtensions.cs TUnit.Assertions.Tests/JsonNodeAssertionTests.cs
+git commit -m "feat(assertions): add JsonNode assertions with equality for .NET 8+"
+```
+
+---
+
+## Task 6: Create JsonArray Assertions
+
+**Files:**
+- Modify: `TUnit.Assertions/Conditions/Json/JsonNodeAssertionExtensions.cs`
+- Modify: `TUnit.Assertions.Tests/JsonNodeAssertionTests.cs`
+
+**Step 1: Write the failing tests**
+
+Add to `TUnit.Assertions.Tests/JsonNodeAssertionTests.cs`:
+
+```csharp
+ [Test]
+ public async Task IsEmpty_WithEmptyArray_Passes()
+ {
+ var array = new JsonArray();
+ await Assert.That(array).IsJsonArrayEmpty();
+ }
+
+ [Test]
+ public async Task IsNotEmpty_WithNonEmptyArray_Passes()
+ {
+ var array = new JsonArray(1, 2, 3);
+ await Assert.That(array).IsJsonArrayNotEmpty();
+ }
+
+ [Test]
+ public async Task HasCount_WithMatchingCount_Passes()
+ {
+ var array = new JsonArray(1, 2, 3);
+ await Assert.That(array).HasJsonArrayCount(3);
+ }
+```
+
+**Step 2: Run tests to verify they fail**
+
+```bash
+cd C:/git/TUnit && dotnet test TUnit.Assertions.Tests/TUnit.Assertions.Tests.csproj --filter "FullyQualifiedName~JsonNodeAssertionTests.IsEmpty" --no-build
+```
+
+Expected: FAIL - methods not found
+
+**Step 3: Add JsonArray assertions**
+
+Add to `TUnit.Assertions/Conditions/Json/JsonNodeAssertionExtensions.cs`:
+
+```csharp
+ [GenerateAssertion(ExpectationMessage = "to be an empty JSON array", InlineMethodBody = true)]
+ public static bool IsJsonArrayEmpty(this JsonArray value)
+ => value.Count == 0;
+
+ [GenerateAssertion(ExpectationMessage = "to not be an empty JSON array", InlineMethodBody = true)]
+ public static bool IsJsonArrayNotEmpty(this JsonArray value)
+ => value.Count > 0;
+
+ [GenerateAssertion(ExpectationMessage = "to have {expected} elements")]
+ public static TUnit.Assertions.Core.AssertionResult HasJsonArrayCount(this JsonArray value, int expected)
+ {
+ if (value.Count == expected)
+ {
+ return TUnit.Assertions.Core.AssertionResult.Passed;
+ }
+ return TUnit.Assertions.Core.AssertionResult.Failed($"has {value.Count} elements");
+ }
+```
+
+**Step 4: Run tests to verify they pass**
+
+```bash
+cd C:/git/TUnit && dotnet test TUnit.Assertions.Tests/TUnit.Assertions.Tests.csproj --filter "FullyQualifiedName~JsonNodeAssertionTests"
+```
+
+Expected: All tests pass
+
+**Step 5: Commit**
+
+```bash
+git add TUnit.Assertions/Conditions/Json/JsonNodeAssertionExtensions.cs TUnit.Assertions.Tests/JsonNodeAssertionTests.cs
+git commit -m "feat(assertions): add JsonArray count and empty assertions"
+```
+
+---
+
+## Task 7: Create String JSON Validation Assertions
+
+**Files:**
+- Create: `TUnit.Assertions/Conditions/Json/JsonStringAssertionExtensions.cs`
+- Create: `TUnit.Assertions.Tests/JsonStringAssertionTests.cs`
+
+**Step 1: Write the failing test**
+
+Create `TUnit.Assertions.Tests/JsonStringAssertionTests.cs`:
+
+```csharp
+using TUnit.Assertions.Extensions;
+
+namespace TUnit.Assertions.Tests;
+
+public class JsonStringAssertionTests
+{
+ [Test]
+ public async Task IsValidJson_WithValidJson_Passes()
+ {
+ var json = "{\"name\":\"Alice\"}";
+ await Assert.That(json).IsValidJson();
+ }
+
+ [Test]
+ public async Task IsValidJson_WithInvalidJson_Fails()
+ {
+ var json = "not valid json";
+ await Assert.ThrowsAsync(
+ async () => await Assert.That(json).IsValidJson());
+ }
+
+ [Test]
+ public async Task IsNotValidJson_WithInvalidJson_Passes()
+ {
+ var json = "not valid json";
+ await Assert.That(json).IsNotValidJson();
+ }
+
+ [Test]
+ public async Task IsValidJsonObject_WithObject_Passes()
+ {
+ var json = "{\"name\":\"Alice\"}";
+ await Assert.That(json).IsValidJsonObject();
+ }
+
+ [Test]
+ public async Task IsValidJsonObject_WithArray_Fails()
+ {
+ var json = "[1,2,3]";
+ await Assert.ThrowsAsync(
+ async () => await Assert.That(json).IsValidJsonObject());
+ }
+
+ [Test]
+ public async Task IsValidJsonArray_WithArray_Passes()
+ {
+ var json = "[1,2,3]";
+ await Assert.That(json).IsValidJsonArray();
+ }
+
+ [Test]
+ public async Task IsValidJsonArray_WithObject_Fails()
+ {
+ var json = "{\"name\":\"Alice\"}";
+ await Assert.ThrowsAsync(
+ async () => await Assert.That(json).IsValidJsonArray());
+ }
+}
+```
+
+**Step 2: Run tests to verify they fail**
+
+```bash
+cd C:/git/TUnit && dotnet test TUnit.Assertions.Tests/TUnit.Assertions.Tests.csproj --filter "FullyQualifiedName~JsonStringAssertionTests" --no-build
+```
+
+Expected: FAIL - methods not found
+
+**Step 3: Write the string JSON assertions**
+
+Create `TUnit.Assertions/Conditions/Json/JsonStringAssertionExtensions.cs`:
+
+```csharp
+using System.Text.Json;
+using TUnit.Assertions.Attributes;
+using TUnit.Assertions.Core;
+
+namespace TUnit.Assertions.Conditions.Json;
+
+///
+/// Source-generated assertions for validating JSON strings.
+///
+file static partial class JsonStringAssertionExtensions
+{
+ [GenerateAssertion(ExpectationMessage = "to be valid JSON")]
+ public static AssertionResult IsValidJson(this string value)
+ {
+ try
+ {
+ using var doc = JsonDocument.Parse(value);
+ return AssertionResult.Passed;
+ }
+ catch (JsonException ex)
+ {
+ return AssertionResult.Failed($"is not valid JSON: {ex.Message}");
+ }
+ }
+
+ [GenerateAssertion(ExpectationMessage = "to not be valid JSON")]
+ public static AssertionResult IsNotValidJson(this string value)
+ {
+ try
+ {
+ using var doc = JsonDocument.Parse(value);
+ return AssertionResult.Failed("is valid JSON");
+ }
+ catch (JsonException)
+ {
+ return AssertionResult.Passed;
+ }
+ }
+
+ [GenerateAssertion(ExpectationMessage = "to be a valid JSON object")]
+ public static AssertionResult IsValidJsonObject(this string value)
+ {
+ try
+ {
+ using var doc = JsonDocument.Parse(value);
+ if (doc.RootElement.ValueKind == JsonValueKind.Object)
+ {
+ return AssertionResult.Passed;
+ }
+ return AssertionResult.Failed($"is a {doc.RootElement.ValueKind}, not an Object");
+ }
+ catch (JsonException ex)
+ {
+ return AssertionResult.Failed($"is not valid JSON: {ex.Message}");
+ }
+ }
+
+ [GenerateAssertion(ExpectationMessage = "to be a valid JSON array")]
+ public static AssertionResult IsValidJsonArray(this string value)
+ {
+ try
+ {
+ using var doc = JsonDocument.Parse(value);
+ if (doc.RootElement.ValueKind == JsonValueKind.Array)
+ {
+ return AssertionResult.Passed;
+ }
+ return AssertionResult.Failed($"is a {doc.RootElement.ValueKind}, not an Array");
+ }
+ catch (JsonException ex)
+ {
+ return AssertionResult.Failed($"is not valid JSON: {ex.Message}");
+ }
+ }
+}
+```
+
+**Step 4: Run tests to verify they pass**
+
+```bash
+cd C:/git/TUnit && dotnet test TUnit.Assertions.Tests/TUnit.Assertions.Tests.csproj --filter "FullyQualifiedName~JsonStringAssertionTests"
+```
+
+Expected: All tests pass
+
+**Step 5: Commit**
+
+```bash
+git add TUnit.Assertions/Conditions/Json/JsonStringAssertionExtensions.cs TUnit.Assertions.Tests/JsonStringAssertionTests.cs
+git commit -m "feat(assertions): add JSON string validation assertions"
+```
+
+---
+
+## Task 8: Run Full Test Suite and Update Snapshots
+
+**Files:**
+- May modify: `TUnit.PublicAPI/**/*.verified.txt`
+- May modify: `TUnit.Core.SourceGenerator.Tests/**/*.verified.txt`
+
+**Step 1: Run full assertion tests**
+
+```bash
+cd C:/git/TUnit && dotnet test TUnit.Assertions.Tests/TUnit.Assertions.Tests.csproj
+```
+
+Expected: All tests pass
+
+**Step 2: Run public API tests**
+
+```bash
+cd C:/git/TUnit && dotnet test TUnit.PublicAPI/TUnit.PublicAPI.csproj
+```
+
+Expected: May fail if new public APIs are detected
+
+**Step 3: Accept snapshots if needed**
+
+```bash
+cd C:/git/TUnit/TUnit.PublicAPI
+for %f in (*.received.txt) do move /Y "%f" "%~nf.verified.txt"
+```
+
+**Step 4: Run source generator tests**
+
+```bash
+cd C:/git/TUnit && dotnet test TUnit.Core.SourceGenerator.Tests/TUnit.Core.SourceGenerator.Tests.csproj
+```
+
+Expected: Should pass (JSON assertions don't affect source generator)
+
+**Step 5: Commit snapshots**
+
+```bash
+git add TUnit.PublicAPI/*.verified.txt
+git commit -m "chore: update public API snapshots for JSON assertions"
+```
+
+---
+
+## Task 9: Final Verification
+
+**Step 1: Run complete test suite**
+
+```bash
+cd C:/git/TUnit && dotnet test
+```
+
+Expected: All tests pass
+
+**Step 2: Test AOT compatibility**
+
+```bash
+cd C:/git/TUnit/TUnit.TestProject && dotnet publish -c Release -p:PublishAot=true --use-current-runtime
+```
+
+Expected: Publish succeeds
+
+**Step 3: Final commit if any cleanup needed**
+
+```bash
+git status
+# If clean, no action needed
+# If changes, commit them
+```
+
+---
+
+## Summary
+
+This plan creates 4 new files:
+- `TUnit.Assertions/Conditions/Json/JsonDiffHelper.cs` - Path-to-difference logic
+- `TUnit.Assertions/Conditions/Json/JsonElementAssertionExtensions.cs` - JsonElement assertions
+- `TUnit.Assertions/Conditions/Json/JsonNodeAssertionExtensions.cs` - JsonNode/JsonArray assertions
+- `TUnit.Assertions/Conditions/Json/JsonStringAssertionExtensions.cs` - String validation assertions
+
+And 3 new test files:
+- `TUnit.Assertions.Tests/JsonElementAssertionTests.cs`
+- `TUnit.Assertions.Tests/JsonNodeAssertionTests.cs`
+- `TUnit.Assertions.Tests/JsonStringAssertionTests.cs`
+
+Total assertions: ~20 across all types with runtime-specific availability.
From cdabd7d0ebc70904a0ce5170ee8bfa8be70202d7 Mon Sep 17 00:00:00 2001
From: Tom Longhurst <30480171+thomhurst@users.noreply.github.com>
Date: Sun, 28 Dec 2025 12:33:05 +0000
Subject: [PATCH 03/10] feat(assertions): add JsonElement property assertions
---
.../JsonElementAssertionTests.cs | 30 +++++++++++++++++++
.../Json/JsonElementAssertionExtensions.cs | 8 +++++
2 files changed, 38 insertions(+)
diff --git a/TUnit.Assertions.Tests/JsonElementAssertionTests.cs b/TUnit.Assertions.Tests/JsonElementAssertionTests.cs
index 871873c112..99414a8272 100644
--- a/TUnit.Assertions.Tests/JsonElementAssertionTests.cs
+++ b/TUnit.Assertions.Tests/JsonElementAssertionTests.cs
@@ -109,4 +109,34 @@ public async Task IsNotNull_WithNull_Fails()
await Assert.ThrowsAsync(
async () => await Assert.That(doc.RootElement).IsNotNull());
}
+
+ [Test]
+ public async Task HasProperty_WhenPropertyExists_Passes()
+ {
+ using var doc = JsonDocument.Parse("{\"name\":\"Alice\",\"age\":30}");
+ await Assert.That(doc.RootElement).HasProperty("name");
+ }
+
+ [Test]
+ public async Task HasProperty_WhenPropertyMissing_Fails()
+ {
+ using var doc = JsonDocument.Parse("{\"name\":\"Alice\"}");
+ await Assert.ThrowsAsync(
+ async () => await Assert.That(doc.RootElement).HasProperty("missing"));
+ }
+
+ [Test]
+ public async Task DoesNotHaveProperty_WhenPropertyMissing_Passes()
+ {
+ using var doc = JsonDocument.Parse("{\"name\":\"Alice\"}");
+ await Assert.That(doc.RootElement).DoesNotHaveProperty("missing");
+ }
+
+ [Test]
+ public async Task DoesNotHaveProperty_WhenPropertyExists_Fails()
+ {
+ using var doc = JsonDocument.Parse("{\"name\":\"Alice\"}");
+ await Assert.ThrowsAsync(
+ async () => await Assert.That(doc.RootElement).DoesNotHaveProperty("name"));
+ }
}
diff --git a/TUnit.Assertions/Conditions/Json/JsonElementAssertionExtensions.cs b/TUnit.Assertions/Conditions/Json/JsonElementAssertionExtensions.cs
index 6b4ba1d018..37ce46cc11 100644
--- a/TUnit.Assertions/Conditions/Json/JsonElementAssertionExtensions.cs
+++ b/TUnit.Assertions/Conditions/Json/JsonElementAssertionExtensions.cs
@@ -35,4 +35,12 @@ public static bool IsNull(this JsonElement value)
[GenerateAssertion(ExpectationMessage = "to not be JSON null", InlineMethodBody = true)]
public static bool IsNotNull(this JsonElement value)
=> value.ValueKind != JsonValueKind.Null;
+
+ [GenerateAssertion(ExpectationMessage = "to have property '{propertyName}'", InlineMethodBody = true)]
+ public static bool HasProperty(this JsonElement value, string propertyName)
+ => value.ValueKind == JsonValueKind.Object && value.TryGetProperty(propertyName, out _);
+
+ [GenerateAssertion(ExpectationMessage = "to not have property '{propertyName}'", InlineMethodBody = true)]
+ public static bool DoesNotHaveProperty(this JsonElement value, string propertyName)
+ => value.ValueKind != JsonValueKind.Object || !value.TryGetProperty(propertyName, out _);
}
From fe355605f7cca7a600856b48c10c11cc3a97ed46 Mon Sep 17 00:00:00 2001
From: Tom Longhurst <30480171+thomhurst@users.noreply.github.com>
Date: Sun, 28 Dec 2025 13:05:10 +0000
Subject: [PATCH 04/10] feat(assertions): add JsonElement deep equality
assertions for .NET 9+
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude Opus 4.5
---
.../JsonElementAssertionTests.cs | 47 +++++++++++++++++++
.../Json/JsonElementAssertionExtensions.cs | 21 ++++++++-
2 files changed, 67 insertions(+), 1 deletion(-)
diff --git a/TUnit.Assertions.Tests/JsonElementAssertionTests.cs b/TUnit.Assertions.Tests/JsonElementAssertionTests.cs
index 99414a8272..d75275e8c7 100644
--- a/TUnit.Assertions.Tests/JsonElementAssertionTests.cs
+++ b/TUnit.Assertions.Tests/JsonElementAssertionTests.cs
@@ -139,4 +139,51 @@ public async Task DoesNotHaveProperty_WhenPropertyExists_Fails()
await Assert.ThrowsAsync(
async () => await Assert.That(doc.RootElement).DoesNotHaveProperty("name"));
}
+
+#if NET9_0_OR_GREATER
+ [Test]
+ public async Task IsDeepEqualTo_WithIdenticalJson_Passes()
+ {
+ using var doc1 = JsonDocument.Parse("{\"name\":\"Alice\",\"age\":30}");
+ using var doc2 = JsonDocument.Parse("{\"name\":\"Alice\",\"age\":30}");
+ await Assert.That(doc1.RootElement).IsDeepEqualTo(doc2.RootElement);
+ }
+
+ [Test]
+ public async Task IsDeepEqualTo_WithDifferentWhitespace_Passes()
+ {
+ using var doc1 = JsonDocument.Parse("{ \"name\" : \"Alice\" }");
+ using var doc2 = JsonDocument.Parse("{\"name\":\"Alice\"}");
+ await Assert.That(doc1.RootElement).IsDeepEqualTo(doc2.RootElement);
+ }
+
+ [Test]
+ public async Task IsDeepEqualTo_WithDifferentValues_FailsWithPath()
+ {
+ using var doc1 = JsonDocument.Parse("{\"name\":\"Alice\",\"age\":30}");
+ using var doc2 = JsonDocument.Parse("{\"name\":\"Alice\",\"age\":31}");
+
+ var exception = await Assert.ThrowsAsync(
+ async () => await Assert.That(doc1.RootElement).IsDeepEqualTo(doc2.RootElement));
+
+ await Assert.That(exception.Message).Contains("$.age");
+ }
+
+ [Test]
+ public async Task IsNotDeepEqualTo_WithDifferentJson_Passes()
+ {
+ using var doc1 = JsonDocument.Parse("{\"name\":\"Alice\"}");
+ using var doc2 = JsonDocument.Parse("{\"name\":\"Bob\"}");
+ await Assert.That(doc1.RootElement).IsNotDeepEqualTo(doc2.RootElement);
+ }
+
+ [Test]
+ public async Task IsNotDeepEqualTo_WithIdenticalJson_Fails()
+ {
+ using var doc1 = JsonDocument.Parse("{\"name\":\"Alice\"}");
+ using var doc2 = JsonDocument.Parse("{\"name\":\"Alice\"}");
+ await Assert.ThrowsAsync(
+ async () => await Assert.That(doc1.RootElement).IsNotDeepEqualTo(doc2.RootElement));
+ }
+#endif
}
diff --git a/TUnit.Assertions/Conditions/Json/JsonElementAssertionExtensions.cs b/TUnit.Assertions/Conditions/Json/JsonElementAssertionExtensions.cs
index 37ce46cc11..004591015f 100644
--- a/TUnit.Assertions/Conditions/Json/JsonElementAssertionExtensions.cs
+++ b/TUnit.Assertions/Conditions/Json/JsonElementAssertionExtensions.cs
@@ -6,7 +6,7 @@ namespace TUnit.Assertions.Conditions.Json;
///
/// Source-generated assertions for JsonElement type checking.
///
-file static partial class JsonElementAssertionExtensions
+public static partial class JsonElementAssertionExtensions
{
[GenerateAssertion(ExpectationMessage = "to be a JSON object", InlineMethodBody = true)]
public static bool IsObject(this JsonElement value)
@@ -43,4 +43,23 @@ public static bool HasProperty(this JsonElement value, string propertyName)
[GenerateAssertion(ExpectationMessage = "to not have property '{propertyName}'", InlineMethodBody = true)]
public static bool DoesNotHaveProperty(this JsonElement value, string propertyName)
=> value.ValueKind != JsonValueKind.Object || !value.TryGetProperty(propertyName, out _);
+
+#if NET9_0_OR_GREATER
+ [GenerateAssertion(ExpectationMessage = "to be deeply equal to {expected}")]
+ public static TUnit.Assertions.Core.AssertionResult IsDeepEqualTo(this JsonElement value, JsonElement expected)
+ {
+ if (JsonElement.DeepEquals(value, expected))
+ {
+ return TUnit.Assertions.Core.AssertionResult.Passed;
+ }
+
+ var diff = JsonDiffHelper.FindFirstDifference(value, expected);
+ return TUnit.Assertions.Core.AssertionResult.Failed(
+ $"differs at {diff.Path}: expected {diff.Expected} but found {diff.Actual}");
+ }
+
+ [GenerateAssertion(ExpectationMessage = "to not be deeply equal to {expected}", InlineMethodBody = true)]
+ public static bool IsNotDeepEqualTo(this JsonElement value, JsonElement expected)
+ => !JsonElement.DeepEquals(value, expected);
+#endif
}
From 84b1a17626863b7b00e7e5b25b5c0be8163298be Mon Sep 17 00:00:00 2001
From: Tom Longhurst <30480171+thomhurst@users.noreply.github.com>
Date: Sun, 28 Dec 2025 13:14:10 +0000
Subject: [PATCH 05/10] feat(assertions): add JsonNode assertions with equality
for .NET 8+
---
.../JsonNodeAssertionTests.cs | 118 ++++++++++++++++++
.../Json/JsonNodeAssertionExtensions.cs | 86 +++++++++++++
2 files changed, 204 insertions(+)
create mode 100644 TUnit.Assertions.Tests/JsonNodeAssertionTests.cs
create mode 100644 TUnit.Assertions/Conditions/Json/JsonNodeAssertionExtensions.cs
diff --git a/TUnit.Assertions.Tests/JsonNodeAssertionTests.cs b/TUnit.Assertions.Tests/JsonNodeAssertionTests.cs
new file mode 100644
index 0000000000..1456a86edf
--- /dev/null
+++ b/TUnit.Assertions.Tests/JsonNodeAssertionTests.cs
@@ -0,0 +1,118 @@
+using System.Text.Json.Nodes;
+using TUnit.Assertions.Extensions;
+
+namespace TUnit.Assertions.Tests;
+
+public class JsonNodeAssertionTests
+{
+ [Test]
+ public async Task IsJsonObject_WithJsonObject_Passes()
+ {
+ JsonNode? node = JsonNode.Parse("{\"name\":\"test\"}");
+ await Assert.That(node).IsJsonObject();
+ }
+
+ [Test]
+ public async Task IsJsonObject_WithJsonArray_Fails()
+ {
+ JsonNode? node = JsonNode.Parse("[1,2,3]");
+ await Assert.ThrowsAsync(
+ async () => await Assert.That(node).IsJsonObject());
+ }
+
+ [Test]
+ public async Task IsJsonArray_WithJsonArray_Passes()
+ {
+ JsonNode? node = JsonNode.Parse("[1,2,3]");
+ await Assert.That(node).IsJsonArray();
+ }
+
+ [Test]
+ public async Task IsJsonArray_WithJsonObject_Fails()
+ {
+ JsonNode? node = JsonNode.Parse("{\"name\":\"test\"}");
+ await Assert.ThrowsAsync(
+ async () => await Assert.That(node).IsJsonArray());
+ }
+
+ [Test]
+ public async Task IsJsonValue_WithJsonValue_Passes()
+ {
+ JsonNode? node = JsonNode.Parse("42");
+ await Assert.That(node).IsJsonValue();
+ }
+
+ [Test]
+ public async Task IsJsonValue_WithJsonObject_Fails()
+ {
+ JsonNode? node = JsonNode.Parse("{\"name\":\"test\"}");
+ await Assert.ThrowsAsync(
+ async () => await Assert.That(node).IsJsonValue());
+ }
+
+ [Test]
+ public async Task HasJsonProperty_WhenPropertyExists_Passes()
+ {
+ JsonNode? node = JsonNode.Parse("{\"name\":\"Alice\"}");
+ await Assert.That(node).HasJsonProperty("name");
+ }
+
+ [Test]
+ public async Task HasJsonProperty_WhenPropertyMissing_Fails()
+ {
+ JsonNode? node = JsonNode.Parse("{\"name\":\"Alice\"}");
+ await Assert.ThrowsAsync(
+ async () => await Assert.That(node).HasJsonProperty("missing"));
+ }
+
+ [Test]
+ public async Task DoesNotHaveJsonProperty_WhenPropertyMissing_Passes()
+ {
+ JsonNode? node = JsonNode.Parse("{\"name\":\"Alice\"}");
+ await Assert.That(node).DoesNotHaveJsonProperty("missing");
+ }
+
+ [Test]
+ public async Task DoesNotHaveJsonProperty_WhenPropertyExists_Fails()
+ {
+ JsonNode? node = JsonNode.Parse("{\"name\":\"Alice\"}");
+ await Assert.ThrowsAsync(
+ async () => await Assert.That(node).DoesNotHaveJsonProperty("name"));
+ }
+
+#if NET8_0_OR_GREATER
+ [Test]
+ public async Task IsDeepEqualTo_WithIdenticalJson_Passes()
+ {
+ JsonNode? node1 = JsonNode.Parse("{\"name\":\"Alice\",\"age\":30}");
+ JsonNode? node2 = JsonNode.Parse("{\"name\":\"Alice\",\"age\":30}");
+ await Assert.That(node1).IsDeepEqualTo(node2);
+ }
+
+ [Test]
+ public async Task IsDeepEqualTo_WithDifferentJson_Fails()
+ {
+ JsonNode? node1 = JsonNode.Parse("{\"name\":\"Alice\"}");
+ JsonNode? node2 = JsonNode.Parse("{\"name\":\"Bob\"}");
+ await Assert.ThrowsAsync(
+ async () => await Assert.That(node1).IsDeepEqualTo(node2));
+ }
+
+ [Test]
+ public async Task IsNotDeepEqualTo_WithDifferentJson_Passes()
+ {
+ JsonNode? node1 = JsonNode.Parse("{\"name\":\"Alice\"}");
+ JsonNode? node2 = JsonNode.Parse("{\"name\":\"Bob\"}");
+ await Assert.That(node1).IsNotDeepEqualTo(node2);
+ }
+
+ [Test]
+ public async Task IsNotDeepEqualTo_WithIdenticalJson_Fails()
+ {
+ JsonNode? node1 = JsonNode.Parse("{\"name\":\"Alice\"}");
+ JsonNode? node2 = JsonNode.Parse("{\"name\":\"Alice\"}");
+ await Assert.ThrowsAsync(
+ async () => await Assert.That(node1).IsNotDeepEqualTo(node2));
+ }
+#endif
+}
diff --git a/TUnit.Assertions/Conditions/Json/JsonNodeAssertionExtensions.cs b/TUnit.Assertions/Conditions/Json/JsonNodeAssertionExtensions.cs
new file mode 100644
index 0000000000..e22110e692
--- /dev/null
+++ b/TUnit.Assertions/Conditions/Json/JsonNodeAssertionExtensions.cs
@@ -0,0 +1,86 @@
+using System.Text.Json.Nodes;
+using TUnit.Assertions.Attributes;
+
+namespace TUnit.Assertions.Conditions.Json;
+
+///
+/// Source-generated assertions for JsonNode types.
+///
+public static partial class JsonNodeAssertionExtensions
+{
+ [GenerateAssertion(ExpectationMessage = "to be a JsonObject")]
+ public static TUnit.Assertions.Core.AssertionResult IsJsonObject(this JsonNode? value)
+ {
+ if (value is JsonObject)
+ {
+ return TUnit.Assertions.Core.AssertionResult.Passed;
+ }
+ return TUnit.Assertions.Core.AssertionResult.Failed($"found {value?.GetType().Name ?? "null"}");
+ }
+
+ [GenerateAssertion(ExpectationMessage = "to be a JsonArray")]
+ public static TUnit.Assertions.Core.AssertionResult IsJsonArray(this JsonNode? value)
+ {
+ if (value is JsonArray)
+ {
+ return TUnit.Assertions.Core.AssertionResult.Passed;
+ }
+ return TUnit.Assertions.Core.AssertionResult.Failed($"found {value?.GetType().Name ?? "null"}");
+ }
+
+ [GenerateAssertion(ExpectationMessage = "to be a JsonValue")]
+ public static TUnit.Assertions.Core.AssertionResult IsJsonValue(this JsonNode? value)
+ {
+ if (value is JsonValue)
+ {
+ return TUnit.Assertions.Core.AssertionResult.Passed;
+ }
+ return TUnit.Assertions.Core.AssertionResult.Failed($"found {value?.GetType().Name ?? "null"}");
+ }
+
+ [GenerateAssertion(ExpectationMessage = "to have property '{propertyName}'")]
+ public static TUnit.Assertions.Core.AssertionResult HasJsonProperty(this JsonNode? value, string propertyName)
+ {
+ if (value is JsonObject obj && obj.ContainsKey(propertyName))
+ {
+ return TUnit.Assertions.Core.AssertionResult.Passed;
+ }
+ if (value is not JsonObject)
+ {
+ return TUnit.Assertions.Core.AssertionResult.Failed($"found {value?.GetType().Name ?? "null"} instead of JsonObject");
+ }
+ return TUnit.Assertions.Core.AssertionResult.Failed($"property '{propertyName}' not found");
+ }
+
+ [GenerateAssertion(ExpectationMessage = "to not have property '{propertyName}'")]
+ public static TUnit.Assertions.Core.AssertionResult DoesNotHaveJsonProperty(this JsonNode? value, string propertyName)
+ {
+ if (value is not JsonObject obj || !obj.ContainsKey(propertyName))
+ {
+ return TUnit.Assertions.Core.AssertionResult.Passed;
+ }
+ return TUnit.Assertions.Core.AssertionResult.Failed($"property '{propertyName}' was found");
+ }
+
+#if NET8_0_OR_GREATER
+ [GenerateAssertion(ExpectationMessage = "to be equal to {expected}")]
+ public static TUnit.Assertions.Core.AssertionResult IsDeepEqualTo(this JsonNode? value, JsonNode? expected)
+ {
+ if (JsonNode.DeepEquals(value, expected))
+ {
+ return TUnit.Assertions.Core.AssertionResult.Passed;
+ }
+ return TUnit.Assertions.Core.AssertionResult.Failed($"found {value}");
+ }
+
+ [GenerateAssertion(ExpectationMessage = "to not be equal to {expected}")]
+ public static TUnit.Assertions.Core.AssertionResult IsNotDeepEqualTo(this JsonNode? value, JsonNode? expected)
+ {
+ if (!JsonNode.DeepEquals(value, expected))
+ {
+ return TUnit.Assertions.Core.AssertionResult.Passed;
+ }
+ return TUnit.Assertions.Core.AssertionResult.Failed($"values are equal");
+ }
+#endif
+}
From 37a61651b743fa15374db28e81547e37f08c0cd7 Mon Sep 17 00:00:00 2001
From: Tom Longhurst <30480171+thomhurst@users.noreply.github.com>
Date: Sun, 28 Dec 2025 13:22:54 +0000
Subject: [PATCH 06/10] fix(assertions): add path-based error messages for
JsonNode equality
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
- Added JsonNode diff helper methods to JsonDiffHelper
- Updated IsDeepEqualTo for JsonNode to show path-to-difference on failure
- Added tests for whitespace handling and error message path verification
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude Opus 4.5
---
.../JsonNodeAssertionTests.cs | 20 +++
.../Conditions/Json/JsonDiffHelper.cs | 135 ++++++++++++++++++
.../Json/JsonNodeAssertionExtensions.cs | 7 +-
3 files changed, 160 insertions(+), 2 deletions(-)
diff --git a/TUnit.Assertions.Tests/JsonNodeAssertionTests.cs b/TUnit.Assertions.Tests/JsonNodeAssertionTests.cs
index 1456a86edf..d76a3c8fea 100644
--- a/TUnit.Assertions.Tests/JsonNodeAssertionTests.cs
+++ b/TUnit.Assertions.Tests/JsonNodeAssertionTests.cs
@@ -89,6 +89,14 @@ public async Task IsDeepEqualTo_WithIdenticalJson_Passes()
await Assert.That(node1).IsDeepEqualTo(node2);
}
+ [Test]
+ public async Task IsDeepEqualTo_WithDifferentWhitespace_Passes()
+ {
+ JsonNode? node1 = JsonNode.Parse("{ \"name\" : \"Alice\" }");
+ JsonNode? node2 = JsonNode.Parse("{\"name\":\"Alice\"}");
+ await Assert.That(node1).IsDeepEqualTo(node2);
+ }
+
[Test]
public async Task IsDeepEqualTo_WithDifferentJson_Fails()
{
@@ -98,6 +106,18 @@ public async Task IsDeepEqualTo_WithDifferentJson_Fails()
async () => await Assert.That(node1).IsDeepEqualTo(node2));
}
+ [Test]
+ public async Task IsDeepEqualTo_ErrorMessageContainsPath()
+ {
+ JsonNode? node1 = JsonNode.Parse("{\"person\":{\"name\":\"Alice\",\"age\":30}}");
+ JsonNode? node2 = JsonNode.Parse("{\"person\":{\"name\":\"Alice\",\"age\":31}}");
+
+ var exception = await Assert.ThrowsAsync(
+ async () => await Assert.That(node1).IsDeepEqualTo(node2));
+
+ await Assert.That(exception.Message).Contains("$.person.age");
+ }
+
[Test]
public async Task IsNotDeepEqualTo_WithDifferentJson_Passes()
{
diff --git a/TUnit.Assertions/Conditions/Json/JsonDiffHelper.cs b/TUnit.Assertions/Conditions/Json/JsonDiffHelper.cs
index 6816e7ffd9..31b0cf41c8 100644
--- a/TUnit.Assertions/Conditions/Json/JsonDiffHelper.cs
+++ b/TUnit.Assertions/Conditions/Json/JsonDiffHelper.cs
@@ -1,4 +1,5 @@
using System.Text.Json;
+using System.Text.Json.Nodes;
namespace TUnit.Assertions.Conditions.Json;
@@ -130,4 +131,138 @@ private static string FormatValue(JsonElement element)
_ => element.GetRawText()
};
}
+
+ ///
+ /// Finds the first difference between two JSON nodes.
+ ///
+ /// The actual JSON node to compare.
+ /// The expected JSON node to compare against.
+ /// A containing information about the first difference found,
+ /// or a result with set to false if the nodes are identical.
+ public static DiffResult FindFirstDifference(JsonNode? actual, JsonNode? expected)
+ {
+ return FindNodeDiff(actual, expected, "$");
+ }
+
+ private static DiffResult FindNodeDiff(JsonNode? actual, JsonNode? expected, string path)
+ {
+ // Handle null cases
+ if (actual is null && expected is null)
+ {
+ return new DiffResult(path, "", "", HasDifference: false);
+ }
+ if (actual is null)
+ {
+ return new DiffResult(path, FormatNode(expected), "null");
+ }
+ if (expected is null)
+ {
+ return new DiffResult(path, "null", FormatNode(actual));
+ }
+
+ // Check type mismatch
+ if (actual.GetType() != expected.GetType())
+ {
+ return new DiffResult(path, GetNodeTypeName(expected), GetNodeTypeName(actual));
+ }
+
+ return actual switch
+ {
+ JsonObject actualObj => CompareJsonObjects(actualObj, (JsonObject)expected, path),
+ JsonArray actualArr => CompareJsonArrays(actualArr, (JsonArray)expected, path),
+ JsonValue actualVal => CompareJsonValues(actualVal, (JsonValue)expected, path),
+ _ => new DiffResult(path, "", "", HasDifference: false)
+ };
+ }
+
+ private static DiffResult CompareJsonObjects(JsonObject actual, JsonObject expected, string path)
+ {
+ // Check for missing properties in actual that exist in expected
+ foreach (var prop in expected)
+ {
+ var propPath = $"{path}.{prop.Key}";
+ if (!actual.TryGetPropertyValue(prop.Key, out var actualProp))
+ {
+ return new DiffResult(propPath, FormatNode(prop.Value), "(missing)");
+ }
+
+ var diff = FindNodeDiff(actualProp, prop.Value, propPath);
+ if (diff.HasDifference)
+ {
+ return diff;
+ }
+ }
+
+ // Check for extra properties in actual that don't exist in expected
+ foreach (var prop in actual)
+ {
+ var propPath = $"{path}.{prop.Key}";
+ if (!expected.ContainsKey(prop.Key))
+ {
+ return new DiffResult(propPath, "(missing)", FormatNode(prop.Value));
+ }
+ }
+
+ return new DiffResult(path, "", "", HasDifference: false);
+ }
+
+ private static DiffResult CompareJsonArrays(JsonArray actual, JsonArray expected, string path)
+ {
+ if (actual.Count != expected.Count)
+ {
+ return new DiffResult($"{path}.Length", expected.Count.ToString(), actual.Count.ToString());
+ }
+
+ for (var i = 0; i < actual.Count; i++)
+ {
+ var itemPath = $"{path}[{i}]";
+ var diff = FindNodeDiff(actual[i], expected[i], itemPath);
+ if (diff.HasDifference)
+ {
+ return diff;
+ }
+ }
+
+ return new DiffResult(path, "", "", HasDifference: false);
+ }
+
+ private static DiffResult CompareJsonValues(JsonValue actual, JsonValue expected, string path)
+ {
+ var actualText = actual.ToJsonString();
+ var expectedText = expected.ToJsonString();
+
+ if (actualText != expectedText)
+ {
+ return new DiffResult(path, expectedText, actualText);
+ }
+
+ return new DiffResult(path, "", "", HasDifference: false);
+ }
+
+ private static string GetNodeTypeName(JsonNode? node)
+ {
+ return node switch
+ {
+ JsonObject => "Object",
+ JsonArray => "Array",
+ JsonValue => "Value",
+ _ => "null"
+ };
+ }
+
+ private static string FormatNode(JsonNode? node)
+ {
+ if (node is null)
+ {
+ return "null";
+ }
+
+ return node switch
+ {
+ JsonObject => "{...}",
+ JsonArray => "[...]",
+ JsonValue val => val.ToJsonString(),
+ _ => node.ToJsonString()
+ };
+ }
}
diff --git a/TUnit.Assertions/Conditions/Json/JsonNodeAssertionExtensions.cs b/TUnit.Assertions/Conditions/Json/JsonNodeAssertionExtensions.cs
index e22110e692..1a98798a7a 100644
--- a/TUnit.Assertions/Conditions/Json/JsonNodeAssertionExtensions.cs
+++ b/TUnit.Assertions/Conditions/Json/JsonNodeAssertionExtensions.cs
@@ -70,7 +70,10 @@ public static TUnit.Assertions.Core.AssertionResult IsDeepEqualTo(this JsonNode?
{
return TUnit.Assertions.Core.AssertionResult.Passed;
}
- return TUnit.Assertions.Core.AssertionResult.Failed($"found {value}");
+
+ var diff = JsonDiffHelper.FindFirstDifference(value, expected);
+ return TUnit.Assertions.Core.AssertionResult.Failed(
+ $"differs at {diff.Path}: expected {diff.Expected} but found {diff.Actual}");
}
[GenerateAssertion(ExpectationMessage = "to not be equal to {expected}")]
@@ -80,7 +83,7 @@ public static TUnit.Assertions.Core.AssertionResult IsNotDeepEqualTo(this JsonNo
{
return TUnit.Assertions.Core.AssertionResult.Passed;
}
- return TUnit.Assertions.Core.AssertionResult.Failed($"values are equal");
+ return TUnit.Assertions.Core.AssertionResult.Failed("values are equal");
}
#endif
}
From d6ef024df9cfd724e7118b14ff763922d0523aee Mon Sep 17 00:00:00 2001
From: Tom Longhurst <30480171+thomhurst@users.noreply.github.com>
Date: Sun, 28 Dec 2025 13:26:57 +0000
Subject: [PATCH 07/10] feat(assertions): add JsonArray count and empty
assertions
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
- Added IsJsonArrayEmpty, IsJsonArrayNotEmpty, HasJsonArrayCount
- Methods work on JsonNode? to avoid TUnit's collection assertion wrapping
- Includes type checking for non-array inputs
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude Opus 4.5
---
.../JsonNodeAssertionTests.cs | 46 +++++++++++++++++++
.../Json/JsonNodeAssertionExtensions.cs | 43 +++++++++++++++++
2 files changed, 89 insertions(+)
diff --git a/TUnit.Assertions.Tests/JsonNodeAssertionTests.cs b/TUnit.Assertions.Tests/JsonNodeAssertionTests.cs
index d76a3c8fea..dafb019332 100644
--- a/TUnit.Assertions.Tests/JsonNodeAssertionTests.cs
+++ b/TUnit.Assertions.Tests/JsonNodeAssertionTests.cs
@@ -80,6 +80,52 @@ public async Task DoesNotHaveJsonProperty_WhenPropertyExists_Fails()
async () => await Assert.That(node).DoesNotHaveJsonProperty("name"));
}
+ // JsonArray assertions - use JsonNode? to work with TUnit's assertion model
+ [Test]
+ public async Task IsJsonArrayEmpty_WithEmptyArray_Passes()
+ {
+ JsonNode? node = JsonNode.Parse("[]");
+ await Assert.That(node).IsJsonArrayEmpty();
+ }
+
+ [Test]
+ public async Task IsJsonArrayEmpty_WithNonEmptyArray_Fails()
+ {
+ JsonNode? node = JsonNode.Parse("[1, 2, 3]");
+ await Assert.ThrowsAsync(
+ async () => await Assert.That(node).IsJsonArrayEmpty());
+ }
+
+ [Test]
+ public async Task IsJsonArrayNotEmpty_WithNonEmptyArray_Passes()
+ {
+ JsonNode? node = JsonNode.Parse("[1, 2, 3]");
+ await Assert.That(node).IsJsonArrayNotEmpty();
+ }
+
+ [Test]
+ public async Task IsJsonArrayNotEmpty_WithEmptyArray_Fails()
+ {
+ JsonNode? node = JsonNode.Parse("[]");
+ await Assert.ThrowsAsync(
+ async () => await Assert.That(node).IsJsonArrayNotEmpty());
+ }
+
+ [Test]
+ public async Task HasJsonArrayCount_WithMatchingCount_Passes()
+ {
+ JsonNode? node = JsonNode.Parse("[1, 2, 3]");
+ await Assert.That(node).HasJsonArrayCount(3);
+ }
+
+ [Test]
+ public async Task HasJsonArrayCount_WithMismatchedCount_Fails()
+ {
+ JsonNode? node = JsonNode.Parse("[1, 2, 3]");
+ await Assert.ThrowsAsync(
+ async () => await Assert.That(node).HasJsonArrayCount(5));
+ }
+
#if NET8_0_OR_GREATER
[Test]
public async Task IsDeepEqualTo_WithIdenticalJson_Passes()
diff --git a/TUnit.Assertions/Conditions/Json/JsonNodeAssertionExtensions.cs b/TUnit.Assertions/Conditions/Json/JsonNodeAssertionExtensions.cs
index 1a98798a7a..23e8aa8bb7 100644
--- a/TUnit.Assertions/Conditions/Json/JsonNodeAssertionExtensions.cs
+++ b/TUnit.Assertions/Conditions/Json/JsonNodeAssertionExtensions.cs
@@ -62,6 +62,49 @@ public static TUnit.Assertions.Core.AssertionResult DoesNotHaveJsonProperty(this
return TUnit.Assertions.Core.AssertionResult.Failed($"property '{propertyName}' was found");
}
+ // JsonArray assertions - work on JsonNode? to avoid collection assertion wrapping issues
+ [GenerateAssertion(ExpectationMessage = "to be an empty JSON array")]
+ public static TUnit.Assertions.Core.AssertionResult IsJsonArrayEmpty(this JsonNode? value)
+ {
+ if (value is not JsonArray array)
+ {
+ return TUnit.Assertions.Core.AssertionResult.Failed($"found {value?.GetType().Name ?? "null"} instead of JsonArray");
+ }
+ if (array.Count == 0)
+ {
+ return TUnit.Assertions.Core.AssertionResult.Passed;
+ }
+ return TUnit.Assertions.Core.AssertionResult.Failed($"has {array.Count} elements");
+ }
+
+ [GenerateAssertion(ExpectationMessage = "to not be an empty JSON array")]
+ public static TUnit.Assertions.Core.AssertionResult IsJsonArrayNotEmpty(this JsonNode? value)
+ {
+ if (value is not JsonArray array)
+ {
+ return TUnit.Assertions.Core.AssertionResult.Failed($"found {value?.GetType().Name ?? "null"} instead of JsonArray");
+ }
+ if (array.Count > 0)
+ {
+ return TUnit.Assertions.Core.AssertionResult.Passed;
+ }
+ return TUnit.Assertions.Core.AssertionResult.Failed("array is empty");
+ }
+
+ [GenerateAssertion(ExpectationMessage = "to have {expected} elements")]
+ public static TUnit.Assertions.Core.AssertionResult HasJsonArrayCount(this JsonNode? value, int expected)
+ {
+ if (value is not JsonArray array)
+ {
+ return TUnit.Assertions.Core.AssertionResult.Failed($"found {value?.GetType().Name ?? "null"} instead of JsonArray");
+ }
+ if (array.Count == expected)
+ {
+ return TUnit.Assertions.Core.AssertionResult.Passed;
+ }
+ return TUnit.Assertions.Core.AssertionResult.Failed($"has {array.Count} elements");
+ }
+
#if NET8_0_OR_GREATER
[GenerateAssertion(ExpectationMessage = "to be equal to {expected}")]
public static TUnit.Assertions.Core.AssertionResult IsDeepEqualTo(this JsonNode? value, JsonNode? expected)
From 6f2ae5b0ca289f077f566331bf012620cfe12c2e Mon Sep 17 00:00:00 2001
From: Tom Longhurst <30480171+thomhurst@users.noreply.github.com>
Date: Sun, 28 Dec 2025 13:28:27 +0000
Subject: [PATCH 08/10] feat(assertions): add string JSON validation assertions
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
- Added IsValidJson, IsNotValidJson, IsValidJsonObject, IsValidJsonArray
- Provides detailed error messages for parsing failures
- Includes tests for valid, invalid, and type mismatch scenarios
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude Opus 4.5
---
.../JsonStringAssertionTests.cs | 82 +++++++++++++++++++
.../Json/JsonStringAssertionExtensions.cs | 75 +++++++++++++++++
2 files changed, 157 insertions(+)
create mode 100644 TUnit.Assertions.Tests/JsonStringAssertionTests.cs
create mode 100644 TUnit.Assertions/Conditions/Json/JsonStringAssertionExtensions.cs
diff --git a/TUnit.Assertions.Tests/JsonStringAssertionTests.cs b/TUnit.Assertions.Tests/JsonStringAssertionTests.cs
new file mode 100644
index 0000000000..18b4eecfbf
--- /dev/null
+++ b/TUnit.Assertions.Tests/JsonStringAssertionTests.cs
@@ -0,0 +1,82 @@
+using TUnit.Assertions.Extensions;
+
+namespace TUnit.Assertions.Tests;
+
+public class JsonStringAssertionTests
+{
+ [Test]
+ public async Task IsValidJson_WithValidJson_Passes()
+ {
+ var json = "{\"name\":\"Alice\"}";
+ await Assert.That(json).IsValidJson();
+ }
+
+ [Test]
+ public async Task IsValidJson_WithInvalidJson_Fails()
+ {
+ var json = "not valid json";
+ await Assert.ThrowsAsync(
+ async () => await Assert.That(json).IsValidJson());
+ }
+
+ [Test]
+ public async Task IsNotValidJson_WithInvalidJson_Passes()
+ {
+ var json = "not valid json";
+ await Assert.That(json).IsNotValidJson();
+ }
+
+ [Test]
+ public async Task IsNotValidJson_WithValidJson_Fails()
+ {
+ var json = "{\"name\":\"Alice\"}";
+ await Assert.ThrowsAsync(
+ async () => await Assert.That(json).IsNotValidJson());
+ }
+
+ [Test]
+ public async Task IsValidJsonObject_WithObject_Passes()
+ {
+ var json = "{\"name\":\"Alice\"}";
+ await Assert.That(json).IsValidJsonObject();
+ }
+
+ [Test]
+ public async Task IsValidJsonObject_WithArray_Fails()
+ {
+ var json = "[1,2,3]";
+ await Assert.ThrowsAsync(
+ async () => await Assert.That(json).IsValidJsonObject());
+ }
+
+ [Test]
+ public async Task IsValidJsonObject_WithInvalidJson_Fails()
+ {
+ var json = "not valid json";
+ await Assert.ThrowsAsync(
+ async () => await Assert.That(json).IsValidJsonObject());
+ }
+
+ [Test]
+ public async Task IsValidJsonArray_WithArray_Passes()
+ {
+ var json = "[1,2,3]";
+ await Assert.That(json).IsValidJsonArray();
+ }
+
+ [Test]
+ public async Task IsValidJsonArray_WithObject_Fails()
+ {
+ var json = "{\"name\":\"Alice\"}";
+ await Assert.ThrowsAsync(
+ async () => await Assert.That(json).IsValidJsonArray());
+ }
+
+ [Test]
+ public async Task IsValidJsonArray_WithInvalidJson_Fails()
+ {
+ var json = "not valid json";
+ await Assert.ThrowsAsync(
+ async () => await Assert.That(json).IsValidJsonArray());
+ }
+}
diff --git a/TUnit.Assertions/Conditions/Json/JsonStringAssertionExtensions.cs b/TUnit.Assertions/Conditions/Json/JsonStringAssertionExtensions.cs
new file mode 100644
index 0000000000..6d8a1fb25e
--- /dev/null
+++ b/TUnit.Assertions/Conditions/Json/JsonStringAssertionExtensions.cs
@@ -0,0 +1,75 @@
+using System.Text.Json;
+using TUnit.Assertions.Attributes;
+using TUnit.Assertions.Core;
+
+namespace TUnit.Assertions.Conditions.Json;
+
+///
+/// Source-generated assertions for validating JSON strings.
+///
+public static partial class JsonStringAssertionExtensions
+{
+ [GenerateAssertion(ExpectationMessage = "to be valid JSON")]
+ public static AssertionResult IsValidJson(this string value)
+ {
+ try
+ {
+ using var doc = JsonDocument.Parse(value);
+ return AssertionResult.Passed;
+ }
+ catch (JsonException ex)
+ {
+ return AssertionResult.Failed($"is not valid JSON: {ex.Message}");
+ }
+ }
+
+ [GenerateAssertion(ExpectationMessage = "to not be valid JSON")]
+ public static AssertionResult IsNotValidJson(this string value)
+ {
+ try
+ {
+ using var doc = JsonDocument.Parse(value);
+ return AssertionResult.Failed("is valid JSON");
+ }
+ catch (JsonException)
+ {
+ return AssertionResult.Passed;
+ }
+ }
+
+ [GenerateAssertion(ExpectationMessage = "to be a valid JSON object")]
+ public static AssertionResult IsValidJsonObject(this string value)
+ {
+ try
+ {
+ using var doc = JsonDocument.Parse(value);
+ if (doc.RootElement.ValueKind == JsonValueKind.Object)
+ {
+ return AssertionResult.Passed;
+ }
+ return AssertionResult.Failed($"is a {doc.RootElement.ValueKind}, not an Object");
+ }
+ catch (JsonException ex)
+ {
+ return AssertionResult.Failed($"is not valid JSON: {ex.Message}");
+ }
+ }
+
+ [GenerateAssertion(ExpectationMessage = "to be a valid JSON array")]
+ public static AssertionResult IsValidJsonArray(this string value)
+ {
+ try
+ {
+ using var doc = JsonDocument.Parse(value);
+ if (doc.RootElement.ValueKind == JsonValueKind.Array)
+ {
+ return AssertionResult.Passed;
+ }
+ return AssertionResult.Failed($"is a {doc.RootElement.ValueKind}, not an Array");
+ }
+ catch (JsonException ex)
+ {
+ return AssertionResult.Failed($"is not valid JSON: {ex.Message}");
+ }
+ }
+}
From 7336bf1f2158fad4dae5a624eb03e219a0cb32c4 Mon Sep 17 00:00:00 2001
From: Tom Longhurst <30480171+thomhurst@users.noreply.github.com>
Date: Sun, 28 Dec 2025 13:32:48 +0000
Subject: [PATCH 09/10] chore: update public API snapshots for JSON assertions
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude Opus 4.5
---
...Has_No_API_Changes.DotNet10_0.verified.txt | 246 ++++++++++++++++++
..._Has_No_API_Changes.DotNet8_0.verified.txt | 228 ++++++++++++++++
..._Has_No_API_Changes.DotNet9_0.verified.txt | 246 ++++++++++++++++++
...ary_Has_No_API_Changes.Net4_7.verified.txt | 210 +++++++++++++++
4 files changed, 930 insertions(+)
diff --git a/TUnit.PublicAPI/Tests.Assertions_Library_Has_No_API_Changes.DotNet10_0.verified.txt b/TUnit.PublicAPI/Tests.Assertions_Library_Has_No_API_Changes.DotNet10_0.verified.txt
index 193caf076a..1d4c70b02f 100644
--- a/TUnit.PublicAPI/Tests.Assertions_Library_Has_No_API_Changes.DotNet10_0.verified.txt
+++ b/TUnit.PublicAPI/Tests.Assertions_Library_Has_No_API_Changes.DotNet10_0.verified.txt
@@ -1380,6 +1380,68 @@ namespace .
}
}
namespace .
+{
+ public static class JsonElementAssertionExtensions
+ {
+ [.(ExpectationMessage="to not have property \'{propertyName}\'", InlineMethodBody=true)]
+ public static bool DoesNotHaveProperty(this . value, string propertyName) { }
+ [.(ExpectationMessage="to have property \'{propertyName}\'", InlineMethodBody=true)]
+ public static bool HasProperty(this . value, string propertyName) { }
+ [.(ExpectationMessage="to be a JSON array", InlineMethodBody=true)]
+ public static bool IsArray(this . value) { }
+ [.(ExpectationMessage="to be a JSON boolean", InlineMethodBody=true)]
+ public static bool IsBoolean(this . value) { }
+ [.(ExpectationMessage="to be deeply equal to {expected}")]
+ public static . IsDeepEqualTo(this . value, . expected) { }
+ [.(ExpectationMessage="to not be deeply equal to {expected}", InlineMethodBody=true)]
+ public static bool IsNotDeepEqualTo(this . value, . expected) { }
+ [.(ExpectationMessage="to not be JSON null", InlineMethodBody=true)]
+ public static bool IsNotNull(this . value) { }
+ [.(ExpectationMessage="to be JSON null", InlineMethodBody=true)]
+ public static bool IsNull(this . value) { }
+ [.(ExpectationMessage="to be a JSON number", InlineMethodBody=true)]
+ public static bool IsNumber(this . value) { }
+ [.(ExpectationMessage="to be a JSON object", InlineMethodBody=true)]
+ public static bool IsObject(this . value) { }
+ [.(ExpectationMessage="to be a JSON string", InlineMethodBody=true)]
+ public static bool IsString(this . value) { }
+ }
+ public static class JsonNodeAssertionExtensions
+ {
+ [.(ExpectationMessage="to not have property \'{propertyName}\'")]
+ public static . DoesNotHaveJsonProperty(this ..JsonNode? value, string propertyName) { }
+ [.(ExpectationMessage="to have {expected} elements")]
+ public static . HasJsonArrayCount(this ..JsonNode? value, int expected) { }
+ [.(ExpectationMessage="to have property \'{propertyName}\'")]
+ public static . HasJsonProperty(this ..JsonNode? value, string propertyName) { }
+ [.(ExpectationMessage="to be equal to {expected}")]
+ public static . IsDeepEqualTo(this ..JsonNode? value, ..JsonNode? expected) { }
+ [.(ExpectationMessage="to be a JsonArray")]
+ public static . IsJsonArray(this ..JsonNode? value) { }
+ [.(ExpectationMessage="to be an empty JSON array")]
+ public static . IsJsonArrayEmpty(this ..JsonNode? value) { }
+ [.(ExpectationMessage="to not be an empty JSON array")]
+ public static . IsJsonArrayNotEmpty(this ..JsonNode? value) { }
+ [.(ExpectationMessage="to be a JsonObject")]
+ public static . IsJsonObject(this ..JsonNode? value) { }
+ [.(ExpectationMessage="to be a JsonValue")]
+ public static . IsJsonValue(this ..JsonNode? value) { }
+ [.(ExpectationMessage="to not be equal to {expected}")]
+ public static . IsNotDeepEqualTo(this ..JsonNode? value, ..JsonNode? expected) { }
+ }
+ public static class JsonStringAssertionExtensions
+ {
+ [.(ExpectationMessage="to not be valid JSON")]
+ public static . IsNotValidJson(this string value) { }
+ [.(ExpectationMessage="to be valid JSON")]
+ public static . IsValidJson(this string value) { }
+ [.(ExpectationMessage="to be a valid JSON array")]
+ public static . IsValidJsonArray(this string value) { }
+ [.(ExpectationMessage="to be a valid JSON object")]
+ public static . IsValidJsonObject(this string value) { }
+ }
+}
+namespace .
{
public class CountWrapper : ., .
where TCollection : .
@@ -3138,6 +3200,166 @@ namespace .Extensions
public static . IsNotDefault(this . source)
where TValue : class { }
}
+ public static class JsonElementAssertionExtensions
+ {
+ public static ._DoesNotHaveProperty_String_Assertion DoesNotHaveProperty(this .<.> source, string propertyName, [.("propertyName")] string? propertyNameExpression = null) { }
+ public static ._HasProperty_String_Assertion HasProperty(this .<.> source, string propertyName, [.("propertyName")] string? propertyNameExpression = null) { }
+ public static ._IsArray_Assertion IsArray(this .<.> source) { }
+ public static ._IsBoolean_Assertion IsBoolean(this .<.> source) { }
+ public static ._IsDeepEqualTo_JsonElement_Assertion IsDeepEqualTo(this .<.> source, . expected, [.("expected")] string? expectedExpression = null) { }
+ public static ._IsNotDeepEqualTo_JsonElement_Assertion IsNotDeepEqualTo(this .<.> source, . expected, [.("expected")] string? expectedExpression = null) { }
+ public static ._IsNotNull_Assertion IsNotNull(this .<.> source) { }
+ public static ._IsNull_Assertion IsNull(this .<.> source) { }
+ public static ._IsNumber_Assertion IsNumber(this .<.> source) { }
+ public static ._IsObject_Assertion IsObject(this .<.> source) { }
+ public static ._IsString_Assertion IsString(this .<.> source) { }
+ }
+ public sealed class JsonElement_DoesNotHaveProperty_String_Assertion : .<.>
+ {
+ public JsonElement_DoesNotHaveProperty_String_Assertion(.<.> context, string propertyName) { }
+ protected override .<.> CheckAsync(.<.> metadata) { }
+ protected override string GetExpectation() { }
+ }
+ public sealed class JsonElement_HasProperty_String_Assertion : .<.>
+ {
+ public JsonElement_HasProperty_String_Assertion(.<.> context, string propertyName) { }
+ protected override .<.> CheckAsync(.<.> metadata) { }
+ protected override string GetExpectation() { }
+ }
+ public sealed class JsonElement_IsArray_Assertion : .<.>
+ {
+ public JsonElement_IsArray_Assertion(.<.> context) { }
+ protected override .<.> CheckAsync(.<.> metadata) { }
+ protected override string GetExpectation() { }
+ }
+ public sealed class JsonElement_IsBoolean_Assertion : .<.>
+ {
+ public JsonElement_IsBoolean_Assertion(.<.> context) { }
+ protected override .<.> CheckAsync(.<.> metadata) { }
+ protected override string GetExpectation() { }
+ }
+ public sealed class JsonElement_IsDeepEqualTo_JsonElement_Assertion : .<.>
+ {
+ public JsonElement_IsDeepEqualTo_JsonElement_Assertion(.<.> context, . expected) { }
+ protected override .<.> CheckAsync(.<.> metadata) { }
+ protected override string GetExpectation() { }
+ }
+ public sealed class JsonElement_IsNotDeepEqualTo_JsonElement_Assertion : .<.>
+ {
+ public JsonElement_IsNotDeepEqualTo_JsonElement_Assertion(.<.> context, . expected) { }
+ protected override .<.> CheckAsync(.<.> metadata) { }
+ protected override string GetExpectation() { }
+ }
+ public sealed class JsonElement_IsNotNull_Assertion : .<.>
+ {
+ public JsonElement_IsNotNull_Assertion(.<.> context) { }
+ protected override .<.> CheckAsync(.<.> metadata) { }
+ protected override string GetExpectation() { }
+ }
+ public sealed class JsonElement_IsNull_Assertion : .<.>
+ {
+ public JsonElement_IsNull_Assertion(.<.> context) { }
+ protected override .<.> CheckAsync(.<.> metadata) { }
+ protected override string GetExpectation() { }
+ }
+ public sealed class JsonElement_IsNumber_Assertion : .<.>
+ {
+ public JsonElement_IsNumber_Assertion(.<.> context) { }
+ protected override .<.> CheckAsync(.<.> metadata) { }
+ protected override string GetExpectation() { }
+ }
+ public sealed class JsonElement_IsObject_Assertion : .<.>
+ {
+ public JsonElement_IsObject_Assertion(.<.> context) { }
+ protected override .<.> CheckAsync(.<.> metadata) { }
+ protected override string GetExpectation() { }
+ }
+ public sealed class JsonElement_IsString_Assertion : .<.>
+ {
+ public JsonElement_IsString_Assertion(.<.> context) { }
+ protected override .<.> CheckAsync(.<.> metadata) { }
+ protected override string GetExpectation() { }
+ }
+ public static class JsonNodeAssertionExtensions
+ {
+ public static ._DoesNotHaveJsonProperty_String_Assertion DoesNotHaveJsonProperty(this .<..JsonNode?> source, string propertyName, [.("propertyName")] string? propertyNameExpression = null) { }
+ public static ._HasJsonArrayCount_Int_Assertion HasJsonArrayCount(this .<..JsonNode?> source, int expected, [.("expected")] string? expectedExpression = null) { }
+ public static ._HasJsonProperty_String_Assertion HasJsonProperty(this .<..JsonNode?> source, string propertyName, [.("propertyName")] string? propertyNameExpression = null) { }
+ public static ._IsDeepEqualTo_JsonNode_Assertion IsDeepEqualTo(this .<..JsonNode?> source, ..JsonNode? expected, [.("expected")] string? expectedExpression = null) { }
+ public static ._IsJsonArray_Assertion IsJsonArray(this .<..JsonNode?> source) { }
+ public static ._IsJsonArrayEmpty_Assertion IsJsonArrayEmpty(this .<..JsonNode?> source) { }
+ public static ._IsJsonArrayNotEmpty_Assertion IsJsonArrayNotEmpty(this .<..JsonNode?> source) { }
+ public static ._IsJsonObject_Assertion IsJsonObject(this .<..JsonNode?> source) { }
+ public static ._IsJsonValue_Assertion IsJsonValue(this .<..JsonNode?> source) { }
+ public static ._IsNotDeepEqualTo_JsonNode_Assertion IsNotDeepEqualTo(this .<..JsonNode?> source, ..JsonNode? expected, [.("expected")] string? expectedExpression = null) { }
+ }
+ public sealed class JsonNode_DoesNotHaveJsonProperty_String_Assertion : .<..JsonNode?>
+ {
+ public JsonNode_DoesNotHaveJsonProperty_String_Assertion(.<..JsonNode?> context, string propertyName) { }
+ protected override .<.> CheckAsync(.<..JsonNode?> metadata) { }
+ protected override string GetExpectation() { }
+ }
+ public sealed class JsonNode_HasJsonArrayCount_Int_Assertion : .<..JsonNode?>
+ {
+ public JsonNode_HasJsonArrayCount_Int_Assertion(.<..JsonNode?> context, int expected) { }
+ protected override .<.> CheckAsync(.<..JsonNode?> metadata) { }
+ protected override string GetExpectation() { }
+ }
+ public sealed class JsonNode_HasJsonProperty_String_Assertion : .<..JsonNode?>
+ {
+ public JsonNode_HasJsonProperty_String_Assertion(.<..JsonNode?> context, string propertyName) { }
+ protected override .<.> CheckAsync(.<..JsonNode?> metadata) { }
+ protected override string GetExpectation() { }
+ }
+ public sealed class JsonNode_IsDeepEqualTo_JsonNode_Assertion : .<..JsonNode?>
+ {
+ public JsonNode_IsDeepEqualTo_JsonNode_Assertion(.<..JsonNode?> context, ..JsonNode? expected) { }
+ protected override .<.> CheckAsync(.<..JsonNode?> metadata) { }
+ protected override string GetExpectation() { }
+ }
+ public sealed class JsonNode_IsJsonArrayEmpty_Assertion : .<..JsonNode?>
+ {
+ public JsonNode_IsJsonArrayEmpty_Assertion(.<..JsonNode?> context) { }
+ protected override .<.> CheckAsync(.<..JsonNode?> metadata) { }
+ protected override string GetExpectation() { }
+ }
+ public sealed class JsonNode_IsJsonArrayNotEmpty_Assertion : .<..JsonNode?>
+ {
+ public JsonNode_IsJsonArrayNotEmpty_Assertion(.<..JsonNode?> context) { }
+ protected override .<.> CheckAsync(.<..JsonNode?> metadata) { }
+ protected override string GetExpectation() { }
+ }
+ public sealed class JsonNode_IsJsonArray_Assertion : .<..JsonNode?>
+ {
+ public JsonNode_IsJsonArray_Assertion(.<..JsonNode?> context) { }
+ protected override .<.> CheckAsync(.<..JsonNode?> metadata) { }
+ protected override string GetExpectation() { }
+ }
+ public sealed class JsonNode_IsJsonObject_Assertion : .<..JsonNode?>
+ {
+ public JsonNode_IsJsonObject_Assertion(.<..JsonNode?> context) { }
+ protected override .<.> CheckAsync(.<..JsonNode?> metadata) { }
+ protected override string GetExpectation() { }
+ }
+ public sealed class JsonNode_IsJsonValue_Assertion : .<..JsonNode?>
+ {
+ public JsonNode_IsJsonValue_Assertion(.<..JsonNode?> context) { }
+ protected override .<.> CheckAsync(.<..JsonNode?> metadata) { }
+ protected override string GetExpectation() { }
+ }
+ public sealed class JsonNode_IsNotDeepEqualTo_JsonNode_Assertion : .<..JsonNode?>
+ {
+ public JsonNode_IsNotDeepEqualTo_JsonNode_Assertion(.<..JsonNode?> context, ..JsonNode? expected) { }
+ protected override .<.> CheckAsync(.<..JsonNode?> metadata) { }
+ protected override string GetExpectation() { }
+ }
+ public static class JsonStringAssertionExtensions
+ {
+ public static ._IsNotValidJson_Assertion IsNotValidJson(this . source) { }
+ public static ._IsValidJson_Assertion IsValidJson(this . source) { }
+ public static ._IsValidJsonArray_Assertion IsValidJsonArray(this . source) { }
+ public static ._IsValidJsonObject_Assertion IsValidJsonObject(this . source) { }
+ }
public static class LazyAssertionExtensions
{
[.("Trimming", "IL2091", Justification="Generic type parameter is only used for property access, not instantiation")]
@@ -3643,6 +3865,30 @@ namespace .Extensions
public static . IsNullOrEmpty(this . source) { }
public static . IsNullOrWhiteSpace(this . source) { }
}
+ public sealed class String_IsNotValidJson_Assertion : .
+ {
+ public String_IsNotValidJson_Assertion(. context) { }
+ protected override .<.> CheckAsync(. metadata) { }
+ protected override string GetExpectation() { }
+ }
+ public sealed class String_IsValidJsonArray_Assertion : .
+ {
+ public String_IsValidJsonArray_Assertion(. context) { }
+ protected override .<.> CheckAsync(. metadata) { }
+ protected override string GetExpectation() { }
+ }
+ public sealed class String_IsValidJsonObject_Assertion : .
+ {
+ public String_IsValidJsonObject_Assertion(. context) { }
+ protected override .<.> CheckAsync(. metadata) { }
+ protected override string GetExpectation() { }
+ }
+ public sealed class String_IsValidJson_Assertion : .
+ {
+ public String_IsValidJson_Assertion(. context) { }
+ protected override .<.> CheckAsync(. metadata) { }
+ protected override string GetExpectation() { }
+ }
[.("Trimming", "IL2091", Justification="Generic type parameter is only used for property access, not instantiation")]
public sealed class T_IsIn_IEnumerableT_Assertion : .
{
diff --git a/TUnit.PublicAPI/Tests.Assertions_Library_Has_No_API_Changes.DotNet8_0.verified.txt b/TUnit.PublicAPI/Tests.Assertions_Library_Has_No_API_Changes.DotNet8_0.verified.txt
index cdf708e41b..406d0957cf 100644
--- a/TUnit.PublicAPI/Tests.Assertions_Library_Has_No_API_Changes.DotNet8_0.verified.txt
+++ b/TUnit.PublicAPI/Tests.Assertions_Library_Has_No_API_Changes.DotNet8_0.verified.txt
@@ -1375,6 +1375,64 @@ namespace .
}
}
namespace .
+{
+ public static class JsonElementAssertionExtensions
+ {
+ [.(ExpectationMessage="to not have property \'{propertyName}\'", InlineMethodBody=true)]
+ public static bool DoesNotHaveProperty(this . value, string propertyName) { }
+ [.(ExpectationMessage="to have property \'{propertyName}\'", InlineMethodBody=true)]
+ public static bool HasProperty(this . value, string propertyName) { }
+ [.(ExpectationMessage="to be a JSON array", InlineMethodBody=true)]
+ public static bool IsArray(this . value) { }
+ [.(ExpectationMessage="to be a JSON boolean", InlineMethodBody=true)]
+ public static bool IsBoolean(this . value) { }
+ [.(ExpectationMessage="to not be JSON null", InlineMethodBody=true)]
+ public static bool IsNotNull(this . value) { }
+ [.(ExpectationMessage="to be JSON null", InlineMethodBody=true)]
+ public static bool IsNull(this . value) { }
+ [.(ExpectationMessage="to be a JSON number", InlineMethodBody=true)]
+ public static bool IsNumber(this . value) { }
+ [.(ExpectationMessage="to be a JSON object", InlineMethodBody=true)]
+ public static bool IsObject(this . value) { }
+ [.(ExpectationMessage="to be a JSON string", InlineMethodBody=true)]
+ public static bool IsString(this . value) { }
+ }
+ public static class JsonNodeAssertionExtensions
+ {
+ [.(ExpectationMessage="to not have property \'{propertyName}\'")]
+ public static . DoesNotHaveJsonProperty(this ..JsonNode? value, string propertyName) { }
+ [.(ExpectationMessage="to have {expected} elements")]
+ public static . HasJsonArrayCount(this ..JsonNode? value, int expected) { }
+ [.(ExpectationMessage="to have property \'{propertyName}\'")]
+ public static . HasJsonProperty(this ..JsonNode? value, string propertyName) { }
+ [.(ExpectationMessage="to be equal to {expected}")]
+ public static . IsDeepEqualTo(this ..JsonNode? value, ..JsonNode? expected) { }
+ [.(ExpectationMessage="to be a JsonArray")]
+ public static . IsJsonArray(this ..JsonNode? value) { }
+ [.(ExpectationMessage="to be an empty JSON array")]
+ public static . IsJsonArrayEmpty(this ..JsonNode? value) { }
+ [.(ExpectationMessage="to not be an empty JSON array")]
+ public static . IsJsonArrayNotEmpty(this ..JsonNode? value) { }
+ [.(ExpectationMessage="to be a JsonObject")]
+ public static . IsJsonObject(this ..JsonNode? value) { }
+ [.(ExpectationMessage="to be a JsonValue")]
+ public static . IsJsonValue(this ..JsonNode? value) { }
+ [.(ExpectationMessage="to not be equal to {expected}")]
+ public static . IsNotDeepEqualTo(this ..JsonNode? value, ..JsonNode? expected) { }
+ }
+ public static class JsonStringAssertionExtensions
+ {
+ [.(ExpectationMessage="to not be valid JSON")]
+ public static . IsNotValidJson(this string value) { }
+ [.(ExpectationMessage="to be valid JSON")]
+ public static . IsValidJson(this string value) { }
+ [.(ExpectationMessage="to be a valid JSON array")]
+ public static . IsValidJsonArray(this string value) { }
+ [.(ExpectationMessage="to be a valid JSON object")]
+ public static . IsValidJsonObject(this string value) { }
+ }
+}
+namespace .
{
public class CountWrapper : ., .
where TCollection : .
@@ -3119,6 +3177,152 @@ namespace .Extensions
public static . IsNotDefault(this . source)
where TValue : class { }
}
+ public static class JsonElementAssertionExtensions
+ {
+ public static ._DoesNotHaveProperty_String_Assertion DoesNotHaveProperty(this .<.> source, string propertyName, [.("propertyName")] string? propertyNameExpression = null) { }
+ public static ._HasProperty_String_Assertion HasProperty(this .<.> source, string propertyName, [.("propertyName")] string? propertyNameExpression = null) { }
+ public static ._IsArray_Assertion IsArray(this .<.> source) { }
+ public static ._IsBoolean_Assertion IsBoolean(this .<.> source) { }
+ public static ._IsNotNull_Assertion IsNotNull(this .<.> source) { }
+ public static ._IsNull_Assertion IsNull(this .<.> source) { }
+ public static ._IsNumber_Assertion IsNumber(this .<.> source) { }
+ public static ._IsObject_Assertion IsObject(this .<.> source) { }
+ public static ._IsString_Assertion IsString(this .<.> source) { }
+ }
+ public sealed class JsonElement_DoesNotHaveProperty_String_Assertion : .<.>
+ {
+ public JsonElement_DoesNotHaveProperty_String_Assertion(.<.> context, string propertyName) { }
+ protected override .<.> CheckAsync(.<.> metadata) { }
+ protected override string GetExpectation() { }
+ }
+ public sealed class JsonElement_HasProperty_String_Assertion : .<.>
+ {
+ public JsonElement_HasProperty_String_Assertion(.<.> context, string propertyName) { }
+ protected override .<.> CheckAsync(.<.> metadata) { }
+ protected override string GetExpectation() { }
+ }
+ public sealed class JsonElement_IsArray_Assertion : .<.>
+ {
+ public JsonElement_IsArray_Assertion(.<.> context) { }
+ protected override .<.> CheckAsync(.<.> metadata) { }
+ protected override string GetExpectation() { }
+ }
+ public sealed class JsonElement_IsBoolean_Assertion : .<.>
+ {
+ public JsonElement_IsBoolean_Assertion(.<.> context) { }
+ protected override .<.> CheckAsync(.<.> metadata) { }
+ protected override string GetExpectation() { }
+ }
+ public sealed class JsonElement_IsNotNull_Assertion : .<.>
+ {
+ public JsonElement_IsNotNull_Assertion(.<.> context) { }
+ protected override .<.> CheckAsync(.<.> metadata) { }
+ protected override string GetExpectation() { }
+ }
+ public sealed class JsonElement_IsNull_Assertion : .<.>
+ {
+ public JsonElement_IsNull_Assertion(.<.> context) { }
+ protected override .<.> CheckAsync(.<.> metadata) { }
+ protected override string GetExpectation() { }
+ }
+ public sealed class JsonElement_IsNumber_Assertion : .<.>
+ {
+ public JsonElement_IsNumber_Assertion(.<.> context) { }
+ protected override .<.> CheckAsync(.<.> metadata) { }
+ protected override string GetExpectation() { }
+ }
+ public sealed class JsonElement_IsObject_Assertion : .<.>
+ {
+ public JsonElement_IsObject_Assertion(.<.> context) { }
+ protected override .<.> CheckAsync(.<.> metadata) { }
+ protected override string GetExpectation() { }
+ }
+ public sealed class JsonElement_IsString_Assertion : .<.>
+ {
+ public JsonElement_IsString_Assertion(.<.> context) { }
+ protected override .<.> CheckAsync(.<.> metadata) { }
+ protected override string GetExpectation() { }
+ }
+ public static class JsonNodeAssertionExtensions
+ {
+ public static ._DoesNotHaveJsonProperty_String_Assertion DoesNotHaveJsonProperty(this .<..JsonNode?> source, string propertyName, [.("propertyName")] string? propertyNameExpression = null) { }
+ public static ._HasJsonArrayCount_Int_Assertion HasJsonArrayCount(this .<..JsonNode?> source, int expected, [.("expected")] string? expectedExpression = null) { }
+ public static ._HasJsonProperty_String_Assertion HasJsonProperty(this .<..JsonNode?> source, string propertyName, [.("propertyName")] string? propertyNameExpression = null) { }
+ public static ._IsDeepEqualTo_JsonNode_Assertion IsDeepEqualTo(this .<..JsonNode?> source, ..JsonNode? expected, [.("expected")] string? expectedExpression = null) { }
+ public static ._IsJsonArray_Assertion IsJsonArray(this .<..JsonNode?> source) { }
+ public static ._IsJsonArrayEmpty_Assertion IsJsonArrayEmpty(this .<..JsonNode?> source) { }
+ public static ._IsJsonArrayNotEmpty_Assertion IsJsonArrayNotEmpty(this .<..JsonNode?> source) { }
+ public static ._IsJsonObject_Assertion IsJsonObject(this .<..JsonNode?> source) { }
+ public static ._IsJsonValue_Assertion IsJsonValue(this .<..JsonNode?> source) { }
+ public static ._IsNotDeepEqualTo_JsonNode_Assertion IsNotDeepEqualTo(this .<..JsonNode?> source, ..JsonNode? expected, [.("expected")] string? expectedExpression = null) { }
+ }
+ public sealed class JsonNode_DoesNotHaveJsonProperty_String_Assertion : .<..JsonNode?>
+ {
+ public JsonNode_DoesNotHaveJsonProperty_String_Assertion(.<..JsonNode?> context, string propertyName) { }
+ protected override .<.> CheckAsync(.<..JsonNode?> metadata) { }
+ protected override string GetExpectation() { }
+ }
+ public sealed class JsonNode_HasJsonArrayCount_Int_Assertion : .<..JsonNode?>
+ {
+ public JsonNode_HasJsonArrayCount_Int_Assertion(.<..JsonNode?> context, int expected) { }
+ protected override .<.> CheckAsync(.<..JsonNode?> metadata) { }
+ protected override string GetExpectation() { }
+ }
+ public sealed class JsonNode_HasJsonProperty_String_Assertion : .<..JsonNode?>
+ {
+ public JsonNode_HasJsonProperty_String_Assertion(.<..JsonNode?> context, string propertyName) { }
+ protected override .<.> CheckAsync(.<..JsonNode?> metadata) { }
+ protected override string GetExpectation() { }
+ }
+ public sealed class JsonNode_IsDeepEqualTo_JsonNode_Assertion : .<..JsonNode?>
+ {
+ public JsonNode_IsDeepEqualTo_JsonNode_Assertion(.<..JsonNode?> context, ..JsonNode? expected) { }
+ protected override .<.> CheckAsync(.<..JsonNode?> metadata) { }
+ protected override string GetExpectation() { }
+ }
+ public sealed class JsonNode_IsJsonArrayEmpty_Assertion : .<..JsonNode?>
+ {
+ public JsonNode_IsJsonArrayEmpty_Assertion(.<..JsonNode?> context) { }
+ protected override .<.> CheckAsync(.<..JsonNode?> metadata) { }
+ protected override string GetExpectation() { }
+ }
+ public sealed class JsonNode_IsJsonArrayNotEmpty_Assertion : .<..JsonNode?>
+ {
+ public JsonNode_IsJsonArrayNotEmpty_Assertion(.<..JsonNode?> context) { }
+ protected override .<.> CheckAsync(.<..JsonNode?> metadata) { }
+ protected override string GetExpectation() { }
+ }
+ public sealed class JsonNode_IsJsonArray_Assertion : .<..JsonNode?>
+ {
+ public JsonNode_IsJsonArray_Assertion(.<..JsonNode?> context) { }
+ protected override .<.> CheckAsync(.<..JsonNode?> metadata) { }
+ protected override string GetExpectation() { }
+ }
+ public sealed class JsonNode_IsJsonObject_Assertion : .<..JsonNode?>
+ {
+ public JsonNode_IsJsonObject_Assertion(.<..JsonNode?> context) { }
+ protected override .<.> CheckAsync(.<..JsonNode?> metadata) { }
+ protected override string GetExpectation() { }
+ }
+ public sealed class JsonNode_IsJsonValue_Assertion : .<..JsonNode?>
+ {
+ public JsonNode_IsJsonValue_Assertion(.<..JsonNode?> context) { }
+ protected override .<.> CheckAsync(.<..JsonNode?> metadata) { }
+ protected override string GetExpectation() { }
+ }
+ public sealed class JsonNode_IsNotDeepEqualTo_JsonNode_Assertion : .<..JsonNode?>
+ {
+ public JsonNode_IsNotDeepEqualTo_JsonNode_Assertion(.<..JsonNode?> context, ..JsonNode? expected) { }
+ protected override .<.> CheckAsync(.<..JsonNode?> metadata) { }
+ protected override string GetExpectation() { }
+ }
+ public static class JsonStringAssertionExtensions
+ {
+ public static ._IsNotValidJson_Assertion IsNotValidJson(this . source) { }
+ public static ._IsValidJson_Assertion IsValidJson(this . source) { }
+ public static ._IsValidJsonArray_Assertion IsValidJsonArray(this . source) { }
+ public static ._IsValidJsonObject_Assertion IsValidJsonObject(this . source) { }
+ }
public static class LazyAssertionExtensions
{
[.("Trimming", "IL2091", Justification="Generic type parameter is only used for property access, not instantiation")]
@@ -3623,6 +3827,30 @@ namespace .Extensions
public static . IsNullOrEmpty(this . source) { }
public static . IsNullOrWhiteSpace(this . source) { }
}
+ public sealed class String_IsNotValidJson_Assertion : .
+ {
+ public String_IsNotValidJson_Assertion(. context) { }
+ protected override .<.> CheckAsync(. metadata) { }
+ protected override string GetExpectation() { }
+ }
+ public sealed class String_IsValidJsonArray_Assertion : .
+ {
+ public String_IsValidJsonArray_Assertion(. context) { }
+ protected override .<.> CheckAsync(. metadata) { }
+ protected override string GetExpectation() { }
+ }
+ public sealed class String_IsValidJsonObject_Assertion : .
+ {
+ public String_IsValidJsonObject_Assertion(. context) { }
+ protected override .<.> CheckAsync(. metadata) { }
+ protected override string GetExpectation() { }
+ }
+ public sealed class String_IsValidJson_Assertion : .
+ {
+ public String_IsValidJson_Assertion(. context) { }
+ protected override .<.> CheckAsync(. metadata) { }
+ protected override string GetExpectation() { }
+ }
[.("Trimming", "IL2091", Justification="Generic type parameter is only used for property access, not instantiation")]
public sealed class T_IsIn_IEnumerableT_Assertion : .
{
diff --git a/TUnit.PublicAPI/Tests.Assertions_Library_Has_No_API_Changes.DotNet9_0.verified.txt b/TUnit.PublicAPI/Tests.Assertions_Library_Has_No_API_Changes.DotNet9_0.verified.txt
index de701714b4..30986ae454 100644
--- a/TUnit.PublicAPI/Tests.Assertions_Library_Has_No_API_Changes.DotNet9_0.verified.txt
+++ b/TUnit.PublicAPI/Tests.Assertions_Library_Has_No_API_Changes.DotNet9_0.verified.txt
@@ -1380,6 +1380,68 @@ namespace .
}
}
namespace .
+{
+ public static class JsonElementAssertionExtensions
+ {
+ [.(ExpectationMessage="to not have property \'{propertyName}\'", InlineMethodBody=true)]
+ public static bool DoesNotHaveProperty(this . value, string propertyName) { }
+ [.(ExpectationMessage="to have property \'{propertyName}\'", InlineMethodBody=true)]
+ public static bool HasProperty(this . value, string propertyName) { }
+ [.(ExpectationMessage="to be a JSON array", InlineMethodBody=true)]
+ public static bool IsArray(this . value) { }
+ [.(ExpectationMessage="to be a JSON boolean", InlineMethodBody=true)]
+ public static bool IsBoolean(this . value) { }
+ [.(ExpectationMessage="to be deeply equal to {expected}")]
+ public static . IsDeepEqualTo(this . value, . expected) { }
+ [.(ExpectationMessage="to not be deeply equal to {expected}", InlineMethodBody=true)]
+ public static bool IsNotDeepEqualTo(this . value, . expected) { }
+ [.(ExpectationMessage="to not be JSON null", InlineMethodBody=true)]
+ public static bool IsNotNull(this . value) { }
+ [.(ExpectationMessage="to be JSON null", InlineMethodBody=true)]
+ public static bool IsNull(this . value) { }
+ [.(ExpectationMessage="to be a JSON number", InlineMethodBody=true)]
+ public static bool IsNumber(this . value) { }
+ [.(ExpectationMessage="to be a JSON object", InlineMethodBody=true)]
+ public static bool IsObject(this . value) { }
+ [.(ExpectationMessage="to be a JSON string", InlineMethodBody=true)]
+ public static bool IsString(this . value) { }
+ }
+ public static class JsonNodeAssertionExtensions
+ {
+ [.(ExpectationMessage="to not have property \'{propertyName}\'")]
+ public static . DoesNotHaveJsonProperty(this ..JsonNode? value, string propertyName) { }
+ [.(ExpectationMessage="to have {expected} elements")]
+ public static . HasJsonArrayCount(this ..JsonNode? value, int expected) { }
+ [.(ExpectationMessage="to have property \'{propertyName}\'")]
+ public static . HasJsonProperty(this ..JsonNode? value, string propertyName) { }
+ [.(ExpectationMessage="to be equal to {expected}")]
+ public static . IsDeepEqualTo(this ..JsonNode? value, ..JsonNode? expected) { }
+ [.(ExpectationMessage="to be a JsonArray")]
+ public static . IsJsonArray(this ..JsonNode? value) { }
+ [.(ExpectationMessage="to be an empty JSON array")]
+ public static . IsJsonArrayEmpty(this ..JsonNode? value) { }
+ [.(ExpectationMessage="to not be an empty JSON array")]
+ public static . IsJsonArrayNotEmpty(this ..JsonNode? value) { }
+ [.(ExpectationMessage="to be a JsonObject")]
+ public static . IsJsonObject(this ..JsonNode? value) { }
+ [.(ExpectationMessage="to be a JsonValue")]
+ public static . IsJsonValue(this ..JsonNode? value) { }
+ [.(ExpectationMessage="to not be equal to {expected}")]
+ public static . IsNotDeepEqualTo(this ..JsonNode? value, ..JsonNode? expected) { }
+ }
+ public static class JsonStringAssertionExtensions
+ {
+ [.(ExpectationMessage="to not be valid JSON")]
+ public static . IsNotValidJson(this string value) { }
+ [.(ExpectationMessage="to be valid JSON")]
+ public static . IsValidJson(this string value) { }
+ [.(ExpectationMessage="to be a valid JSON array")]
+ public static . IsValidJsonArray(this string value) { }
+ [.(ExpectationMessage="to be a valid JSON object")]
+ public static . IsValidJsonObject(this string value) { }
+ }
+}
+namespace .
{
public class CountWrapper : ., .
where TCollection : .
@@ -3138,6 +3200,166 @@ namespace .Extensions
public static . IsNotDefault(this . source)
where TValue : class { }
}
+ public static class JsonElementAssertionExtensions
+ {
+ public static ._DoesNotHaveProperty_String_Assertion DoesNotHaveProperty(this .<.> source, string propertyName, [.("propertyName")] string? propertyNameExpression = null) { }
+ public static ._HasProperty_String_Assertion HasProperty(this .<.> source, string propertyName, [.("propertyName")] string? propertyNameExpression = null) { }
+ public static ._IsArray_Assertion IsArray(this .<.> source) { }
+ public static ._IsBoolean_Assertion IsBoolean(this .<.> source) { }
+ public static ._IsDeepEqualTo_JsonElement_Assertion IsDeepEqualTo(this .<.> source, . expected, [.("expected")] string? expectedExpression = null) { }
+ public static ._IsNotDeepEqualTo_JsonElement_Assertion IsNotDeepEqualTo(this .<.> source, . expected, [.("expected")] string? expectedExpression = null) { }
+ public static ._IsNotNull_Assertion IsNotNull(this .<.> source) { }
+ public static ._IsNull_Assertion IsNull(this .<.> source) { }
+ public static ._IsNumber_Assertion IsNumber(this .<.> source) { }
+ public static ._IsObject_Assertion IsObject(this .<.> source) { }
+ public static ._IsString_Assertion IsString(this .<.> source) { }
+ }
+ public sealed class JsonElement_DoesNotHaveProperty_String_Assertion : .<.>
+ {
+ public JsonElement_DoesNotHaveProperty_String_Assertion(.<.> context, string propertyName) { }
+ protected override .<.> CheckAsync(.<.> metadata) { }
+ protected override string GetExpectation() { }
+ }
+ public sealed class JsonElement_HasProperty_String_Assertion : .<.>
+ {
+ public JsonElement_HasProperty_String_Assertion(.<.> context, string propertyName) { }
+ protected override .<.> CheckAsync(.<.> metadata) { }
+ protected override string GetExpectation() { }
+ }
+ public sealed class JsonElement_IsArray_Assertion : .<.>
+ {
+ public JsonElement_IsArray_Assertion(.<.> context) { }
+ protected override .<.> CheckAsync(.<.> metadata) { }
+ protected override string GetExpectation() { }
+ }
+ public sealed class JsonElement_IsBoolean_Assertion : .<.>
+ {
+ public JsonElement_IsBoolean_Assertion(.<.> context) { }
+ protected override .<.> CheckAsync(.<.> metadata) { }
+ protected override string GetExpectation() { }
+ }
+ public sealed class JsonElement_IsDeepEqualTo_JsonElement_Assertion : .<.>
+ {
+ public JsonElement_IsDeepEqualTo_JsonElement_Assertion(.<.> context, . expected) { }
+ protected override .<.> CheckAsync(.<.> metadata) { }
+ protected override string GetExpectation() { }
+ }
+ public sealed class JsonElement_IsNotDeepEqualTo_JsonElement_Assertion : .<.>
+ {
+ public JsonElement_IsNotDeepEqualTo_JsonElement_Assertion(.<.> context, . expected) { }
+ protected override .<.> CheckAsync(.<.> metadata) { }
+ protected override string GetExpectation() { }
+ }
+ public sealed class JsonElement_IsNotNull_Assertion : .<.>
+ {
+ public JsonElement_IsNotNull_Assertion(.<.> context) { }
+ protected override .<.> CheckAsync(.<.> metadata) { }
+ protected override string GetExpectation() { }
+ }
+ public sealed class JsonElement_IsNull_Assertion : .<.>
+ {
+ public JsonElement_IsNull_Assertion(.<.> context) { }
+ protected override .<.> CheckAsync(.<.> metadata) { }
+ protected override string GetExpectation() { }
+ }
+ public sealed class JsonElement_IsNumber_Assertion : .<.>
+ {
+ public JsonElement_IsNumber_Assertion(.<.> context) { }
+ protected override .<.> CheckAsync(.<.> metadata) { }
+ protected override string GetExpectation() { }
+ }
+ public sealed class JsonElement_IsObject_Assertion : .<.>
+ {
+ public JsonElement_IsObject_Assertion(.<.> context) { }
+ protected override .<.> CheckAsync(.<.> metadata) { }
+ protected override string GetExpectation() { }
+ }
+ public sealed class JsonElement_IsString_Assertion : .<.>
+ {
+ public JsonElement_IsString_Assertion(.<.> context) { }
+ protected override .<.> CheckAsync(.<.> metadata) { }
+ protected override string GetExpectation() { }
+ }
+ public static class JsonNodeAssertionExtensions
+ {
+ public static ._DoesNotHaveJsonProperty_String_Assertion DoesNotHaveJsonProperty(this .<..JsonNode?> source, string propertyName, [.("propertyName")] string? propertyNameExpression = null) { }
+ public static ._HasJsonArrayCount_Int_Assertion HasJsonArrayCount(this .<..JsonNode?> source, int expected, [.("expected")] string? expectedExpression = null) { }
+ public static ._HasJsonProperty_String_Assertion HasJsonProperty(this .<..JsonNode?> source, string propertyName, [.("propertyName")] string? propertyNameExpression = null) { }
+ public static ._IsDeepEqualTo_JsonNode_Assertion IsDeepEqualTo(this .<..JsonNode?> source, ..JsonNode? expected, [.("expected")] string? expectedExpression = null) { }
+ public static ._IsJsonArray_Assertion IsJsonArray(this .<..JsonNode?> source) { }
+ public static ._IsJsonArrayEmpty_Assertion IsJsonArrayEmpty(this .<..JsonNode?> source) { }
+ public static ._IsJsonArrayNotEmpty_Assertion IsJsonArrayNotEmpty(this .<..JsonNode?> source) { }
+ public static ._IsJsonObject_Assertion IsJsonObject(this .<..JsonNode?> source) { }
+ public static ._IsJsonValue_Assertion IsJsonValue(this .<..JsonNode?> source) { }
+ public static ._IsNotDeepEqualTo_JsonNode_Assertion IsNotDeepEqualTo(this .<..JsonNode?> source, ..JsonNode? expected, [.("expected")] string? expectedExpression = null) { }
+ }
+ public sealed class JsonNode_DoesNotHaveJsonProperty_String_Assertion : .<..JsonNode?>
+ {
+ public JsonNode_DoesNotHaveJsonProperty_String_Assertion(.<..JsonNode?> context, string propertyName) { }
+ protected override .<.> CheckAsync(.<..JsonNode?> metadata) { }
+ protected override string GetExpectation() { }
+ }
+ public sealed class JsonNode_HasJsonArrayCount_Int_Assertion : .<..JsonNode?>
+ {
+ public JsonNode_HasJsonArrayCount_Int_Assertion(.<..JsonNode?> context, int expected) { }
+ protected override .<.> CheckAsync(.<..JsonNode?> metadata) { }
+ protected override string GetExpectation() { }
+ }
+ public sealed class JsonNode_HasJsonProperty_String_Assertion : .<..JsonNode?>
+ {
+ public JsonNode_HasJsonProperty_String_Assertion(.<..JsonNode?> context, string propertyName) { }
+ protected override .<.> CheckAsync(.<..JsonNode?> metadata) { }
+ protected override string GetExpectation() { }
+ }
+ public sealed class JsonNode_IsDeepEqualTo_JsonNode_Assertion : .<..JsonNode?>
+ {
+ public JsonNode_IsDeepEqualTo_JsonNode_Assertion(.<..JsonNode?> context, ..JsonNode? expected) { }
+ protected override .<.> CheckAsync(.<..JsonNode?> metadata) { }
+ protected override string GetExpectation() { }
+ }
+ public sealed class JsonNode_IsJsonArrayEmpty_Assertion : .<..JsonNode?>
+ {
+ public JsonNode_IsJsonArrayEmpty_Assertion(.<..JsonNode?> context) { }
+ protected override .<.> CheckAsync(.<..JsonNode?> metadata) { }
+ protected override string GetExpectation() { }
+ }
+ public sealed class JsonNode_IsJsonArrayNotEmpty_Assertion : .<..JsonNode?>
+ {
+ public JsonNode_IsJsonArrayNotEmpty_Assertion(.<..JsonNode?> context) { }
+ protected override .<.> CheckAsync(.<..JsonNode?> metadata) { }
+ protected override string GetExpectation() { }
+ }
+ public sealed class JsonNode_IsJsonArray_Assertion : .<..JsonNode?>
+ {
+ public JsonNode_IsJsonArray_Assertion(.<..JsonNode?> context) { }
+ protected override .<.> CheckAsync(.<..JsonNode?> metadata) { }
+ protected override string GetExpectation() { }
+ }
+ public sealed class JsonNode_IsJsonObject_Assertion : .<..JsonNode?>
+ {
+ public JsonNode_IsJsonObject_Assertion(.<..JsonNode?> context) { }
+ protected override .<.> CheckAsync(.<..JsonNode?> metadata) { }
+ protected override string GetExpectation() { }
+ }
+ public sealed class JsonNode_IsJsonValue_Assertion : .<..JsonNode?>
+ {
+ public JsonNode_IsJsonValue_Assertion(.<..JsonNode?> context) { }
+ protected override .<.> CheckAsync(.<..JsonNode?> metadata) { }
+ protected override string GetExpectation() { }
+ }
+ public sealed class JsonNode_IsNotDeepEqualTo_JsonNode_Assertion : .<..JsonNode?>
+ {
+ public JsonNode_IsNotDeepEqualTo_JsonNode_Assertion(.<..JsonNode?> context, ..JsonNode? expected) { }
+ protected override .<.> CheckAsync(.<..JsonNode?> metadata) { }
+ protected override string GetExpectation() { }
+ }
+ public static class JsonStringAssertionExtensions
+ {
+ public static ._IsNotValidJson_Assertion IsNotValidJson(this . source) { }
+ public static ._IsValidJson_Assertion IsValidJson(this . source) { }
+ public static ._IsValidJsonArray_Assertion IsValidJsonArray(this . source) { }
+ public static ._IsValidJsonObject_Assertion IsValidJsonObject(this . source) { }
+ }
public static class LazyAssertionExtensions
{
[.("Trimming", "IL2091", Justification="Generic type parameter is only used for property access, not instantiation")]
@@ -3643,6 +3865,30 @@ namespace .Extensions
public static . IsNullOrEmpty(this . source) { }
public static . IsNullOrWhiteSpace(this . source) { }
}
+ public sealed class String_IsNotValidJson_Assertion : .
+ {
+ public String_IsNotValidJson_Assertion(. context) { }
+ protected override .<.> CheckAsync(. metadata) { }
+ protected override string GetExpectation() { }
+ }
+ public sealed class String_IsValidJsonArray_Assertion : .
+ {
+ public String_IsValidJsonArray_Assertion(. context) { }
+ protected override .<.> CheckAsync(. metadata) { }
+ protected override string GetExpectation() { }
+ }
+ public sealed class String_IsValidJsonObject_Assertion : .
+ {
+ public String_IsValidJsonObject_Assertion(. context) { }
+ protected override .<.> CheckAsync(. metadata) { }
+ protected override string GetExpectation() { }
+ }
+ public sealed class String_IsValidJson_Assertion : .
+ {
+ public String_IsValidJson_Assertion(. context) { }
+ protected override .<.> CheckAsync(. metadata) { }
+ protected override string GetExpectation() { }
+ }
[.("Trimming", "IL2091", Justification="Generic type parameter is only used for property access, not instantiation")]
public sealed class T_IsIn_IEnumerableT_Assertion : .
{
diff --git a/TUnit.PublicAPI/Tests.Assertions_Library_Has_No_API_Changes.Net4_7.verified.txt b/TUnit.PublicAPI/Tests.Assertions_Library_Has_No_API_Changes.Net4_7.verified.txt
index 702517902b..3d72a6033d 100644
--- a/TUnit.PublicAPI/Tests.Assertions_Library_Has_No_API_Changes.Net4_7.verified.txt
+++ b/TUnit.PublicAPI/Tests.Assertions_Library_Has_No_API_Changes.Net4_7.verified.txt
@@ -1290,6 +1290,60 @@ namespace .
}
}
namespace .
+{
+ public static class JsonElementAssertionExtensions
+ {
+ [.(ExpectationMessage="to not have property \'{propertyName}\'", InlineMethodBody=true)]
+ public static bool DoesNotHaveProperty(this . value, string propertyName) { }
+ [.(ExpectationMessage="to have property \'{propertyName}\'", InlineMethodBody=true)]
+ public static bool HasProperty(this . value, string propertyName) { }
+ [.(ExpectationMessage="to be a JSON array", InlineMethodBody=true)]
+ public static bool IsArray(this . value) { }
+ [.(ExpectationMessage="to be a JSON boolean", InlineMethodBody=true)]
+ public static bool IsBoolean(this . value) { }
+ [.(ExpectationMessage="to not be JSON null", InlineMethodBody=true)]
+ public static bool IsNotNull(this . value) { }
+ [.(ExpectationMessage="to be JSON null", InlineMethodBody=true)]
+ public static bool IsNull(this . value) { }
+ [.(ExpectationMessage="to be a JSON number", InlineMethodBody=true)]
+ public static bool IsNumber(this . value) { }
+ [.(ExpectationMessage="to be a JSON object", InlineMethodBody=true)]
+ public static bool IsObject(this . value) { }
+ [.(ExpectationMessage="to be a JSON string", InlineMethodBody=true)]
+ public static bool IsString(this . value) { }
+ }
+ public static class JsonNodeAssertionExtensions
+ {
+ [.(ExpectationMessage="to not have property \'{propertyName}\'")]
+ public static . DoesNotHaveJsonProperty(this ..JsonNode? value, string propertyName) { }
+ [.(ExpectationMessage="to have {expected} elements")]
+ public static . HasJsonArrayCount(this ..JsonNode? value, int expected) { }
+ [.(ExpectationMessage="to have property \'{propertyName}\'")]
+ public static . HasJsonProperty(this ..JsonNode? value, string propertyName) { }
+ [.(ExpectationMessage="to be a JsonArray")]
+ public static . IsJsonArray(this ..JsonNode? value) { }
+ [.(ExpectationMessage="to be an empty JSON array")]
+ public static . IsJsonArrayEmpty(this ..JsonNode? value) { }
+ [.(ExpectationMessage="to not be an empty JSON array")]
+ public static . IsJsonArrayNotEmpty(this ..JsonNode? value) { }
+ [.(ExpectationMessage="to be a JsonObject")]
+ public static . IsJsonObject(this ..JsonNode? value) { }
+ [.(ExpectationMessage="to be a JsonValue")]
+ public static . IsJsonValue(this ..JsonNode? value) { }
+ }
+ public static class JsonStringAssertionExtensions
+ {
+ [.(ExpectationMessage="to not be valid JSON")]
+ public static . IsNotValidJson(this string value) { }
+ [.(ExpectationMessage="to be valid JSON")]
+ public static . IsValidJson(this string value) { }
+ [.(ExpectationMessage="to be a valid JSON array")]
+ public static . IsValidJsonArray(this string value) { }
+ [.(ExpectationMessage="to be a valid JSON object")]
+ public static . IsValidJsonObject(this string value) { }
+ }
+}
+namespace .
{
public class CountWrapper : ., .
where TCollection : .
@@ -2846,6 +2900,138 @@ namespace .Extensions
public static . IsNotDefault(this . source)
where TValue : class { }
}
+ public static class JsonElementAssertionExtensions
+ {
+ public static ._DoesNotHaveProperty_String_Assertion DoesNotHaveProperty(this .<.> source, string propertyName, [.("propertyName")] string? propertyNameExpression = null) { }
+ public static ._HasProperty_String_Assertion HasProperty(this .<.> source, string propertyName, [.("propertyName")] string? propertyNameExpression = null) { }
+ public static ._IsArray_Assertion IsArray(this .<.> source) { }
+ public static ._IsBoolean_Assertion IsBoolean(this .<.> source) { }
+ public static ._IsNotNull_Assertion IsNotNull(this .<.> source) { }
+ public static ._IsNull_Assertion IsNull(this .<.> source) { }
+ public static ._IsNumber_Assertion IsNumber(this .<.> source) { }
+ public static ._IsObject_Assertion IsObject(this .<.> source) { }
+ public static ._IsString_Assertion IsString(this .<.> source) { }
+ }
+ public sealed class JsonElement_DoesNotHaveProperty_String_Assertion : .<.>
+ {
+ public JsonElement_DoesNotHaveProperty_String_Assertion(.<.> context, string propertyName) { }
+ protected override .<.> CheckAsync(.<.> metadata) { }
+ protected override string GetExpectation() { }
+ }
+ public sealed class JsonElement_HasProperty_String_Assertion : .<.>
+ {
+ public JsonElement_HasProperty_String_Assertion(.<.> context, string propertyName) { }
+ protected override .<.> CheckAsync(.<.> metadata) { }
+ protected override string GetExpectation() { }
+ }
+ public sealed class JsonElement_IsArray_Assertion : .<.>
+ {
+ public JsonElement_IsArray_Assertion(.<.> context) { }
+ protected override .<.> CheckAsync(.<.> metadata) { }
+ protected override string GetExpectation() { }
+ }
+ public sealed class JsonElement_IsBoolean_Assertion : .<.>
+ {
+ public JsonElement_IsBoolean_Assertion(.<.> context) { }
+ protected override .<.> CheckAsync(.<.> metadata) { }
+ protected override string GetExpectation() { }
+ }
+ public sealed class JsonElement_IsNotNull_Assertion : .<.>
+ {
+ public JsonElement_IsNotNull_Assertion(.<.> context) { }
+ protected override .<.> CheckAsync(.<.> metadata) { }
+ protected override string GetExpectation() { }
+ }
+ public sealed class JsonElement_IsNull_Assertion : .<.>
+ {
+ public JsonElement_IsNull_Assertion(.<.> context) { }
+ protected override .<.> CheckAsync(.<.> metadata) { }
+ protected override string GetExpectation() { }
+ }
+ public sealed class JsonElement_IsNumber_Assertion : .<.>
+ {
+ public JsonElement_IsNumber_Assertion(.<.> context) { }
+ protected override .<.> CheckAsync(.<.> metadata) { }
+ protected override string GetExpectation() { }
+ }
+ public sealed class JsonElement_IsObject_Assertion : .<.>
+ {
+ public JsonElement_IsObject_Assertion(.<.> context) { }
+ protected override .<.> CheckAsync(.<.> metadata) { }
+ protected override string GetExpectation() { }
+ }
+ public sealed class JsonElement_IsString_Assertion : .<.>
+ {
+ public JsonElement_IsString_Assertion(.<.> context) { }
+ protected override .<.> CheckAsync(.<.> metadata) { }
+ protected override string GetExpectation() { }
+ }
+ public static class JsonNodeAssertionExtensions
+ {
+ public static ._DoesNotHaveJsonProperty_String_Assertion DoesNotHaveJsonProperty(this .<..JsonNode?> source, string propertyName, [.("propertyName")] string? propertyNameExpression = null) { }
+ public static ._HasJsonArrayCount_Int_Assertion HasJsonArrayCount(this .<..JsonNode?> source, int expected, [.("expected")] string? expectedExpression = null) { }
+ public static ._HasJsonProperty_String_Assertion HasJsonProperty(this .<..JsonNode?> source, string propertyName, [.("propertyName")] string? propertyNameExpression = null) { }
+ public static ._IsJsonArray_Assertion IsJsonArray(this .<..JsonNode?> source) { }
+ public static ._IsJsonArrayEmpty_Assertion IsJsonArrayEmpty(this .<..JsonNode?> source) { }
+ public static ._IsJsonArrayNotEmpty_Assertion IsJsonArrayNotEmpty(this .<..JsonNode?> source) { }
+ public static ._IsJsonObject_Assertion IsJsonObject(this .<..JsonNode?> source) { }
+ public static ._IsJsonValue_Assertion IsJsonValue(this .<..JsonNode?> source) { }
+ }
+ public sealed class JsonNode_DoesNotHaveJsonProperty_String_Assertion : .<..JsonNode?>
+ {
+ public JsonNode_DoesNotHaveJsonProperty_String_Assertion(.<..JsonNode?> context, string propertyName) { }
+ protected override .<.> CheckAsync(.<..JsonNode?> metadata) { }
+ protected override string GetExpectation() { }
+ }
+ public sealed class JsonNode_HasJsonArrayCount_Int_Assertion : .<..JsonNode?>
+ {
+ public JsonNode_HasJsonArrayCount_Int_Assertion(.<..JsonNode?> context, int expected) { }
+ protected override .<.> CheckAsync(.<..JsonNode?> metadata) { }
+ protected override string GetExpectation() { }
+ }
+ public sealed class JsonNode_HasJsonProperty_String_Assertion : .<..JsonNode?>
+ {
+ public JsonNode_HasJsonProperty_String_Assertion(.<..JsonNode?> context, string propertyName) { }
+ protected override .<.> CheckAsync(.<..JsonNode?> metadata) { }
+ protected override string GetExpectation() { }
+ }
+ public sealed class JsonNode_IsJsonArrayEmpty_Assertion : .<..JsonNode?>
+ {
+ public JsonNode_IsJsonArrayEmpty_Assertion(.<..JsonNode?> context) { }
+ protected override .<.> CheckAsync(.<..JsonNode?> metadata) { }
+ protected override string GetExpectation() { }
+ }
+ public sealed class JsonNode_IsJsonArrayNotEmpty_Assertion : .<..JsonNode?>
+ {
+ public JsonNode_IsJsonArrayNotEmpty_Assertion(.<..JsonNode?> context) { }
+ protected override .<.> CheckAsync(.<..JsonNode?> metadata) { }
+ protected override string GetExpectation() { }
+ }
+ public sealed class JsonNode_IsJsonArray_Assertion : .<..JsonNode?>
+ {
+ public JsonNode_IsJsonArray_Assertion(.<..JsonNode?> context) { }
+ protected override .<.> CheckAsync(.<..JsonNode?> metadata) { }
+ protected override string GetExpectation() { }
+ }
+ public sealed class JsonNode_IsJsonObject_Assertion : .<..JsonNode?>
+ {
+ public JsonNode_IsJsonObject_Assertion(.<..JsonNode?> context) { }
+ protected override .<.> CheckAsync(.<..JsonNode?> metadata) { }
+ protected override string GetExpectation() { }
+ }
+ public sealed class JsonNode_IsJsonValue_Assertion : .<..JsonNode?>
+ {
+ public JsonNode_IsJsonValue_Assertion(.<..JsonNode?> context) { }
+ protected override .<.> CheckAsync(.<..JsonNode?> metadata) { }
+ protected override string GetExpectation() { }
+ }
+ public static class JsonStringAssertionExtensions
+ {
+ public static ._IsNotValidJson_Assertion IsNotValidJson(this . source) { }
+ public static ._IsValidJson_Assertion IsValidJson(this . source) { }
+ public static ._IsValidJsonArray_Assertion IsValidJsonArray(this . source) { }
+ public static ._IsValidJsonObject_Assertion IsValidJsonObject(this . source) { }
+ }
public static class LazyAssertionExtensions
{
public static ._IsValueCreated_Assertion IsValueCreated(this .<> source) { }
@@ -3197,6 +3383,30 @@ namespace .Extensions
public static . IsNullOrEmpty(this . source) { }
public static . IsNullOrWhiteSpace(this . source) { }
}
+ public sealed class String_IsNotValidJson_Assertion : .
+ {
+ public String_IsNotValidJson_Assertion(. context) { }
+ protected override .<.> CheckAsync(. metadata) { }
+ protected override string GetExpectation() { }
+ }
+ public sealed class String_IsValidJsonArray_Assertion : .
+ {
+ public String_IsValidJsonArray_Assertion(. context) { }
+ protected override .<.> CheckAsync(. metadata) { }
+ protected override string GetExpectation() { }
+ }
+ public sealed class String_IsValidJsonObject_Assertion : .
+ {
+ public String_IsValidJsonObject_Assertion(. context) { }
+ protected override .<.> CheckAsync(. metadata) { }
+ protected override string GetExpectation() { }
+ }
+ public sealed class String_IsValidJson_Assertion : .
+ {
+ public String_IsValidJson_Assertion(. context) { }
+ protected override .<.> CheckAsync(. metadata) { }
+ protected override string GetExpectation() { }
+ }
public sealed class T_IsIn_IEnumerableT_Assertion : .
{
public T_IsIn_IEnumerableT_Assertion(. context, . collection) { }
From 664a85696892c89b7b3e00b4efa049b89719cae0 Mon Sep 17 00:00:00 2001
From: Tom Longhurst <30480171+thomhurst@users.noreply.github.com>
Date: Sun, 28 Dec 2025 13:40:09 +0000
Subject: [PATCH 10/10] feat(assertions): remove JSON assertions design and
implementation plan documents
- Deleted the JSON Assertions Design document and the JSON Assertions Implementation Plan document as they are no longer needed.
---
...-25-nunit-expectedresult-implementation.md | 800 --------------
...5-nunit-expectedresult-migration-design.md | 215 ----
...-26-lock-contention-optimization-design.md | 377 -------
...2025-12-26-lock-contention-optimization.md | 505 ---------
.../2025-12-28-json-assertions-design.md | 230 ----
docs/plans/2025-12-28-json-assertions.md | 991 ------------------
6 files changed, 3118 deletions(-)
delete mode 100644 docs/plans/2025-12-25-nunit-expectedresult-implementation.md
delete mode 100644 docs/plans/2025-12-25-nunit-expectedresult-migration-design.md
delete mode 100644 docs/plans/2025-12-26-lock-contention-optimization-design.md
delete mode 100644 docs/plans/2025-12-26-lock-contention-optimization.md
delete mode 100644 docs/plans/2025-12-28-json-assertions-design.md
delete mode 100644 docs/plans/2025-12-28-json-assertions.md
diff --git a/docs/plans/2025-12-25-nunit-expectedresult-implementation.md b/docs/plans/2025-12-25-nunit-expectedresult-implementation.md
deleted file mode 100644
index c27e59817e..0000000000
--- a/docs/plans/2025-12-25-nunit-expectedresult-implementation.md
+++ /dev/null
@@ -1,800 +0,0 @@
-# NUnit ExpectedResult Migration Implementation Plan
-
-> **For Claude:** REQUIRED SUB-SKILL: Use superpowers:executing-plans to implement this plan task-by-task.
-
-**Goal:** Add code fixer support for migrating NUnit's `ExpectedResult` pattern to TUnit's assertion-based approach.
-
-**Architecture:** Extend the existing `NUnitMigrationCodeFixProvider` with a new `NUnitExpectedResultRewriter` that transforms `[TestCase(..., ExpectedResult = X)]` into `[Arguments(..., X)]` with an assertion in the method body. The rewriter runs in the `ApplyFrameworkSpecificConversions` hook before attribute conversion.
-
-**Tech Stack:** Roslyn CodeAnalysis, C# Syntax Rewriters, existing TUnit.Analyzers infrastructure.
-
----
-
-## Task 1: Add Failing Test for Simple ExpectedResult
-
-**Files:**
-- Modify: `TUnit.Analyzers.Tests/NUnitMigrationAnalyzerTests.cs`
-
-**Step 1: Write the failing test**
-
-Add this test method at the end of the test class (before the `ConfigureNUnitTest` methods):
-
-```csharp
-[Test]
-public async Task NUnit_ExpectedResult_Converted()
-{
- await CodeFixer.VerifyCodeFixAsync(
- """
- using NUnit.Framework;
-
- {|#0:public class MyClass|}
- {
- [TestCase(2, 3, ExpectedResult = 5)]
- public int Add(int a, int b) => a + b;
- }
- """,
- Verifier.Diagnostic(Rules.NUnitMigration).WithLocation(0),
- """
- using TUnit.Core;
- using TUnit.Assertions;
- using static TUnit.Assertions.Assert;
- using TUnit.Assertions.Extensions;
-
- public class MyClass
- {
- [Test]
- [Arguments(2, 3, 5)]
- public async Task Add(int a, int b, int expected)
- {
- await Assert.That(a + b).IsEqualTo(expected);
- }
- }
- """,
- ConfigureNUnitTest
- );
-}
-```
-
-**Step 2: Run test to verify it fails**
-
-Run: `dotnet test TUnit.Analyzers.Tests --filter "NUnit_ExpectedResult_Converted"`
-
-Expected: FAIL (code fixer doesn't handle ExpectedResult yet)
-
-**Step 3: Commit**
-
-```bash
-git add TUnit.Analyzers.Tests/NUnitMigrationAnalyzerTests.cs
-git commit -m "test: add failing test for NUnit ExpectedResult migration"
-```
-
----
-
-## Task 2: Create NUnitExpectedResultRewriter Class
-
-**Files:**
-- Create: `TUnit.Analyzers.CodeFixers/NUnitExpectedResultRewriter.cs`
-
-**Step 1: Create the rewriter skeleton**
-
-```csharp
-using Microsoft.CodeAnalysis;
-using Microsoft.CodeAnalysis.CSharp;
-using Microsoft.CodeAnalysis.CSharp.Syntax;
-
-namespace TUnit.Analyzers.CodeFixers;
-
-///
-/// Transforms NUnit [TestCase(..., ExpectedResult = X)] patterns to TUnit assertions.
-///
-public class NUnitExpectedResultRewriter : CSharpSyntaxRewriter
-{
- private readonly SemanticModel _semanticModel;
-
- public NUnitExpectedResultRewriter(SemanticModel semanticModel)
- {
- _semanticModel = semanticModel;
- }
-
- public override SyntaxNode? VisitMethodDeclaration(MethodDeclarationSyntax node)
- {
- // Check if method has any TestCase attributes with ExpectedResult
- var testCaseAttributes = GetTestCaseAttributesWithExpectedResult(node);
-
- if (testCaseAttributes.Count == 0)
- {
- return base.VisitMethodDeclaration(node);
- }
-
- // Get the return type (will become the expected parameter type)
- var returnType = node.ReturnType;
- if (returnType is PredefinedTypeSyntax predefined && predefined.Keyword.IsKind(SyntaxKind.VoidKeyword))
- {
- // void methods can't have ExpectedResult
- return base.VisitMethodDeclaration(node);
- }
-
- // Transform the method
- return TransformMethod(node, testCaseAttributes, returnType);
- }
-
- private List GetTestCaseAttributesWithExpectedResult(MethodDeclarationSyntax method)
- {
- var result = new List();
-
- foreach (var attributeList in method.AttributeLists)
- {
- foreach (var attribute in attributeList.Attributes)
- {
- var name = attribute.Name.ToString();
- if (name is "TestCase" or "NUnit.Framework.TestCase" or "TestCaseAttribute" or "NUnit.Framework.TestCaseAttribute")
- {
- if (HasExpectedResultArgument(attribute))
- {
- result.Add(attribute);
- }
- }
- }
- }
-
- return result;
- }
-
- private bool HasExpectedResultArgument(AttributeSyntax attribute)
- {
- if (attribute.ArgumentList == null)
- {
- return false;
- }
-
- return attribute.ArgumentList.Arguments
- .Any(arg => arg.NameEquals?.Name.Identifier.Text == "ExpectedResult");
- }
-
- private MethodDeclarationSyntax TransformMethod(
- MethodDeclarationSyntax method,
- List testCaseAttributes,
- TypeSyntax originalReturnType)
- {
- // 1. Add 'expected' parameter
- var expectedParam = SyntaxFactory.Parameter(SyntaxFactory.Identifier("expected"))
- .WithType(originalReturnType.WithTrailingTrivia(SyntaxFactory.Space));
-
- var newParameters = method.ParameterList.AddParameters(expectedParam);
-
- // 2. Change return type to async Task
- var asyncTaskType = SyntaxFactory.ParseTypeName("async Task ")
- .WithTrailingTrivia(SyntaxFactory.Space);
-
- // 3. Transform the body
- var newBody = TransformBody(method, originalReturnType);
-
- // 4. Build the new method
- var newMethod = method
- .WithReturnType(SyntaxFactory.ParseTypeName("Task").WithTrailingTrivia(SyntaxFactory.Space))
- .WithParameterList(newParameters)
- .WithBody(newBody)
- .WithExpressionBody(null)
- .WithSemicolonToken(default);
-
- // 5. Add async modifier if not present
- if (!method.Modifiers.Any(SyntaxKind.AsyncKeyword))
- {
- var asyncModifier = SyntaxFactory.Token(SyntaxKind.AsyncKeyword).WithTrailingTrivia(SyntaxFactory.Space);
- newMethod = newMethod.AddModifiers(asyncModifier);
- }
-
- // 6. Update attribute lists (remove ExpectedResult from TestCase, add [Test])
- var newAttributeLists = TransformAttributeLists(method.AttributeLists, testCaseAttributes);
- newMethod = newMethod.WithAttributeLists(newAttributeLists);
-
- return newMethod;
- }
-
- private BlockSyntax TransformBody(MethodDeclarationSyntax method, TypeSyntax returnType)
- {
- ExpressionSyntax returnExpression;
-
- if (method.ExpressionBody != null)
- {
- // Expression-bodied: => a + b
- returnExpression = method.ExpressionBody.Expression;
- }
- else if (method.Body != null)
- {
- // Block body - find return statements
- var returnStatements = method.Body.Statements
- .OfType()
- .ToList();
-
- if (returnStatements.Count == 1 && returnStatements[0].Expression != null)
- {
- // Single return - use the expression directly
- returnExpression = returnStatements[0].Expression;
-
- // Build new body with all statements except the return, plus assertion
- var statementsWithoutReturn = method.Body.Statements
- .Where(s => s != returnStatements[0])
- .ToList();
-
- var assertStatement = CreateAssertStatement(returnExpression);
- statementsWithoutReturn.Add(assertStatement);
-
- return SyntaxFactory.Block(statementsWithoutReturn);
- }
- else if (returnStatements.Count > 1)
- {
- // Multiple returns - use local variable pattern
- return TransformMultipleReturns(method.Body, returnType);
- }
- else
- {
- // No return found - shouldn't happen for ExpectedResult
- return method.Body;
- }
- }
- else
- {
- // No body - shouldn't happen
- return SyntaxFactory.Block();
- }
-
- // For expression-bodied, create block with assertion
- var assertion = CreateAssertStatement(returnExpression);
- return SyntaxFactory.Block(assertion);
- }
-
- private BlockSyntax TransformMultipleReturns(BlockSyntax body, TypeSyntax returnType)
- {
- // Declare: {returnType} result;
- var resultDeclaration = SyntaxFactory.LocalDeclarationStatement(
- SyntaxFactory.VariableDeclaration(returnType.WithTrailingTrivia(SyntaxFactory.Space))
- .WithVariables(SyntaxFactory.SingletonSeparatedList(
- SyntaxFactory.VariableDeclarator("result"))));
-
- // Replace each 'return X;' with 'result = X;'
- var rewriter = new ReturnToAssignmentRewriter();
- var transformedBody = (BlockSyntax)rewriter.Visit(body);
-
- // Add result declaration at start
- var statements = new List { resultDeclaration };
- statements.AddRange(transformedBody.Statements);
-
- // Add assertion at end
- var assertStatement = CreateAssertStatement(
- SyntaxFactory.IdentifierName("result"));
- statements.Add(assertStatement);
-
- return SyntaxFactory.Block(statements);
- }
-
- private ExpressionStatementSyntax CreateAssertStatement(ExpressionSyntax actualExpression)
- {
- // await Assert.That(actualExpression).IsEqualTo(expected);
- var assertThat = SyntaxFactory.InvocationExpression(
- SyntaxFactory.MemberAccessExpression(
- SyntaxKind.SimpleMemberAccessExpression,
- SyntaxFactory.IdentifierName("Assert"),
- SyntaxFactory.IdentifierName("That")),
- SyntaxFactory.ArgumentList(SyntaxFactory.SingletonSeparatedList(
- SyntaxFactory.Argument(actualExpression))));
-
- var isEqualTo = SyntaxFactory.InvocationExpression(
- SyntaxFactory.MemberAccessExpression(
- SyntaxKind.SimpleMemberAccessExpression,
- assertThat,
- SyntaxFactory.IdentifierName("IsEqualTo")),
- SyntaxFactory.ArgumentList(SyntaxFactory.SingletonSeparatedList(
- SyntaxFactory.Argument(SyntaxFactory.IdentifierName("expected")))));
-
- var awaitExpr = SyntaxFactory.AwaitExpression(isEqualTo);
-
- return SyntaxFactory.ExpressionStatement(awaitExpr);
- }
-
- private SyntaxList TransformAttributeLists(
- SyntaxList attributeLists,
- List testCaseAttributes)
- {
- var result = new List();
- bool hasTestAttribute = false;
-
- foreach (var attrList in attributeLists)
- {
- var newAttributes = new List();
-
- foreach (var attr in attrList.Attributes)
- {
- var name = attr.Name.ToString();
-
- if (name is "Test" or "NUnit.Framework.Test")
- {
- hasTestAttribute = true;
- newAttributes.Add(attr);
- }
- else if (testCaseAttributes.Contains(attr))
- {
- // Transform TestCase with ExpectedResult to Arguments
- var transformed = TransformTestCaseAttribute(attr);
- newAttributes.Add(transformed);
- }
- else
- {
- newAttributes.Add(attr);
- }
- }
-
- if (newAttributes.Count > 0)
- {
- result.Add(attrList.WithAttributes(SyntaxFactory.SeparatedList(newAttributes)));
- }
- }
-
- // Add [Test] attribute if not present
- if (!hasTestAttribute)
- {
- var testAttr = SyntaxFactory.Attribute(SyntaxFactory.IdentifierName("Test"));
- var testAttrList = SyntaxFactory.AttributeList(SyntaxFactory.SingletonSeparatedList(testAttr))
- .WithLeadingTrivia(attributeLists.First().GetLeadingTrivia());
- result.Insert(0, testAttrList);
- }
-
- return SyntaxFactory.List(result);
- }
-
- private AttributeSyntax TransformTestCaseAttribute(AttributeSyntax attribute)
- {
- if (attribute.ArgumentList == null)
- {
- return attribute;
- }
-
- var newArgs = new List();
- ExpressionSyntax? expectedValue = null;
-
- foreach (var arg in attribute.ArgumentList.Arguments)
- {
- if (arg.NameEquals?.Name.Identifier.Text == "ExpectedResult")
- {
- expectedValue = arg.Expression;
- }
- else if (arg.NameColon == null && arg.NameEquals == null)
- {
- // Positional argument - keep it
- newArgs.Add(arg);
- }
- // Skip other named arguments for now
- }
-
- // Add expected value as last positional argument
- if (expectedValue != null)
- {
- newArgs.Add(SyntaxFactory.AttributeArgument(expectedValue));
- }
-
- // The attribute will be renamed to "Arguments" by the existing attribute rewriter
- return attribute.WithArgumentList(
- SyntaxFactory.AttributeArgumentList(SyntaxFactory.SeparatedList(newArgs)));
- }
-
- private class ReturnToAssignmentRewriter : CSharpSyntaxRewriter
- {
- public override SyntaxNode? VisitReturnStatement(ReturnStatementSyntax node)
- {
- if (node.Expression == null)
- {
- return node;
- }
-
- // return X; -> result = X;
- return SyntaxFactory.ExpressionStatement(
- SyntaxFactory.AssignmentExpression(
- SyntaxKind.SimpleAssignmentExpression,
- SyntaxFactory.IdentifierName("result"),
- node.Expression));
- }
- }
-}
-```
-
-**Step 2: Build to verify no syntax errors**
-
-Run: `dotnet build TUnit.Analyzers.CodeFixers`
-
-Expected: Build succeeded
-
-**Step 3: Commit**
-
-```bash
-git add TUnit.Analyzers.CodeFixers/NUnitExpectedResultRewriter.cs
-git commit -m "feat: add NUnitExpectedResultRewriter skeleton"
-```
-
----
-
-## Task 3: Integrate Rewriter into Code Fix Provider
-
-**Files:**
-- Modify: `TUnit.Analyzers.CodeFixers/NUnitMigrationCodeFixProvider.cs`
-
-**Step 1: Update ApplyFrameworkSpecificConversions**
-
-Replace the `ApplyFrameworkSpecificConversions` method:
-
-```csharp
-protected override CompilationUnitSyntax ApplyFrameworkSpecificConversions(CompilationUnitSyntax compilationUnit, SemanticModel semanticModel, Compilation compilation)
-{
- // Transform ExpectedResult patterns before attribute conversion
- var expectedResultRewriter = new NUnitExpectedResultRewriter(semanticModel);
- compilationUnit = (CompilationUnitSyntax)expectedResultRewriter.Visit(compilationUnit);
-
- return compilationUnit;
-}
-```
-
-**Step 2: Run the test**
-
-Run: `dotnet test TUnit.Analyzers.Tests --filter "NUnit_ExpectedResult_Converted"`
-
-Expected: May pass or fail depending on edge cases - we'll iterate
-
-**Step 3: Commit**
-
-```bash
-git add TUnit.Analyzers.CodeFixers/NUnitMigrationCodeFixProvider.cs
-git commit -m "feat: integrate ExpectedResult rewriter into NUnit migration"
-```
-
----
-
-## Task 4: Fix Formatting Issues
-
-**Files:**
-- Modify: `TUnit.Analyzers.CodeFixers/NUnitExpectedResultRewriter.cs`
-
-**Step 1: Run test and check output**
-
-Run: `dotnet test TUnit.Analyzers.Tests --filter "NUnit_ExpectedResult_Converted" -v n`
-
-Examine the actual output vs expected - likely issues with:
-- Trivia (whitespace, newlines)
-- Attribute ordering
-- Method modifier ordering
-
-**Step 2: Fix identified issues**
-
-Common fixes needed:
-- Add proper leading/trailing trivia to statements
-- Ensure async modifier is in correct position
-- Fix attribute list formatting
-
-**Step 3: Run test again**
-
-Run: `dotnet test TUnit.Analyzers.Tests --filter "NUnit_ExpectedResult_Converted"`
-
-Expected: PASS
-
-**Step 4: Commit**
-
-```bash
-git add TUnit.Analyzers.CodeFixers/NUnitExpectedResultRewriter.cs
-git commit -m "fix: correct formatting in ExpectedResult transformation"
-```
-
----
-
-## Task 5: Add Test for Multiple TestCase Attributes
-
-**Files:**
-- Modify: `TUnit.Analyzers.Tests/NUnitMigrationAnalyzerTests.cs`
-
-**Step 1: Write the test**
-
-```csharp
-[Test]
-public async Task NUnit_Multiple_ExpectedResult_Converted()
-{
- await CodeFixer.VerifyCodeFixAsync(
- """
- using NUnit.Framework;
-
- {|#0:public class MyClass|}
- {
- [TestCase(2, 3, ExpectedResult = 5)]
- [TestCase(10, 5, ExpectedResult = 15)]
- [TestCase(0, 0, ExpectedResult = 0)]
- public int Add(int a, int b) => a + b;
- }
- """,
- Verifier.Diagnostic(Rules.NUnitMigration).WithLocation(0),
- """
- using TUnit.Core;
- using TUnit.Assertions;
- using static TUnit.Assertions.Assert;
- using TUnit.Assertions.Extensions;
-
- public class MyClass
- {
- [Test]
- [Arguments(2, 3, 5)]
- [Arguments(10, 5, 15)]
- [Arguments(0, 0, 0)]
- public async Task Add(int a, int b, int expected)
- {
- await Assert.That(a + b).IsEqualTo(expected);
- }
- }
- """,
- ConfigureNUnitTest
- );
-}
-```
-
-**Step 2: Run test**
-
-Run: `dotnet test TUnit.Analyzers.Tests --filter "NUnit_Multiple_ExpectedResult_Converted"`
-
-Expected: PASS
-
-**Step 3: Commit**
-
-```bash
-git add TUnit.Analyzers.Tests/NUnitMigrationAnalyzerTests.cs
-git commit -m "test: add test for multiple TestCase with ExpectedResult"
-```
-
----
-
-## Task 6: Add Test for Block-Bodied Method with Single Return
-
-**Files:**
-- Modify: `TUnit.Analyzers.Tests/NUnitMigrationAnalyzerTests.cs`
-
-**Step 1: Write the test**
-
-```csharp
-[Test]
-public async Task NUnit_ExpectedResult_BlockBody_SingleReturn_Converted()
-{
- await CodeFixer.VerifyCodeFixAsync(
- """
- using NUnit.Framework;
-
- {|#0:public class MyClass|}
- {
- [TestCase(2, 3, ExpectedResult = 5)]
- public int Add(int a, int b)
- {
- var sum = a + b;
- return sum;
- }
- }
- """,
- Verifier.Diagnostic(Rules.NUnitMigration).WithLocation(0),
- """
- using TUnit.Core;
- using TUnit.Assertions;
- using static TUnit.Assertions.Assert;
- using TUnit.Assertions.Extensions;
-
- public class MyClass
- {
- [Test]
- [Arguments(2, 3, 5)]
- public async Task Add(int a, int b, int expected)
- {
- var sum = a + b;
- await Assert.That(sum).IsEqualTo(expected);
- }
- }
- """,
- ConfigureNUnitTest
- );
-}
-```
-
-**Step 2: Run test**
-
-Run: `dotnet test TUnit.Analyzers.Tests --filter "NUnit_ExpectedResult_BlockBody_SingleReturn_Converted"`
-
-Expected: PASS (or fix if needed)
-
-**Step 3: Commit**
-
-```bash
-git add TUnit.Analyzers.Tests/NUnitMigrationAnalyzerTests.cs
-git commit -m "test: add test for block-bodied ExpectedResult with single return"
-```
-
----
-
-## Task 7: Add Test for Multiple Return Statements
-
-**Files:**
-- Modify: `TUnit.Analyzers.Tests/NUnitMigrationAnalyzerTests.cs`
-
-**Step 1: Write the test**
-
-```csharp
-[Test]
-public async Task NUnit_ExpectedResult_MultipleReturns_Converted()
-{
- await CodeFixer.VerifyCodeFixAsync(
- """
- using NUnit.Framework;
-
- {|#0:public class MyClass|}
- {
- [TestCase(-1, ExpectedResult = 0)]
- [TestCase(0, ExpectedResult = 1)]
- [TestCase(5, ExpectedResult = 120)]
- public int Factorial(int n)
- {
- if (n < 0) return 0;
- if (n <= 1) return 1;
- return n * Factorial(n - 1);
- }
- }
- """,
- Verifier.Diagnostic(Rules.NUnitMigration).WithLocation(0),
- """
- using TUnit.Core;
- using TUnit.Assertions;
- using static TUnit.Assertions.Assert;
- using TUnit.Assertions.Extensions;
-
- public class MyClass
- {
- [Test]
- [Arguments(-1, 0)]
- [Arguments(0, 1)]
- [Arguments(5, 120)]
- public async Task Factorial(int n, int expected)
- {
- int result;
- if (n < 0) result = 0;
- else if (n <= 1) result = 1;
- else result = n * Factorial(n - 1);
- await Assert.That(result).IsEqualTo(expected);
- }
- }
- """,
- ConfigureNUnitTest
- );
-}
-```
-
-**Step 2: Run test**
-
-Run: `dotnet test TUnit.Analyzers.Tests --filter "NUnit_ExpectedResult_MultipleReturns_Converted"`
-
-Expected: May fail initially - multiple returns require more complex transformation
-
-**Step 3: Fix if needed and commit**
-
-```bash
-git add TUnit.Analyzers.Tests/NUnitMigrationAnalyzerTests.cs
-git commit -m "test: add test for ExpectedResult with multiple returns"
-```
-
----
-
-## Task 8: Add Test for String ExpectedResult
-
-**Files:**
-- Modify: `TUnit.Analyzers.Tests/NUnitMigrationAnalyzerTests.cs`
-
-**Step 1: Write the test**
-
-```csharp
-[Test]
-public async Task NUnit_ExpectedResult_String_Converted()
-{
- await CodeFixer.VerifyCodeFixAsync(
- """
- using NUnit.Framework;
-
- {|#0:public class MyClass|}
- {
- [TestCase("hello", ExpectedResult = "HELLO")]
- [TestCase("World", ExpectedResult = "WORLD")]
- public string ToUpper(string input) => input.ToUpper();
- }
- """,
- Verifier.Diagnostic(Rules.NUnitMigration).WithLocation(0),
- """
- using TUnit.Core;
- using TUnit.Assertions;
- using static TUnit.Assertions.Assert;
- using TUnit.Assertions.Extensions;
-
- public class MyClass
- {
- [Test]
- [Arguments("hello", "HELLO")]
- [Arguments("World", "WORLD")]
- public async Task ToUpper(string input, string expected)
- {
- await Assert.That(input.ToUpper()).IsEqualTo(expected);
- }
- }
- """,
- ConfigureNUnitTest
- );
-}
-```
-
-**Step 2: Run test**
-
-Run: `dotnet test TUnit.Analyzers.Tests --filter "NUnit_ExpectedResult_String_Converted"`
-
-Expected: PASS
-
-**Step 3: Commit**
-
-```bash
-git add TUnit.Analyzers.Tests/NUnitMigrationAnalyzerTests.cs
-git commit -m "test: add test for string ExpectedResult migration"
-```
-
----
-
-## Task 9: Run Full Test Suite
-
-**Files:** None (verification only)
-
-**Step 1: Run all NUnit migration tests**
-
-Run: `dotnet test TUnit.Analyzers.Tests --filter "NUnitMigration"`
-
-Expected: All tests PASS
-
-**Step 2: Run full analyzer test suite**
-
-Run: `dotnet test TUnit.Analyzers.Tests`
-
-Expected: All tests PASS
-
-**Step 3: Commit any remaining fixes**
-
-```bash
-git add -A
-git commit -m "fix: address any remaining test failures"
-```
-
----
-
-## Task 10: Update Design Document Status
-
-**Files:**
-- Modify: `docs/plans/2025-12-25-nunit-expectedresult-migration-design.md`
-
-**Step 1: Update status**
-
-Change the Status line from:
-```
-**Status**: Design Complete
-```
-
-To:
-```
-**Status**: Implemented
-```
-
-**Step 2: Commit**
-
-```bash
-git add docs/plans/2025-12-25-nunit-expectedresult-migration-design.md
-git commit -m "docs: mark ExpectedResult migration as implemented"
-```
-
----
-
-## Verification Checklist
-
-After all tasks complete:
-
-- [ ] `dotnet test TUnit.Analyzers.Tests` passes
-- [ ] `dotnet build TUnit.Analyzers.CodeFixers` succeeds
-- [ ] New tests cover: simple ExpectedResult, multiple TestCase, block body, multiple returns, string types
-- [ ] Code follows existing patterns in NUnitMigrationCodeFixProvider
diff --git a/docs/plans/2025-12-25-nunit-expectedresult-migration-design.md b/docs/plans/2025-12-25-nunit-expectedresult-migration-design.md
deleted file mode 100644
index 6fc8da8aeb..0000000000
--- a/docs/plans/2025-12-25-nunit-expectedresult-migration-design.md
+++ /dev/null
@@ -1,215 +0,0 @@
-# NUnit ExpectedResult Migration Code Fixer
-
-**Issue**: #4167
-**Date**: 2025-12-25
-**Status**: Implemented
-
-## Overview
-
-A Roslyn analyzer + code fixer that extends TUnit's existing NUnit migration infrastructure to handle `ExpectedResult` patterns. Converts NUnit's return-value-based test assertions to TUnit's explicit assertion approach.
-
-## Scope
-
-### Patterns Handled
-
-1. `[TestCase(..., ExpectedResult = X)]` - Inline expected result property
-2. `TestCaseData.Returns(X)` - Fluent expected result in data sources
-
-### Patterns Flagged for Manual Review
-
-- Mixed attributes (some with ExpectedResult, some without)
-- `TestCaseData` with `.SetName()`, `.SetCategory()`, or other chained methods
-- Dynamic/computed `TestCaseData` (loops, conditionals)
-- `TestCaseData` constructed outside simple array/collection initializers
-
-## Transformation Examples
-
-### TestCase with ExpectedResult
-
-```csharp
-// BEFORE (NUnit)
-[TestCase(2, 3, ExpectedResult = 5)]
-[TestCase(10, 5, ExpectedResult = 15)]
-public int Add(int a, int b) => a + b;
-
-// AFTER (TUnit)
-[Test]
-[Arguments(2, 3, 5)]
-[Arguments(10, 5, 15)]
-public async Task Add(int a, int b, int expected)
-{
- await Assert.That(a + b).IsEqualTo(expected);
-}
-```
-
-### TestCaseData.Returns()
-
-```csharp
-// BEFORE (NUnit)
-public static IEnumerable AddCases => new[]
-{
- new TestCaseData(2, 3).Returns(5),
- new TestCaseData(10, 5).Returns(15)
-};
-
-[TestCaseSource(nameof(AddCases))]
-public int Add(int a, int b) => a + b;
-
-// AFTER (TUnit)
-public static IEnumerable<(int, int, int)> AddCases => new[]
-{
- (2, 3, 5),
- (10, 5, 15)
-};
-
-[Test]
-[MethodDataSource(nameof(AddCases))]
-public async Task Add(int a, int b, int expected)
-{
- await Assert.That(a + b).IsEqualTo(expected);
-}
-```
-
-## Design Decisions
-
-| Decision | Choice | Rationale |
-|----------|--------|-----------|
-| Multiple ExpectedResults | Add as method parameter | Preserves parameterized structure, single method |
-| Expression-bodied members | Convert to block body | Semantic change warrants explicit block |
-| Multiple return statements | Extract to local variable | Cleaner code, single assertion point |
-| Null expected values | Use `IsEqualTo(expected)` | Parameter value unknown at compile-time |
-| Parameter naming | `expected` | Concise, idiomatic in testing |
-
-## Method Body Transformation
-
-### Case A: Expression-Bodied
-
-```csharp
-// BEFORE
-public int Add(int a, int b) => a + b;
-
-// AFTER
-public async Task Add(int a, int b, int expected)
-{
- await Assert.That(a + b).IsEqualTo(expected);
-}
-```
-
-### Case B: Single Return
-
-```csharp
-// BEFORE
-public int Add(int a, int b)
-{
- var sum = a + b;
- return sum;
-}
-
-// AFTER
-public async Task Add(int a, int b, int expected)
-{
- var sum = a + b;
- await Assert.That(sum).IsEqualTo(expected);
-}
-```
-
-### Case C: Multiple Returns
-
-```csharp
-// BEFORE
-public int Factorial(int n)
-{
- if (n < 0) return 0;
- if (n <= 1) return 1;
- return n * Factorial(n - 1);
-}
-
-// AFTER
-public async Task Factorial(int n, int expected)
-{
- int result;
- if (n < 0) result = 0;
- else if (n <= 1) result = 1;
- else result = n * Factorial(n - 1);
- await Assert.That(result).IsEqualTo(expected);
-}
-```
-
-**Algorithm for multiple returns**:
-1. Declare `{returnType} result;` at method start
-2. Replace each `return X;` with `result = X;`
-3. Convert `if (...) return` chains to `if/else if/else`
-4. Append `await Assert.That(result).IsEqualTo(expected);` at end
-
-## Implementation
-
-### Analyzer
-
-Extend `NUnitMigrationAnalyzer` to detect:
-- `ExpectedResult` named argument in `[TestCase]` attributes
-- `.Returns()` method calls on `TestCaseData`
-
-**Diagnostic**: `TUnit0050` (Info severity)
-**Message**: "NUnit ExpectedResult can be converted to TUnit assertion"
-
-### Code Fixer
-
-#### New Files
-
-```
-TUnit.Analyzers.CodeFixers/
-├── NUnitExpectedResultRewriter.cs # TestCase ExpectedResult transform
-└── NUnitTestCaseDataRewriter.cs # TestCaseData.Returns() transform
-```
-
-#### Integration
-
-Add to `NUnitMigrationCodeFixProvider.cs` transformation pipeline:
-
-```csharp
-// Before attribute conversion:
-root = new NUnitExpectedResultRewriter(semanticModel).Visit(root);
-root = new NUnitTestCaseDataRewriter(semanticModel).Visit(root);
-```
-
-### TestCase Transformation Steps
-
-1. Extract `ExpectedResult` values from each `[TestCase]`
-2. Convert `[TestCase(args, ExpectedResult = X)]` → `[Arguments(args, X)]`
-3. Add `expected` parameter with original return type
-4. Change return type to `async Task`
-5. Transform method body per cases above
-6. Add `[Test]` attribute if not present
-
-### TestCaseData Transformation Steps
-
-1. Locate data source method/property
-2. For each `TestCaseData`: extract args + `.Returns()` value → tuple
-3. Update return type: `IEnumerable` → `IEnumerable<(T1, T2, ..., TExpected)>`
-4. Transform test method same as TestCase
-
-## Testing
-
-Add to `TUnit.Analyzers.Tests/NUnitMigrationAnalyzerTests.cs`:
-
-- Simple ExpectedResult (single TestCase)
-- Multiple TestCase attributes with different expected values
-- Expression-bodied methods
-- Block-bodied with single return
-- Block-bodied with multiple returns
-- Null expected values
-- Reference type expected values
-- TestCaseData.Returns() simple case
-- TestCaseData.Returns() with multiple entries
-- Mixed scenarios (flagged for manual review)
-
-## Edge Cases
-
-| Scenario | Handling |
-|----------|----------|
-| Null ExpectedResult | `IsEqualTo(expected)` handles null at runtime |
-| Constant references (`int.MaxValue`) | Passed through as parameter value |
-| Generic return types | Parameter type matches original return type |
-| Nullable return types | Parameter type includes nullability |
-| Recursive methods | Works - method signature change is compatible |
-| Mixed TestCase (with/without ExpectedResult) | Flag for manual review |
diff --git a/docs/plans/2025-12-26-lock-contention-optimization-design.md b/docs/plans/2025-12-26-lock-contention-optimization-design.md
deleted file mode 100644
index 010cb0313f..0000000000
--- a/docs/plans/2025-12-26-lock-contention-optimization-design.md
+++ /dev/null
@@ -1,377 +0,0 @@
-# Lock Contention Optimization Design
-
-**Issue:** [#4162 - perf: reduce lock contention in test discovery and scheduling](https://github.com/thomhurst/TUnit/issues/4162)
-**Date:** 2025-12-26
-**Priority:** P1
-**Goal:** Maximum parallelism - minimize lock duration at any cost
-
----
-
-## Problem Statement
-
-Two performance-critical code paths unnecessarily extend lock durations, creating bottlenecks in parallel test execution:
-
-1. **ReflectionTestDataCollector.cs (lines 137-141):** Lock remains held while creating a full defensive copy of discovered tests
-2. **ConstraintKeyScheduler.cs (lines 56-69, 151-190):** LINQ evaluation and list allocation happen within lock scope
-
-These practices serialize operations that should execute in parallel, degrading throughput on multi-core systems.
-
----
-
-## Solution Overview
-
-| Component | Approach | Complexity |
-|-----------|----------|------------|
-| ReflectionTestDataCollector | ImmutableList with atomic swap | Medium |
-| ConstraintKeyScheduler | Manual loops + two-phase locking + pre-allocation | High |
-
----
-
-## Design: ReflectionTestDataCollector
-
-### Current Implementation (Problem)
-
-```csharp
-private static readonly List _discoveredTests = new(capacity: 1000);
-private static readonly Lock _discoveredTestsLock = new();
-
-public async Task> CollectTestsAsync(string testSessionId)
-{
- // ... discovery logic ...
-
- lock (_discoveredTestsLock)
- {
- _discoveredTests.AddRange(newTests);
- return new List(_discoveredTests); // O(n) copy under lock
- }
-}
-```
-
-### Proposed Implementation
-
-Replace `List` + lock with `ImmutableList` + atomic swap:
-
-```csharp
-private static ImmutableList _discoveredTests = ImmutableList.Empty;
-
-public async Task> CollectTestsAsync(string testSessionId)
-{
- // ... discovery logic unchanged ...
-
- // Atomic swap - no lock needed for readers
- ImmutableList original, updated;
- do
- {
- original = _discoveredTests;
- updated = original.AddRange(newTests);
- } while (Interlocked.CompareExchange(ref _discoveredTests, updated, original) != original);
-
- return _discoveredTests; // Already immutable, no copy needed
-}
-```
-
-For streaming writes, use `ImmutableInterlocked.Update()` helper:
-
-```csharp
-// In CollectTestsStreamingAsync
-ImmutableInterlocked.Update(ref _discoveredTests, list => list.Add(test));
-yield return test;
-```
-
-### Benefits
-
-- Zero lock contention on reads (callers get immutable snapshot)
-- Writes use lock-free CAS loop (brief spin during concurrent writes)
-- Eliminates defensive copy entirely
-- Thread-safe enumeration without locks
-
-### Trade-offs
-
-- Slightly higher allocation on writes (new immutable list per update)
-- Discovery is infrequent compared to reads, so this is acceptable
-
----
-
-## Design: ConstraintKeyScheduler
-
-### Problem 1: LINQ Inside Lock (lines 56-69)
-
-**Current:**
-```csharp
-lock (lockObject)
-{
- canStart = !constraintKeys.Any(key => lockedKeys.Contains(key)); // LINQ allocation
- if (canStart)
- {
- foreach (var key in constraintKeys)
- lockedKeys.Add(key);
- }
-}
-```
-
-**Proposed - Manual loop with indexer access:**
-```csharp
-lock (lockObject)
-{
- canStart = true;
- var keyCount = constraintKeys.Count;
- for (var i = 0; i < keyCount; i++)
- {
- if (lockedKeys.Contains(constraintKeys[i]))
- {
- canStart = false;
- break; // Early exit
- }
- }
-
- if (canStart)
- {
- for (var i = 0; i < keyCount; i++)
- lockedKeys.Add(constraintKeys[i]);
- }
-}
-```
-
-**Benefits:**
-- No delegate allocation
-- No enumerator allocation
-- Early exit on first conflict
-
-### Problem 2: Allocations and Extended Lock Scope (lines 149-190)
-
-**Current:**
-```csharp
-var testsToStart = new List<...>(); // Outside lock - good
-
-lock (lockObject)
-{
- foreach (var key in constraintKeys)
- lockedKeys.Remove(key);
-
- var tempQueue = new List<...>(); // Allocation INSIDE lock - bad
-
- while (waitingTests.TryDequeue(out var waitingTest))
- {
- var canStart = !waitingTest.ConstraintKeys.Any(...); // LINQ inside lock
- // ... extensive work inside lock
- }
-
- foreach (var item in tempQueue)
- waitingTests.Enqueue(item);
-}
-```
-
-**Proposed - Two-phase locking with pre-allocation:**
-
-```csharp
-// Pre-allocate outside any lock
-var testsToStart = new List<(..., TaskCompletionSource)>(4);
-var testsToRequeue = new List<(..., TaskCompletionSource)>(8);
-var waitingSnapshot = new List<(..., TaskCompletionSource)>(8);
-
-// Phase 1: Release keys and snapshot queue (single brief lock)
-lock (lockObject)
-{
- var keyCount = constraintKeys.Count;
- for (var i = 0; i < keyCount; i++)
- lockedKeys.Remove(constraintKeys[i]);
-
- while (waitingTests.TryDequeue(out var item))
- waitingSnapshot.Add(item);
-}
-
-// Phase 2: For each candidate, try to acquire keys (brief lock per candidate)
-foreach (var waitingTest in waitingSnapshot)
-{
- bool acquired;
- lock (lockObject)
- {
- acquired = true;
- var keys = waitingTest.ConstraintKeys;
- var keyCount = keys.Count;
- for (var i = 0; i < keyCount; i++)
- {
- if (lockedKeys.Contains(keys[i]))
- {
- acquired = false;
- break;
- }
- }
-
- if (acquired)
- {
- for (var i = 0; i < keyCount; i++)
- lockedKeys.Add(keys[i]);
- }
- }
-
- if (acquired)
- testsToStart.Add(waitingTest);
- else
- testsToRequeue.Add(waitingTest);
-}
-
-// Phase 3: Requeue non-starters (single brief lock)
-if (testsToRequeue.Count > 0)
-{
- lock (lockObject)
- {
- foreach (var item in testsToRequeue)
- waitingTests.Enqueue(item);
- }
-}
-
-// Phase 4: Signal starters (outside lock - no contention)
-foreach (var test in testsToStart)
- test.StartSignal.SetResult(true);
-```
-
-### Benefits
-
-- Multiple brief locks instead of one long lock
-- Other threads can interleave between phases
-- All allocations outside locks
-- No LINQ allocations
-- Early exit in availability checks
-
----
-
-## Testing Strategy
-
-### 1. Stress Tests for Thread Safety
-
-Add to `TUnit.Engine.Tests`:
-
-```csharp
-[Test]
-[Repeat(10)]
-public async Task ReflectionTestDataCollector_ConcurrentDiscovery_NoRaceConditions()
-{
- var tasks = Enumerable.Range(0, 50)
- .Select(_ => collector.CollectTestsAsync(Guid.NewGuid().ToString()));
-
- var results = await Task.WhenAll(tasks);
-
- await Assert.That(results.SelectMany(r => r).Distinct().Count())
- .IsGreaterThan(0);
-}
-
-[Test]
-[Repeat(10)]
-public async Task ConstraintKeyScheduler_HighContention_NoDeadlocks()
-{
- var tests = CreateTestsWithOverlappingConstraints(100, overlapFactor: 0.3);
-
- using var cts = new CancellationTokenSource(TimeSpan.FromSeconds(30));
- await scheduler.ExecuteTestsWithConstraintsAsync(tests, cts.Token);
-}
-```
-
-### 2. Wall Clock Benchmarks
-
-```csharp
-[Test]
-[Category("Performance")]
-public async Task Benchmark_TestDiscovery_WallClock()
-{
- var stopwatch = Stopwatch.StartNew();
- var tests = await collector.CollectTestsAsync(testSessionId);
- stopwatch.Stop();
-
- Console.WriteLine($"Discovery wall clock: {stopwatch.ElapsedMilliseconds}ms");
- Console.WriteLine($"Tests discovered: {tests.Count()}");
- Console.WriteLine($"Throughput: {tests.Count() / stopwatch.Elapsed.TotalSeconds:F0} tests/sec");
-}
-
-[Test]
-[Category("Performance")]
-public async Task Benchmark_ConstrainedExecution_WallClock()
-{
- var tests = CreateTestsWithConstraints(count: 500, constraintOverlap: 0.3);
-
- var stopwatch = Stopwatch.StartNew();
- await scheduler.ExecuteTestsWithConstraintsAsync(tests, CancellationToken.None);
- stopwatch.Stop();
-
- Console.WriteLine($"Constrained execution wall clock: {stopwatch.ElapsedMilliseconds}ms");
-}
-```
-
-### 3. Profiling with dotnet-trace
-
-```bash
-# Baseline (before changes)
-dotnet trace collect --name TUnit.PerformanceTests -- dotnet run -c Release
-mv trace.nettrace baseline.nettrace
-
-# Optimized (after changes)
-dotnet trace collect --name TUnit.PerformanceTests -- dotnet run -c Release
-mv trace.nettrace optimized.nettrace
-```
-
-### 4. Key Metrics
-
-| Scenario | Metric | Target |
-|----------|--------|--------|
-| Discovery (1000 tests) | Wall clock time | >=10% improvement |
-| Constrained execution (500 tests, 30% overlap) | Wall clock time | >=15% improvement |
-| High parallelism (16+ cores) | Scaling efficiency | Near-linear |
-| Lock contention | Thread wait time | >=50% reduction |
-| Memory | Hot path allocations | >=30% reduction |
-
----
-
-## Risk Mitigation
-
-### Race Condition Risks
-
-| Risk | Mitigation |
-|------|------------|
-| ImmutableList CAS loop starvation | Add retry limit with fallback to lock; contention is rare during discovery |
-| Lost updates in two-phase lock | Each phase is atomic; tests either start or requeue, never lost |
-| Stale reads of `_discoveredTests` | Acceptable - immutable snapshots are always consistent |
-
-### Behavioral Compatibility
-
-| Concern | Mitigation |
-|---------|------------|
-| Test ordering changes | Discovery order was never guaranteed |
-| API consumers expecting mutable list | Return type is `IEnumerable`, already read-only contract |
-| Constraint scheduling order | Priority ordering preserved |
-
-### Rollback Strategy
-
-Both changes are isolated:
-- `ReflectionTestDataCollector`: Revert to `List` + lock with single file change
-- `ConstraintKeyScheduler`: Revert loop-by-loop if specific optimization causes issues
-
----
-
-## Implementation Plan
-
-### Incremental Rollout (Suggested Merge Order)
-
-1. **PR 1: LINQ to manual loop replacements** (lowest risk)
- - Replace `.Any()` with manual loops in ConstraintKeyScheduler
- - Immediate benefit, minimal code change
-
-2. **PR 2: Lock scope restructuring** (medium risk)
- - Two-phase locking in ConstraintKeyScheduler
- - Pre-allocate lists outside locks
-
-3. **PR 3: ImmutableList migration** (medium risk)
- - Replace List + lock with ImmutableList in ReflectionTestDataCollector
- - Atomic swap pattern
-
-This allows isolating any regressions to specific changes.
-
----
-
-## Verification Checklist
-
-- [ ] All existing tests pass
-- [ ] Stress tests added and passing
-- [ ] Wall clock benchmarks show improvement
-- [ ] dotnet-trace shows reduced lock contention
-- [ ] No deadlocks under high parallelism (16+ cores)
-- [ ] Memory allocations reduced in hot paths
diff --git a/docs/plans/2025-12-26-lock-contention-optimization.md b/docs/plans/2025-12-26-lock-contention-optimization.md
deleted file mode 100644
index fc88ab6335..0000000000
--- a/docs/plans/2025-12-26-lock-contention-optimization.md
+++ /dev/null
@@ -1,505 +0,0 @@
-# Lock Contention Optimization Implementation Plan
-
-> **For Claude:** REQUIRED SUB-SKILL: Use superpowers:executing-plans to implement this plan task-by-task.
-
-**Goal:** Reduce lock contention in test discovery and scheduling to improve parallel test execution throughput.
-
-**Architecture:** Replace `List` + lock with `ImmutableList` + atomic swap in ReflectionTestDataCollector. Replace LINQ with manual loops and restructure to two-phase locking in ConstraintKeyScheduler.
-
-**Tech Stack:** C#, System.Collections.Immutable, System.Threading
-
----
-
-## Task 1: Add Stress Tests for Thread Safety Baseline
-
-**Files:**
-- Create: `TUnit.Engine.Tests/Scheduling/ConstraintKeySchedulerConcurrencyTests.cs`
-
-**Step 1: Write the failing test for high contention scenarios**
-
-```csharp
-using TUnit.Core;
-using TUnit.Engine.Scheduling;
-
-namespace TUnit.Engine.Tests.Scheduling;
-
-public class ConstraintKeySchedulerConcurrencyTests
-{
- [Test]
- [Repeat(5)]
- public async Task HighContention_WithOverlappingConstraints_CompletesWithoutDeadlock()
- {
- // Arrange - create mock tests with overlapping constraint keys
- // This test establishes baseline behavior before optimization
-
- using var cts = new CancellationTokenSource(TimeSpan.FromSeconds(30));
-
- // Act & Assert - should complete without timeout/deadlock
- await Assert.That(async () =>
- {
- // Placeholder - will be implemented after understanding test infrastructure
- await Task.Delay(1, cts.Token);
- }).ThrowsNothing();
- }
-}
-```
-
-**Step 2: Run test to verify it passes (baseline)**
-
-Run: `cd TUnit.Engine.Tests && dotnet test --filter "FullyQualifiedName~ConstraintKeySchedulerConcurrencyTests"`
-Expected: PASS (baseline test)
-
-**Step 3: Commit**
-
-```bash
-git add TUnit.Engine.Tests/Scheduling/ConstraintKeySchedulerConcurrencyTests.cs
-git commit -m "test: add concurrency stress test baseline for ConstraintKeyScheduler"
-```
-
----
-
-## Task 2: Replace LINQ with Manual Loops in ConstraintKeyScheduler (lines 56-69)
-
-**Files:**
-- Modify: `TUnit.Engine/Scheduling/ConstraintKeyScheduler.cs:56-69`
-
-**Step 1: Write test for constraint key checking behavior**
-
-```csharp
-[Test]
-public async Task ConstraintKeyCheck_WithNoConflicts_StartsImmediately()
-{
- // This test verifies the behavior is unchanged after LINQ removal
- // Exact implementation depends on testability of ConstraintKeyScheduler
-}
-```
-
-**Step 2: Run existing tests to establish baseline**
-
-Run: `cd TUnit.Engine.Tests && dotnet test`
-Expected: All tests PASS
-
-**Step 3: Replace LINQ `.Any()` with manual loop**
-
-In `ConstraintKeyScheduler.cs`, change lines 56-69 from:
-
-```csharp
-lock (lockObject)
-{
- // Check if all constraint keys are available
- canStart = !constraintKeys.Any(key => lockedKeys.Contains(key));
-
- if (canStart)
- {
- // Lock all the constraint keys for this test
- foreach (var key in constraintKeys)
- {
- lockedKeys.Add(key);
- }
- }
-}
-```
-
-To:
-
-```csharp
-lock (lockObject)
-{
- // Check if all constraint keys are available - manual loop avoids LINQ allocation
- canStart = true;
- var keyCount = constraintKeys.Count;
- for (var i = 0; i < keyCount; i++)
- {
- if (lockedKeys.Contains(constraintKeys[i]))
- {
- canStart = false;
- break;
- }
- }
-
- if (canStart)
- {
- // Lock all the constraint keys for this test
- for (var i = 0; i < keyCount; i++)
- {
- lockedKeys.Add(constraintKeys[i]);
- }
- }
-}
-```
-
-**Step 4: Run tests to verify behavior unchanged**
-
-Run: `cd TUnit.Engine.Tests && dotnet test`
-Expected: All tests PASS
-
-**Step 5: Commit**
-
-```bash
-git add TUnit.Engine/Scheduling/ConstraintKeyScheduler.cs
-git commit -m "perf: replace LINQ with manual loop in ConstraintKeyScheduler key checking"
-```
-
----
-
-## Task 3: Replace LINQ in Waiting Test Check (line 165)
-
-**Files:**
-- Modify: `TUnit.Engine/Scheduling/ConstraintKeyScheduler.cs:165`
-
-**Step 1: Run existing tests to establish baseline**
-
-Run: `cd TUnit.Engine.Tests && dotnet test`
-Expected: All tests PASS
-
-**Step 2: Replace LINQ `.Any()` in waiting test check**
-
-In `ConstraintKeyScheduler.cs`, change line 165 from:
-
-```csharp
-var canStart = !waitingTest.ConstraintKeys.Any(key => lockedKeys.Contains(key));
-```
-
-To:
-
-```csharp
-var canStart = true;
-var waitingKeys = waitingTest.ConstraintKeys;
-var waitingKeyCount = waitingKeys.Count;
-for (var j = 0; j < waitingKeyCount; j++)
-{
- if (lockedKeys.Contains(waitingKeys[j]))
- {
- canStart = false;
- break;
- }
-}
-```
-
-**Step 3: Run tests to verify behavior unchanged**
-
-Run: `cd TUnit.Engine.Tests && dotnet test`
-Expected: All tests PASS
-
-**Step 4: Commit**
-
-```bash
-git add TUnit.Engine/Scheduling/ConstraintKeyScheduler.cs
-git commit -m "perf: replace LINQ with manual loop in waiting test availability check"
-```
-
----
-
-## Task 4: Move List Allocation Outside Lock (lines 149-190)
-
-**Files:**
-- Modify: `TUnit.Engine/Scheduling/ConstraintKeyScheduler.cs:149-190`
-
-**Step 1: Run existing tests to establish baseline**
-
-Run: `cd TUnit.Engine.Tests && dotnet test`
-Expected: All tests PASS
-
-**Step 2: Pre-allocate lists outside lock scope**
-
-Change the structure from allocating `tempQueue` inside lock to pre-allocating outside. Replace lines 149-190:
-
-```csharp
-// Release the constraint keys and check if any waiting tests can now run
-var testsToStart = new List<(AbstractExecutableTest Test, IReadOnlyList ConstraintKeys, TaskCompletionSource StartSignal)>(4);
-var testsToRequeue = new List<(AbstractExecutableTest Test, IReadOnlyList ConstraintKeys, TaskCompletionSource StartSignal)>(8);
-
-lock (lockObject)
-{
- // Release all constraint keys for this test
- var keyCount = constraintKeys.Count;
- for (var i = 0; i < keyCount; i++)
- {
- lockedKeys.Remove(constraintKeys[i]);
- }
-
- // Check waiting tests to see if any can now run
- while (waitingTests.TryDequeue(out var waitingTest))
- {
- // Check if all constraint keys are available for this waiting test
- var canStart = true;
- var waitingKeys = waitingTest.ConstraintKeys;
- var waitingKeyCount = waitingKeys.Count;
- for (var j = 0; j < waitingKeyCount; j++)
- {
- if (lockedKeys.Contains(waitingKeys[j]))
- {
- canStart = false;
- break;
- }
- }
-
- if (canStart)
- {
- // Lock the keys for this test
- for (var j = 0; j < waitingKeyCount; j++)
- {
- lockedKeys.Add(waitingKeys[j]);
- }
-
- // Mark test to start after we exit the lock
- testsToStart.Add(waitingTest);
- }
- else
- {
- // Still can't run, keep it for re-queuing
- testsToRequeue.Add(waitingTest);
- }
- }
-
- // Re-add tests that still can't run
- foreach (var waitingTestItem in testsToRequeue)
- {
- waitingTests.Enqueue(waitingTestItem);
- }
-}
-```
-
-**Step 3: Run tests to verify behavior unchanged**
-
-Run: `cd TUnit.Engine.Tests && dotnet test`
-Expected: All tests PASS
-
-**Step 4: Commit**
-
-```bash
-git add TUnit.Engine/Scheduling/ConstraintKeyScheduler.cs
-git commit -m "perf: pre-allocate lists outside lock scope in ConstraintKeyScheduler"
-```
-
----
-
-## Task 5: Add ImmutableList to ReflectionTestDataCollector
-
-**Files:**
-- Modify: `TUnit.Engine/Discovery/ReflectionTestDataCollector.cs:31-32`
-
-**Step 1: Run existing tests to establish baseline**
-
-Run: `cd TUnit.Engine.Tests && dotnet test`
-Expected: All tests PASS
-
-**Step 2: Add System.Collections.Immutable using and change field declaration**
-
-At the top of `ReflectionTestDataCollector.cs`, ensure this using is present:
-
-```csharp
-using System.Collections.Immutable;
-```
-
-Change lines 31-32 from:
-
-```csharp
-private static readonly List _discoveredTests = new(capacity: 1000);
-private static readonly Lock _discoveredTestsLock = new();
-```
-
-To:
-
-```csharp
-private static ImmutableList _discoveredTests = ImmutableList.Empty;
-```
-
-**Step 3: Run tests (will fail - fields referenced elsewhere)**
-
-Run: `cd TUnit.Engine.Tests && dotnet test`
-Expected: FAIL (compilation errors due to field changes)
-
-**Step 4: Commit partial change**
-
-```bash
-git add TUnit.Engine/Discovery/ReflectionTestDataCollector.cs
-git commit -m "refactor: change _discoveredTests to ImmutableList (WIP)"
-```
-
----
-
-## Task 6: Update CollectTestsAsync for ImmutableList
-
-**Files:**
-- Modify: `TUnit.Engine/Discovery/ReflectionTestDataCollector.cs:136-141`
-
-**Step 1: Replace lock with atomic swap**
-
-Change lines 136-141 from:
-
-```csharp
-// Add to discovered tests with lock
-lock (_discoveredTestsLock)
-{
- _discoveredTests.AddRange(newTests);
- return new List(_discoveredTests);
-}
-```
-
-To:
-
-```csharp
-// Atomic swap - no lock needed for readers
-ImmutableList original, updated;
-do
-{
- original = _discoveredTests;
- updated = original.AddRange(newTests);
-} while (Interlocked.CompareExchange(ref _discoveredTests, updated, original) != original);
-
-return _discoveredTests;
-```
-
-**Step 2: Run tests (may still fail if other usages remain)**
-
-Run: `cd TUnit.Engine.Tests && dotnet test`
-Expected: May fail if streaming methods still use old lock
-
-**Step 3: Commit**
-
-```bash
-git add TUnit.Engine/Discovery/ReflectionTestDataCollector.cs
-git commit -m "perf: use atomic swap for CollectTestsAsync"
-```
-
----
-
-## Task 7: Update CollectTestsStreamingAsync for ImmutableList
-
-**Files:**
-- Modify: `TUnit.Engine/Discovery/ReflectionTestDataCollector.cs:174-190`
-
-**Step 1: Replace lock with ImmutableInterlocked.Update**
-
-Change lines 174-178 from:
-
-```csharp
-lock (_discoveredTestsLock)
-{
- _discoveredTests.Add(test);
-}
-yield return test;
-```
-
-To:
-
-```csharp
-ImmutableInterlocked.Update(ref _discoveredTests, list => list.Add(test));
-yield return test;
-```
-
-Similarly for lines 185-188:
-
-```csharp
-ImmutableInterlocked.Update(ref _discoveredTests, list => list.Add(dynamicTest));
-yield return dynamicTest;
-```
-
-**Step 2: Run tests to verify streaming works**
-
-Run: `cd TUnit.Engine.Tests && dotnet test`
-Expected: All tests PASS
-
-**Step 3: Commit**
-
-```bash
-git add TUnit.Engine/Discovery/ReflectionTestDataCollector.cs
-git commit -m "perf: use ImmutableInterlocked.Update for streaming discovery"
-```
-
----
-
-## Task 8: Update ClearCaches for ImmutableList
-
-**Files:**
-- Modify: `TUnit.Engine/Discovery/ReflectionTestDataCollector.cs:47-60`
-
-**Step 1: Simplify ClearCaches to use atomic assignment**
-
-Change lines 50-53 from:
-
-```csharp
-lock (_discoveredTestsLock)
-{
- _discoveredTests.Clear();
-}
-```
-
-To:
-
-```csharp
-Interlocked.Exchange(ref _discoveredTests, ImmutableList.Empty);
-```
-
-**Step 2: Run all tests**
-
-Run: `cd TUnit.Engine.Tests && dotnet test`
-Expected: All tests PASS
-
-**Step 3: Commit**
-
-```bash
-git add TUnit.Engine/Discovery/ReflectionTestDataCollector.cs
-git commit -m "perf: simplify ClearCaches with atomic exchange"
-```
-
----
-
-## Task 9: Run Full Test Suite and Performance Verification
-
-**Files:**
-- None (verification only)
-
-**Step 1: Run full TUnit test suite**
-
-Run: `dotnet test`
-Expected: All tests PASS
-
-**Step 2: Run performance tests with dotnet-trace**
-
-```bash
-cd TUnit.PerformanceTests
-dotnet trace collect -- dotnet run -c Release
-```
-
-**Step 3: Verify no regressions**
-
-Compare trace output with baseline (if available). Look for:
-- Reduced lock contention time
-- Reduced thread wait time
-- Similar or better wall clock time
-
-**Step 4: Final commit with summary**
-
-```bash
-git add -A
-git commit -m "perf: reduce lock contention in test discovery and scheduling
-
-Implements optimizations for #4162:
-- ReflectionTestDataCollector: ImmutableList with atomic swap
-- ConstraintKeyScheduler: manual loops replacing LINQ, pre-allocated lists
-
-This eliminates defensive copies under lock and reduces LINQ allocations
-in hot paths during parallel test execution."
-```
-
----
-
-## Verification Checklist
-
-- [ ] All existing tests pass
-- [ ] Stress tests pass under high contention
-- [ ] No deadlocks under parallel execution
-- [ ] Wall clock time equal or improved
-- [ ] Lock contention reduced (verify with dotnet-trace)
-
----
-
-Plan complete and saved to `docs/plans/2025-12-26-lock-contention-optimization.md`. Two execution options:
-
-**1. Subagent-Driven (this session)** - I dispatch fresh subagent per task, review between tasks, fast iteration
-
-**2. Parallel Session (separate)** - Open new session with executing-plans, batch execution with checkpoints
-
-**Which approach?**
diff --git a/docs/plans/2025-12-28-json-assertions-design.md b/docs/plans/2025-12-28-json-assertions-design.md
deleted file mode 100644
index 40f3b62377..0000000000
--- a/docs/plans/2025-12-28-json-assertions-design.md
+++ /dev/null
@@ -1,230 +0,0 @@
-# JSON Assertions Design
-
-**Issue**: [#4178 - JsonElement assertions](https://github.com/thomhurst/TUnit/issues/4178)
-**Date**: 2025-12-28
-**Status**: Approved Design
-
-## Summary
-
-Add comprehensive JSON assertions to TUnit supporting both `JsonElement` and `JsonNode` type hierarchies, with runtime-appropriate features via conditional compilation.
-
-## Problem Statement
-
-Currently, `Assert.That(json1).IsEqualTo(json2)` performs string comparison on JSON, which:
-- Fails on semantically equivalent JSON with different whitespace
-- Provides unhelpful error messages that don't indicate where differences occur
-
-Users need:
-- Semantic JSON comparison (ignoring formatting)
-- Detailed error messages with paths to differences (e.g., "differs at $.abc.def")
-
-## Design Decisions
-
-| Decision | Choice | Rationale |
-|----------|--------|-----------|
-| Target Types | Both JsonElement and JsonNode | Complete coverage for all System.Text.Json users |
-| Equality Logic | Built-in `DeepEquals` | Leverage .NET's tested implementation |
-| Runtime Support | Per-runtime via `#if` | Provide what's available without polyfills |
-| Assertion Categories | Equality, Validity, Property, Type, Array | Comprehensive without external dependencies |
-| Path Queries | Deferred | JSONPath not built-in, avoid dependencies |
-| Error Messages | Path-to-difference | Addresses core issue request |
-| API Entry Point | `Assert.That()` extensions | Consistent with TUnit patterns |
-| Package Location | Inside TUnit.Assertions | No extra package for users |
-| Implementation | `[GenerateAssertion]` | Simplified code, source generator handles infrastructure |
-
-## Runtime Availability Matrix
-
-| Assertion | .NET 6-7 | .NET 8 | .NET 9+ |
-|-----------|----------|--------|---------|
-| `IsEqualTo` (JsonNode) | - | Y | Y |
-| `IsEqualTo` (JsonElement) | - | - | Y |
-| `IsValidJson` (string) | Y | Y | Y |
-| `HasProperty` | Y | Y | Y |
-| Type checks (`IsObject`, etc.) | Y | Y | Y |
-| Array assertions | Y | Y | Y |
-
-## File Structure
-
-```
-TUnit.Assertions/Conditions/Json/
-├── JsonElementAssertionExtensions.cs
-├── JsonNodeAssertionExtensions.cs
-├── JsonStringAssertionExtensions.cs
-└── JsonDiffHelper.cs
-```
-
-## Assertion Inventory
-
-### JsonElement Assertions
-
-```csharp
-// Type checking (all runtimes)
-IsObject()
-IsArray()
-IsString()
-IsNumber()
-IsBoolean()
-IsNull()
-IsNotNull()
-
-// Property access (all runtimes)
-HasProperty(string propertyName)
-DoesNotHaveProperty(string propertyName)
-
-// Equality (.NET 9+ only)
-#if NET9_0_OR_GREATER
-IsEqualTo(JsonElement expected)
-IsNotEqualTo(JsonElement expected)
-#endif
-```
-
-### JsonNode Assertions
-
-```csharp
-// Type checking (all runtimes)
-IsObject()
-IsArray()
-IsValue()
-
-// Property access (all runtimes)
-HasProperty(string propertyName)
-DoesNotHaveProperty(string propertyName)
-
-// Equality (.NET 8+ only)
-#if NET8_0_OR_GREATER
-IsEqualTo(JsonNode? expected)
-IsNotEqualTo(JsonNode? expected)
-#endif
-```
-
-### JsonArray Assertions
-
-```csharp
-// All runtimes
-IsEmpty()
-IsNotEmpty()
-HasCount(int expected)
-```
-
-### String (Raw JSON) Assertions
-
-```csharp
-// All runtimes
-IsValidJson()
-IsNotValidJson()
-IsValidJsonObject()
-IsValidJsonArray()
-```
-
-## Implementation Pattern
-
-Using `[GenerateAssertion]` for simplified code:
-
-```csharp
-using System.Text.Json;
-using TUnit.Assertions.Attributes;
-using TUnit.Assertions.Core;
-
-namespace TUnit.Assertions.Conditions.Json;
-
-file static partial class JsonElementAssertionExtensions
-{
- [GenerateAssertion(ExpectationMessage = "to be a JSON object", InlineMethodBody = true)]
- public static bool IsObject(this JsonElement value)
- => value.ValueKind == JsonValueKind.Object;
-
- [GenerateAssertion(ExpectationMessage = "to have property \"{propertyName}\"")]
- public static bool HasProperty(this JsonElement value, string propertyName)
- => value.TryGetProperty(propertyName, out _);
-
-#if NET9_0_OR_GREATER
- [GenerateAssertion(ExpectationMessage = "to be equal to {expected}")]
- public static AssertionResult IsEqualTo(this JsonElement value, JsonElement expected)
- {
- if (JsonElement.DeepEquals(value, expected))
- return AssertionResult.Passed;
-
- var diff = JsonDiffHelper.FindFirstDifference(value, expected);
- return AssertionResult.Failed(
- $"differs at {diff.Path}: expected {diff.Expected} but found {diff.Actual}");
- }
-#endif
-}
-```
-
-## JsonDiffHelper
-
-Provides path-to-difference for error messages:
-
-```csharp
-internal static class JsonDiffHelper
-{
- public readonly record struct DiffResult(string Path, string Expected, string Actual);
-
- public static DiffResult FindFirstDifference(JsonElement left, JsonElement right)
- {
- return FindDiff(left, right, "$");
- }
-
- private static DiffResult FindDiff(JsonElement left, JsonElement right, string path)
- {
- if (left.ValueKind != right.ValueKind)
- return new DiffResult(path, right.ValueKind.ToString(), left.ValueKind.ToString());
-
- return left.ValueKind switch
- {
- JsonValueKind.Object => CompareObjects(left, right, path),
- JsonValueKind.Array => CompareArrays(left, right, path),
- _ => ComparePrimitives(left, right, path)
- };
- }
- // ... implementation details
-}
-```
-
-## Error Message Examples
-
-```
-Expected JSON to be equal to {"name":"Alice","age":31}
-but differs at $.age: expected 31 but found 30
-
-Expected JSON to be equal to {"users":[{"id":1}]}
-but differs at $.users[0].id: expected 1 but found 2
-
-Expected JSON to be equal to {"active":true}
-but differs at $.active: expected True but found False
-```
-
-## Usage Examples
-
-```csharp
-// Type checking
-await Assert.That(element).IsObject();
-await Assert.That(node).IsArray();
-
-// Property access
-await Assert.That(element).HasProperty("name");
-await Assert.That(element).DoesNotHaveProperty("deleted");
-
-// Equality (.NET 8+/9+)
-await Assert.That(element).IsEqualTo(expectedElement);
-await Assert.That(node).IsEqualTo(expectedNode);
-
-// String validation
-await Assert.That(jsonString).IsValidJson();
-await Assert.That(jsonString).IsValidJsonObject();
-```
-
-## Testing Strategy
-
-- Unit tests in `TUnit.Assertions.Tests` with `#if` for runtime-specific tests
-- Test both pass and fail scenarios for each assertion
-- Verify error message format includes correct JSON paths
-- Multi-target test project to validate behavior on each runtime
-
-## Future Considerations
-
-- JSONPath support via optional dependency or built-in simple path syntax
-- `HasPropertyWithValue(name, value)` combined assertion
-- JSON Schema validation
-- Partial matching / subset assertions
diff --git a/docs/plans/2025-12-28-json-assertions.md b/docs/plans/2025-12-28-json-assertions.md
deleted file mode 100644
index 7c728655a2..0000000000
--- a/docs/plans/2025-12-28-json-assertions.md
+++ /dev/null
@@ -1,991 +0,0 @@
-# JSON Assertions Implementation Plan
-
-> **For Claude:** REQUIRED SUB-SKILL: Use superpowers:executing-plans to implement this plan task-by-task.
-
-**Goal:** Add comprehensive JSON assertions for JsonElement, JsonNode, and string types with semantic equality and path-to-difference error messages.
-
-**Architecture:** Use `[GenerateAssertion]` attribute pattern for all assertions. Conditional compilation (`#if NET8_0_OR_GREATER`, `#if NET9_0_OR_GREATER`) enables runtime-specific features. JsonDiffHelper provides detailed error messages showing where JSON structures differ.
-
-**Tech Stack:** System.Text.Json (built-in), TUnit.Assertions source generator, C# 12
-
----
-
-## Task 1: Create JsonDiffHelper
-
-**Files:**
-- Create: `TUnit.Assertions/Conditions/Json/JsonDiffHelper.cs`
-
-**Step 1: Create the Json directory**
-
-```bash
-mkdir TUnit.Assertions/Conditions/Json
-```
-
-**Step 2: Write the JsonDiffHelper**
-
-Create `TUnit.Assertions/Conditions/Json/JsonDiffHelper.cs`:
-
-```csharp
-using System.Text.Json;
-
-namespace TUnit.Assertions.Conditions.Json;
-
-internal static class JsonDiffHelper
-{
- public readonly record struct DiffResult(string Path, string Expected, string Actual);
-
- public static DiffResult FindFirstDifference(JsonElement left, JsonElement right)
- {
- return FindDiff(left, right, "$");
- }
-
- private static DiffResult FindDiff(JsonElement left, JsonElement right, string path)
- {
- if (left.ValueKind != right.ValueKind)
- {
- return new DiffResult(path, right.ValueKind.ToString(), left.ValueKind.ToString());
- }
-
- return left.ValueKind switch
- {
- JsonValueKind.Object => CompareObjects(left, right, path),
- JsonValueKind.Array => CompareArrays(left, right, path),
- _ => ComparePrimitives(left, right, path)
- };
- }
-
- private static DiffResult CompareObjects(JsonElement left, JsonElement right, string path)
- {
- // Check for missing properties in left that exist in right
- foreach (var prop in right.EnumerateObject())
- {
- var propPath = $"{path}.{prop.Name}";
- if (!left.TryGetProperty(prop.Name, out var leftProp))
- {
- return new DiffResult(propPath, FormatValue(prop.Value), "(missing)");
- }
-
- var diff = FindDiff(leftProp, prop.Value, propPath);
- if (!string.IsNullOrEmpty(diff.Expected) || !string.IsNullOrEmpty(diff.Actual))
- {
- return diff;
- }
- }
-
- // Check for extra properties in left that don't exist in right
- foreach (var prop in left.EnumerateObject())
- {
- var propPath = $"{path}.{prop.Name}";
- if (!right.TryGetProperty(prop.Name, out _))
- {
- return new DiffResult(propPath, "(missing)", FormatValue(prop.Value));
- }
- }
-
- return default;
- }
-
- private static DiffResult CompareArrays(JsonElement left, JsonElement right, string path)
- {
- var leftLength = left.GetArrayLength();
- var rightLength = right.GetArrayLength();
-
- if (leftLength != rightLength)
- {
- return new DiffResult($"{path}.Length", rightLength.ToString(), leftLength.ToString());
- }
-
- var leftEnumerator = left.EnumerateArray();
- var rightEnumerator = right.EnumerateArray();
- var index = 0;
-
- while (leftEnumerator.MoveNext() && rightEnumerator.MoveNext())
- {
- var itemPath = $"{path}[{index}]";
- var diff = FindDiff(leftEnumerator.Current, rightEnumerator.Current, itemPath);
- if (!string.IsNullOrEmpty(diff.Expected) || !string.IsNullOrEmpty(diff.Actual))
- {
- return diff;
- }
- index++;
- }
-
- return default;
- }
-
- private static DiffResult ComparePrimitives(JsonElement left, JsonElement right, string path)
- {
- var leftText = FormatValue(left);
- var rightText = FormatValue(right);
-
- if (leftText != rightText)
- {
- return new DiffResult(path, rightText, leftText);
- }
-
- return default;
- }
-
- private static string FormatValue(JsonElement element)
- {
- return element.ValueKind switch
- {
- JsonValueKind.String => $"\"{element.GetString()}\"",
- JsonValueKind.Number => element.GetRawText(),
- JsonValueKind.True => "true",
- JsonValueKind.False => "false",
- JsonValueKind.Null => "null",
- JsonValueKind.Object => "{...}",
- JsonValueKind.Array => "[...]",
- _ => element.GetRawText()
- };
- }
-}
-```
-
-**Step 3: Verify it compiles**
-
-```bash
-cd C:/git/TUnit && dotnet build TUnit.Assertions/TUnit.Assertions.csproj --no-restore -v q
-```
-
-Expected: Build succeeded
-
-**Step 4: Commit**
-
-```bash
-git add TUnit.Assertions/Conditions/Json/JsonDiffHelper.cs
-git commit -m "feat(assertions): add JsonDiffHelper for path-to-difference error messages"
-```
-
----
-
-## Task 2: Create JsonElement Type Assertions
-
-**Files:**
-- Create: `TUnit.Assertions/Conditions/Json/JsonElementAssertionExtensions.cs`
-- Test: `TUnit.Assertions.Tests/JsonElementAssertionTests.cs`
-
-**Step 1: Write the failing test**
-
-Create `TUnit.Assertions.Tests/JsonElementAssertionTests.cs`:
-
-```csharp
-using System.Text.Json;
-using TUnit.Assertions.Extensions;
-
-namespace TUnit.Assertions.Tests;
-
-public class JsonElementAssertionTests
-{
- [Test]
- public async Task IsObject_WithObject_Passes()
- {
- using var doc = JsonDocument.Parse("{\"name\":\"test\"}");
- await Assert.That(doc.RootElement).IsObject();
- }
-
- [Test]
- public async Task IsObject_WithArray_Fails()
- {
- using var doc = JsonDocument.Parse("[1,2,3]");
- await Assert.ThrowsAsync(
- async () => await Assert.That(doc.RootElement).IsObject());
- }
-
- [Test]
- public async Task IsArray_WithArray_Passes()
- {
- using var doc = JsonDocument.Parse("[1,2,3]");
- await Assert.That(doc.RootElement).IsArray();
- }
-
- [Test]
- public async Task IsString_WithString_Passes()
- {
- using var doc = JsonDocument.Parse("\"hello\"");
- await Assert.That(doc.RootElement).IsString();
- }
-
- [Test]
- public async Task IsNumber_WithNumber_Passes()
- {
- using var doc = JsonDocument.Parse("42");
- await Assert.That(doc.RootElement).IsNumber();
- }
-
- [Test]
- public async Task IsBoolean_WithTrue_Passes()
- {
- using var doc = JsonDocument.Parse("true");
- await Assert.That(doc.RootElement).IsBoolean();
- }
-
- [Test]
- public async Task IsNull_WithNull_Passes()
- {
- using var doc = JsonDocument.Parse("null");
- await Assert.That(doc.RootElement).IsNull();
- }
-
- [Test]
- public async Task IsNotNull_WithObject_Passes()
- {
- using var doc = JsonDocument.Parse("{}");
- await Assert.That(doc.RootElement).IsNotNull();
- }
-}
-```
-
-**Step 2: Run test to verify it fails**
-
-```bash
-cd C:/git/TUnit && dotnet test TUnit.Assertions.Tests/TUnit.Assertions.Tests.csproj --filter "FullyQualifiedName~JsonElementAssertionTests" --no-build
-```
-
-Expected: FAIL - IsObject method not found
-
-**Step 3: Write the JsonElement type assertions**
-
-Create `TUnit.Assertions/Conditions/Json/JsonElementAssertionExtensions.cs`:
-
-```csharp
-using System.Text.Json;
-using TUnit.Assertions.Attributes;
-
-namespace TUnit.Assertions.Conditions.Json;
-
-///
-/// Source-generated assertions for JsonElement type checking.
-///
-file static partial class JsonElementAssertionExtensions
-{
- [GenerateAssertion(ExpectationMessage = "to be a JSON object", InlineMethodBody = true)]
- public static bool IsObject(this JsonElement value)
- => value.ValueKind == JsonValueKind.Object;
-
- [GenerateAssertion(ExpectationMessage = "to be a JSON array", InlineMethodBody = true)]
- public static bool IsArray(this JsonElement value)
- => value.ValueKind == JsonValueKind.Array;
-
- [GenerateAssertion(ExpectationMessage = "to be a JSON string", InlineMethodBody = true)]
- public static bool IsString(this JsonElement value)
- => value.ValueKind == JsonValueKind.String;
-
- [GenerateAssertion(ExpectationMessage = "to be a JSON number", InlineMethodBody = true)]
- public static bool IsNumber(this JsonElement value)
- => value.ValueKind == JsonValueKind.Number;
-
- [GenerateAssertion(ExpectationMessage = "to be a JSON boolean", InlineMethodBody = true)]
- public static bool IsBoolean(this JsonElement value)
- => value.ValueKind == JsonValueKind.True || value.ValueKind == JsonValueKind.False;
-
- [GenerateAssertion(ExpectationMessage = "to be JSON null", InlineMethodBody = true)]
- public static bool IsNull(this JsonElement value)
- => value.ValueKind == JsonValueKind.Null;
-
- [GenerateAssertion(ExpectationMessage = "to not be JSON null", InlineMethodBody = true)]
- public static bool IsNotNull(this JsonElement value)
- => value.ValueKind != JsonValueKind.Null;
-}
-```
-
-**Step 4: Build to generate extension methods**
-
-```bash
-cd C:/git/TUnit && dotnet build TUnit.Assertions/TUnit.Assertions.csproj -v q
-```
-
-Expected: Build succeeded
-
-**Step 5: Run tests to verify they pass**
-
-```bash
-cd C:/git/TUnit && dotnet test TUnit.Assertions.Tests/TUnit.Assertions.Tests.csproj --filter "FullyQualifiedName~JsonElementAssertionTests"
-```
-
-Expected: All tests pass
-
-**Step 6: Commit**
-
-```bash
-git add TUnit.Assertions/Conditions/Json/JsonElementAssertionExtensions.cs TUnit.Assertions.Tests/JsonElementAssertionTests.cs
-git commit -m "feat(assertions): add JsonElement type checking assertions"
-```
-
----
-
-## Task 3: Add JsonElement Property Assertions
-
-**Files:**
-- Modify: `TUnit.Assertions/Conditions/Json/JsonElementAssertionExtensions.cs`
-- Modify: `TUnit.Assertions.Tests/JsonElementAssertionTests.cs`
-
-**Step 1: Write the failing tests**
-
-Add to `TUnit.Assertions.Tests/JsonElementAssertionTests.cs`:
-
-```csharp
- [Test]
- public async Task HasProperty_WhenPropertyExists_Passes()
- {
- using var doc = JsonDocument.Parse("{\"name\":\"Alice\",\"age\":30}");
- await Assert.That(doc.RootElement).HasProperty("name");
- }
-
- [Test]
- public async Task HasProperty_WhenPropertyMissing_Fails()
- {
- using var doc = JsonDocument.Parse("{\"name\":\"Alice\"}");
- await Assert.ThrowsAsync(
- async () => await Assert.That(doc.RootElement).HasProperty("missing"));
- }
-
- [Test]
- public async Task DoesNotHaveProperty_WhenPropertyMissing_Passes()
- {
- using var doc = JsonDocument.Parse("{\"name\":\"Alice\"}");
- await Assert.That(doc.RootElement).DoesNotHaveProperty("missing");
- }
-
- [Test]
- public async Task DoesNotHaveProperty_WhenPropertyExists_Fails()
- {
- using var doc = JsonDocument.Parse("{\"name\":\"Alice\"}");
- await Assert.ThrowsAsync(
- async () => await Assert.That(doc.RootElement).DoesNotHaveProperty("name"));
- }
-```
-
-**Step 2: Run tests to verify they fail**
-
-```bash
-cd C:/git/TUnit && dotnet test TUnit.Assertions.Tests/TUnit.Assertions.Tests.csproj --filter "FullyQualifiedName~JsonElementAssertionTests.HasProperty" --no-build
-```
-
-Expected: FAIL - HasProperty method not found
-
-**Step 3: Add property assertions**
-
-Add to `TUnit.Assertions/Conditions/Json/JsonElementAssertionExtensions.cs`:
-
-```csharp
- [GenerateAssertion(ExpectationMessage = "to have property \"{propertyName}\"", InlineMethodBody = true)]
- public static bool HasProperty(this JsonElement value, string propertyName)
- => value.ValueKind == JsonValueKind.Object && value.TryGetProperty(propertyName, out _);
-
- [GenerateAssertion(ExpectationMessage = "to not have property \"{propertyName}\"", InlineMethodBody = true)]
- public static bool DoesNotHaveProperty(this JsonElement value, string propertyName)
- => value.ValueKind != JsonValueKind.Object || !value.TryGetProperty(propertyName, out _);
-```
-
-**Step 4: Run tests to verify they pass**
-
-```bash
-cd C:/git/TUnit && dotnet test TUnit.Assertions.Tests/TUnit.Assertions.Tests.csproj --filter "FullyQualifiedName~JsonElementAssertionTests"
-```
-
-Expected: All tests pass
-
-**Step 5: Commit**
-
-```bash
-git add TUnit.Assertions/Conditions/Json/JsonElementAssertionExtensions.cs TUnit.Assertions.Tests/JsonElementAssertionTests.cs
-git commit -m "feat(assertions): add JsonElement property assertions"
-```
-
----
-
-## Task 4: Add JsonElement Equality Assertions (.NET 9+)
-
-**Files:**
-- Modify: `TUnit.Assertions/Conditions/Json/JsonElementAssertionExtensions.cs`
-- Modify: `TUnit.Assertions.Tests/JsonElementAssertionTests.cs`
-
-**Step 1: Write the failing tests**
-
-Add to `TUnit.Assertions.Tests/JsonElementAssertionTests.cs`:
-
-```csharp
-#if NET9_0_OR_GREATER
- [Test]
- public async Task IsEqualTo_WithIdenticalJson_Passes()
- {
- using var doc1 = JsonDocument.Parse("{\"name\":\"Alice\",\"age\":30}");
- using var doc2 = JsonDocument.Parse("{\"name\":\"Alice\",\"age\":30}");
- await Assert.That(doc1.RootElement).IsEqualTo(doc2.RootElement);
- }
-
- [Test]
- public async Task IsEqualTo_WithDifferentWhitespace_Passes()
- {
- using var doc1 = JsonDocument.Parse("{ \"name\" : \"Alice\" }");
- using var doc2 = JsonDocument.Parse("{\"name\":\"Alice\"}");
- await Assert.That(doc1.RootElement).IsEqualTo(doc2.RootElement);
- }
-
- [Test]
- public async Task IsEqualTo_WithDifferentValues_FailsWithPath()
- {
- using var doc1 = JsonDocument.Parse("{\"name\":\"Alice\",\"age\":30}");
- using var doc2 = JsonDocument.Parse("{\"name\":\"Alice\",\"age\":31}");
-
- var exception = await Assert.ThrowsAsync(
- async () => await Assert.That(doc1.RootElement).IsEqualTo(doc2.RootElement));
-
- await Assert.That(exception.Message).Contains("$.age");
- }
-
- [Test]
- public async Task IsNotEqualTo_WithDifferentJson_Passes()
- {
- using var doc1 = JsonDocument.Parse("{\"name\":\"Alice\"}");
- using var doc2 = JsonDocument.Parse("{\"name\":\"Bob\"}");
- await Assert.That(doc1.RootElement).IsNotEqualTo(doc2.RootElement);
- }
-#endif
-```
-
-**Step 2: Run tests to verify they fail**
-
-```bash
-cd C:/git/TUnit && dotnet test TUnit.Assertions.Tests/TUnit.Assertions.Tests.csproj --filter "FullyQualifiedName~JsonElementAssertionTests.IsEqualTo" --framework net9.0 --no-build
-```
-
-Expected: FAIL - IsEqualTo method not found
-
-**Step 3: Add equality assertions**
-
-Add to `TUnit.Assertions/Conditions/Json/JsonElementAssertionExtensions.cs`:
-
-```csharp
-#if NET9_0_OR_GREATER
- [GenerateAssertion(ExpectationMessage = "to be equal to {expected}")]
- public static TUnit.Assertions.Core.AssertionResult IsEqualTo(this JsonElement value, JsonElement expected)
- {
- if (JsonElement.DeepEquals(value, expected))
- {
- return TUnit.Assertions.Core.AssertionResult.Passed;
- }
-
- var diff = JsonDiffHelper.FindFirstDifference(value, expected);
- return TUnit.Assertions.Core.AssertionResult.Failed(
- $"differs at {diff.Path}: expected {diff.Expected} but found {diff.Actual}");
- }
-
- [GenerateAssertion(ExpectationMessage = "to not be equal to {expected}", InlineMethodBody = true)]
- public static bool IsNotEqualTo(this JsonElement value, JsonElement expected)
- => !JsonElement.DeepEquals(value, expected);
-#endif
-```
-
-**Step 4: Run tests to verify they pass**
-
-```bash
-cd C:/git/TUnit && dotnet test TUnit.Assertions.Tests/TUnit.Assertions.Tests.csproj --filter "FullyQualifiedName~JsonElementAssertionTests" --framework net9.0
-```
-
-Expected: All tests pass
-
-**Step 5: Commit**
-
-```bash
-git add TUnit.Assertions/Conditions/Json/JsonElementAssertionExtensions.cs TUnit.Assertions.Tests/JsonElementAssertionTests.cs
-git commit -m "feat(assertions): add JsonElement equality assertions for .NET 9+"
-```
-
----
-
-## Task 5: Create JsonNode Assertions
-
-**Files:**
-- Create: `TUnit.Assertions/Conditions/Json/JsonNodeAssertionExtensions.cs`
-- Create: `TUnit.Assertions.Tests/JsonNodeAssertionTests.cs`
-
-**Step 1: Write the failing test**
-
-Create `TUnit.Assertions.Tests/JsonNodeAssertionTests.cs`:
-
-```csharp
-using System.Text.Json.Nodes;
-using TUnit.Assertions.Extensions;
-
-namespace TUnit.Assertions.Tests;
-
-public class JsonNodeAssertionTests
-{
- [Test]
- public async Task IsObject_WithJsonObject_Passes()
- {
- JsonNode? node = JsonNode.Parse("{\"name\":\"test\"}");
- await Assert.That(node).IsJsonObject();
- }
-
- [Test]
- public async Task IsArray_WithJsonArray_Passes()
- {
- JsonNode? node = JsonNode.Parse("[1,2,3]");
- await Assert.That(node).IsJsonArray();
- }
-
- [Test]
- public async Task IsValue_WithJsonValue_Passes()
- {
- JsonNode? node = JsonNode.Parse("42");
- await Assert.That(node).IsJsonValue();
- }
-
- [Test]
- public async Task HasProperty_WhenPropertyExists_Passes()
- {
- JsonNode? node = JsonNode.Parse("{\"name\":\"Alice\"}");
- await Assert.That(node).HasJsonProperty("name");
- }
-
- [Test]
- public async Task DoesNotHaveProperty_WhenPropertyMissing_Passes()
- {
- JsonNode? node = JsonNode.Parse("{\"name\":\"Alice\"}");
- await Assert.That(node).DoesNotHaveJsonProperty("missing");
- }
-
-#if NET8_0_OR_GREATER
- [Test]
- public async Task IsEqualTo_WithIdenticalJson_Passes()
- {
- JsonNode? node1 = JsonNode.Parse("{\"name\":\"Alice\",\"age\":30}");
- JsonNode? node2 = JsonNode.Parse("{\"name\":\"Alice\",\"age\":30}");
- await Assert.That(node1).IsEqualTo(node2);
- }
-
- [Test]
- public async Task IsNotEqualTo_WithDifferentJson_Passes()
- {
- JsonNode? node1 = JsonNode.Parse("{\"name\":\"Alice\"}");
- JsonNode? node2 = JsonNode.Parse("{\"name\":\"Bob\"}");
- await Assert.That(node1).IsNotEqualTo(node2);
- }
-#endif
-}
-```
-
-**Step 2: Run tests to verify they fail**
-
-```bash
-cd C:/git/TUnit && dotnet test TUnit.Assertions.Tests/TUnit.Assertions.Tests.csproj --filter "FullyQualifiedName~JsonNodeAssertionTests" --no-build
-```
-
-Expected: FAIL - methods not found
-
-**Step 3: Write the JsonNode assertions**
-
-Create `TUnit.Assertions/Conditions/Json/JsonNodeAssertionExtensions.cs`:
-
-```csharp
-using System.Text.Json.Nodes;
-using TUnit.Assertions.Attributes;
-
-namespace TUnit.Assertions.Conditions.Json;
-
-///
-/// Source-generated assertions for JsonNode types.
-///
-file static partial class JsonNodeAssertionExtensions
-{
- [GenerateAssertion(ExpectationMessage = "to be a JsonObject", InlineMethodBody = true)]
- public static bool IsJsonObject(this JsonNode? value)
- => value is JsonObject;
-
- [GenerateAssertion(ExpectationMessage = "to be a JsonArray", InlineMethodBody = true)]
- public static bool IsJsonArray(this JsonNode? value)
- => value is JsonArray;
-
- [GenerateAssertion(ExpectationMessage = "to be a JsonValue", InlineMethodBody = true)]
- public static bool IsJsonValue(this JsonNode? value)
- => value is JsonValue;
-
- [GenerateAssertion(ExpectationMessage = "to have property \"{propertyName}\"", InlineMethodBody = true)]
- public static bool HasJsonProperty(this JsonNode? value, string propertyName)
- => value is JsonObject obj && obj.ContainsKey(propertyName);
-
- [GenerateAssertion(ExpectationMessage = "to not have property \"{propertyName}\"", InlineMethodBody = true)]
- public static bool DoesNotHaveJsonProperty(this JsonNode? value, string propertyName)
- => value is not JsonObject obj || !obj.ContainsKey(propertyName);
-
-#if NET8_0_OR_GREATER
- [GenerateAssertion(ExpectationMessage = "to be equal to {expected}", InlineMethodBody = true)]
- public static bool IsEqualTo(this JsonNode? value, JsonNode? expected)
- => JsonNode.DeepEquals(value, expected);
-
- [GenerateAssertion(ExpectationMessage = "to not be equal to {expected}", InlineMethodBody = true)]
- public static bool IsNotEqualTo(this JsonNode? value, JsonNode? expected)
- => !JsonNode.DeepEquals(value, expected);
-#endif
-}
-```
-
-**Step 4: Run tests to verify they pass**
-
-```bash
-cd C:/git/TUnit && dotnet test TUnit.Assertions.Tests/TUnit.Assertions.Tests.csproj --filter "FullyQualifiedName~JsonNodeAssertionTests"
-```
-
-Expected: All tests pass
-
-**Step 5: Commit**
-
-```bash
-git add TUnit.Assertions/Conditions/Json/JsonNodeAssertionExtensions.cs TUnit.Assertions.Tests/JsonNodeAssertionTests.cs
-git commit -m "feat(assertions): add JsonNode assertions with equality for .NET 8+"
-```
-
----
-
-## Task 6: Create JsonArray Assertions
-
-**Files:**
-- Modify: `TUnit.Assertions/Conditions/Json/JsonNodeAssertionExtensions.cs`
-- Modify: `TUnit.Assertions.Tests/JsonNodeAssertionTests.cs`
-
-**Step 1: Write the failing tests**
-
-Add to `TUnit.Assertions.Tests/JsonNodeAssertionTests.cs`:
-
-```csharp
- [Test]
- public async Task IsEmpty_WithEmptyArray_Passes()
- {
- var array = new JsonArray();
- await Assert.That(array).IsJsonArrayEmpty();
- }
-
- [Test]
- public async Task IsNotEmpty_WithNonEmptyArray_Passes()
- {
- var array = new JsonArray(1, 2, 3);
- await Assert.That(array).IsJsonArrayNotEmpty();
- }
-
- [Test]
- public async Task HasCount_WithMatchingCount_Passes()
- {
- var array = new JsonArray(1, 2, 3);
- await Assert.That(array).HasJsonArrayCount(3);
- }
-```
-
-**Step 2: Run tests to verify they fail**
-
-```bash
-cd C:/git/TUnit && dotnet test TUnit.Assertions.Tests/TUnit.Assertions.Tests.csproj --filter "FullyQualifiedName~JsonNodeAssertionTests.IsEmpty" --no-build
-```
-
-Expected: FAIL - methods not found
-
-**Step 3: Add JsonArray assertions**
-
-Add to `TUnit.Assertions/Conditions/Json/JsonNodeAssertionExtensions.cs`:
-
-```csharp
- [GenerateAssertion(ExpectationMessage = "to be an empty JSON array", InlineMethodBody = true)]
- public static bool IsJsonArrayEmpty(this JsonArray value)
- => value.Count == 0;
-
- [GenerateAssertion(ExpectationMessage = "to not be an empty JSON array", InlineMethodBody = true)]
- public static bool IsJsonArrayNotEmpty(this JsonArray value)
- => value.Count > 0;
-
- [GenerateAssertion(ExpectationMessage = "to have {expected} elements")]
- public static TUnit.Assertions.Core.AssertionResult HasJsonArrayCount(this JsonArray value, int expected)
- {
- if (value.Count == expected)
- {
- return TUnit.Assertions.Core.AssertionResult.Passed;
- }
- return TUnit.Assertions.Core.AssertionResult.Failed($"has {value.Count} elements");
- }
-```
-
-**Step 4: Run tests to verify they pass**
-
-```bash
-cd C:/git/TUnit && dotnet test TUnit.Assertions.Tests/TUnit.Assertions.Tests.csproj --filter "FullyQualifiedName~JsonNodeAssertionTests"
-```
-
-Expected: All tests pass
-
-**Step 5: Commit**
-
-```bash
-git add TUnit.Assertions/Conditions/Json/JsonNodeAssertionExtensions.cs TUnit.Assertions.Tests/JsonNodeAssertionTests.cs
-git commit -m "feat(assertions): add JsonArray count and empty assertions"
-```
-
----
-
-## Task 7: Create String JSON Validation Assertions
-
-**Files:**
-- Create: `TUnit.Assertions/Conditions/Json/JsonStringAssertionExtensions.cs`
-- Create: `TUnit.Assertions.Tests/JsonStringAssertionTests.cs`
-
-**Step 1: Write the failing test**
-
-Create `TUnit.Assertions.Tests/JsonStringAssertionTests.cs`:
-
-```csharp
-using TUnit.Assertions.Extensions;
-
-namespace TUnit.Assertions.Tests;
-
-public class JsonStringAssertionTests
-{
- [Test]
- public async Task IsValidJson_WithValidJson_Passes()
- {
- var json = "{\"name\":\"Alice\"}";
- await Assert.That(json).IsValidJson();
- }
-
- [Test]
- public async Task IsValidJson_WithInvalidJson_Fails()
- {
- var json = "not valid json";
- await Assert.ThrowsAsync(
- async () => await Assert.That(json).IsValidJson());
- }
-
- [Test]
- public async Task IsNotValidJson_WithInvalidJson_Passes()
- {
- var json = "not valid json";
- await Assert.That(json).IsNotValidJson();
- }
-
- [Test]
- public async Task IsValidJsonObject_WithObject_Passes()
- {
- var json = "{\"name\":\"Alice\"}";
- await Assert.That(json).IsValidJsonObject();
- }
-
- [Test]
- public async Task IsValidJsonObject_WithArray_Fails()
- {
- var json = "[1,2,3]";
- await Assert.ThrowsAsync(
- async () => await Assert.That(json).IsValidJsonObject());
- }
-
- [Test]
- public async Task IsValidJsonArray_WithArray_Passes()
- {
- var json = "[1,2,3]";
- await Assert.That(json).IsValidJsonArray();
- }
-
- [Test]
- public async Task IsValidJsonArray_WithObject_Fails()
- {
- var json = "{\"name\":\"Alice\"}";
- await Assert.ThrowsAsync(
- async () => await Assert.That(json).IsValidJsonArray());
- }
-}
-```
-
-**Step 2: Run tests to verify they fail**
-
-```bash
-cd C:/git/TUnit && dotnet test TUnit.Assertions.Tests/TUnit.Assertions.Tests.csproj --filter "FullyQualifiedName~JsonStringAssertionTests" --no-build
-```
-
-Expected: FAIL - methods not found
-
-**Step 3: Write the string JSON assertions**
-
-Create `TUnit.Assertions/Conditions/Json/JsonStringAssertionExtensions.cs`:
-
-```csharp
-using System.Text.Json;
-using TUnit.Assertions.Attributes;
-using TUnit.Assertions.Core;
-
-namespace TUnit.Assertions.Conditions.Json;
-
-///
-/// Source-generated assertions for validating JSON strings.
-///
-file static partial class JsonStringAssertionExtensions
-{
- [GenerateAssertion(ExpectationMessage = "to be valid JSON")]
- public static AssertionResult IsValidJson(this string value)
- {
- try
- {
- using var doc = JsonDocument.Parse(value);
- return AssertionResult.Passed;
- }
- catch (JsonException ex)
- {
- return AssertionResult.Failed($"is not valid JSON: {ex.Message}");
- }
- }
-
- [GenerateAssertion(ExpectationMessage = "to not be valid JSON")]
- public static AssertionResult IsNotValidJson(this string value)
- {
- try
- {
- using var doc = JsonDocument.Parse(value);
- return AssertionResult.Failed("is valid JSON");
- }
- catch (JsonException)
- {
- return AssertionResult.Passed;
- }
- }
-
- [GenerateAssertion(ExpectationMessage = "to be a valid JSON object")]
- public static AssertionResult IsValidJsonObject(this string value)
- {
- try
- {
- using var doc = JsonDocument.Parse(value);
- if (doc.RootElement.ValueKind == JsonValueKind.Object)
- {
- return AssertionResult.Passed;
- }
- return AssertionResult.Failed($"is a {doc.RootElement.ValueKind}, not an Object");
- }
- catch (JsonException ex)
- {
- return AssertionResult.Failed($"is not valid JSON: {ex.Message}");
- }
- }
-
- [GenerateAssertion(ExpectationMessage = "to be a valid JSON array")]
- public static AssertionResult IsValidJsonArray(this string value)
- {
- try
- {
- using var doc = JsonDocument.Parse(value);
- if (doc.RootElement.ValueKind == JsonValueKind.Array)
- {
- return AssertionResult.Passed;
- }
- return AssertionResult.Failed($"is a {doc.RootElement.ValueKind}, not an Array");
- }
- catch (JsonException ex)
- {
- return AssertionResult.Failed($"is not valid JSON: {ex.Message}");
- }
- }
-}
-```
-
-**Step 4: Run tests to verify they pass**
-
-```bash
-cd C:/git/TUnit && dotnet test TUnit.Assertions.Tests/TUnit.Assertions.Tests.csproj --filter "FullyQualifiedName~JsonStringAssertionTests"
-```
-
-Expected: All tests pass
-
-**Step 5: Commit**
-
-```bash
-git add TUnit.Assertions/Conditions/Json/JsonStringAssertionExtensions.cs TUnit.Assertions.Tests/JsonStringAssertionTests.cs
-git commit -m "feat(assertions): add JSON string validation assertions"
-```
-
----
-
-## Task 8: Run Full Test Suite and Update Snapshots
-
-**Files:**
-- May modify: `TUnit.PublicAPI/**/*.verified.txt`
-- May modify: `TUnit.Core.SourceGenerator.Tests/**/*.verified.txt`
-
-**Step 1: Run full assertion tests**
-
-```bash
-cd C:/git/TUnit && dotnet test TUnit.Assertions.Tests/TUnit.Assertions.Tests.csproj
-```
-
-Expected: All tests pass
-
-**Step 2: Run public API tests**
-
-```bash
-cd C:/git/TUnit && dotnet test TUnit.PublicAPI/TUnit.PublicAPI.csproj
-```
-
-Expected: May fail if new public APIs are detected
-
-**Step 3: Accept snapshots if needed**
-
-```bash
-cd C:/git/TUnit/TUnit.PublicAPI
-for %f in (*.received.txt) do move /Y "%f" "%~nf.verified.txt"
-```
-
-**Step 4: Run source generator tests**
-
-```bash
-cd C:/git/TUnit && dotnet test TUnit.Core.SourceGenerator.Tests/TUnit.Core.SourceGenerator.Tests.csproj
-```
-
-Expected: Should pass (JSON assertions don't affect source generator)
-
-**Step 5: Commit snapshots**
-
-```bash
-git add TUnit.PublicAPI/*.verified.txt
-git commit -m "chore: update public API snapshots for JSON assertions"
-```
-
----
-
-## Task 9: Final Verification
-
-**Step 1: Run complete test suite**
-
-```bash
-cd C:/git/TUnit && dotnet test
-```
-
-Expected: All tests pass
-
-**Step 2: Test AOT compatibility**
-
-```bash
-cd C:/git/TUnit/TUnit.TestProject && dotnet publish -c Release -p:PublishAot=true --use-current-runtime
-```
-
-Expected: Publish succeeds
-
-**Step 3: Final commit if any cleanup needed**
-
-```bash
-git status
-# If clean, no action needed
-# If changes, commit them
-```
-
----
-
-## Summary
-
-This plan creates 4 new files:
-- `TUnit.Assertions/Conditions/Json/JsonDiffHelper.cs` - Path-to-difference logic
-- `TUnit.Assertions/Conditions/Json/JsonElementAssertionExtensions.cs` - JsonElement assertions
-- `TUnit.Assertions/Conditions/Json/JsonNodeAssertionExtensions.cs` - JsonNode/JsonArray assertions
-- `TUnit.Assertions/Conditions/Json/JsonStringAssertionExtensions.cs` - String validation assertions
-
-And 3 new test files:
-- `TUnit.Assertions.Tests/JsonElementAssertionTests.cs`
-- `TUnit.Assertions.Tests/JsonNodeAssertionTests.cs`
-- `TUnit.Assertions.Tests/JsonStringAssertionTests.cs`
-
-Total assertions: ~20 across all types with runtime-specific availability.