From 50c002dd1d614a7702ee3b6346d8f044d58ca4bb Mon Sep 17 00:00:00 2001 From: "sam.segers" Date: Mon, 26 Jan 2026 12:08:27 +0100 Subject: [PATCH 1/4] reuse JToken.Parse error handling --- Src/FluentAssertions.Json/JTokenAssertions.cs | 58 ++++++------------- .../JTokenAssertionsSpecs.cs | 2 +- 2 files changed, 20 insertions(+), 40 deletions(-) diff --git a/Src/FluentAssertions.Json/JTokenAssertions.cs b/Src/FluentAssertions.Json/JTokenAssertions.cs index f737e25..a3f8eb1 100644 --- a/Src/FluentAssertions.Json/JTokenAssertions.cs +++ b/Src/FluentAssertions.Json/JTokenAssertions.cs @@ -54,19 +54,7 @@ public JTokenAssertions(JToken subject, AssertionChain assertionChain) public AndConstraint BeEquivalentTo(string expected, string because = "", params object[] becauseArgs) { - JToken parsedExpected; - try - { - parsedExpected = JToken.Parse(expected); - } - catch (Exception ex) - { - throw new ArgumentException( - $"Unable to parse expected JSON string:{Environment.NewLine}" + - $"{expected}{Environment.NewLine}" + - "Check inner exception for more details.", - nameof(expected), ex); - } + JToken parsedExpected = Parse(expected, nameof(expected)); return BeEquivalentTo(parsedExpected, because, becauseArgs); } @@ -150,19 +138,7 @@ private AndConstraint BeEquivalentTo(JToken expected, bool ign public AndConstraint NotBeEquivalentTo(string unexpected, string because = "", params object[] becauseArgs) { - JToken parsedUnexpected; - try - { - parsedUnexpected = JToken.Parse(unexpected); - } - catch (Exception ex) - { - throw new ArgumentException( - $"Unable to parse unexpected JSON string:{Environment.NewLine}" + - $"{unexpected}{Environment.NewLine}" + - "Check inner exception for more details.", - nameof(unexpected), ex); - } + JToken parsedUnexpected = Parse(unexpected, nameof(unexpected)); return NotBeEquivalentTo(parsedUnexpected, because, becauseArgs); } @@ -448,19 +424,7 @@ public AndConstraint HaveCount(int expected, string because = /// public AndConstraint ContainSubtree(string subtree, string because = "", params object[] becauseArgs) { - JToken subtreeToken; - try - { - subtreeToken = JToken.Parse(subtree); - } - catch (Exception ex) - { - throw new ArgumentException( - $"Unable to parse expected JSON string:{Environment.NewLine}" + - $"{subtree}{Environment.NewLine}" + - "Check inner exception for more details.", - nameof(subtree), ex); - } + JToken subtreeToken = Parse(subtree, nameof(subtree)); return ContainSubtree(subtreeToken, because, becauseArgs); } @@ -496,6 +460,22 @@ public AndConstraint ContainSubtree(JToken subtree, string bec return BeEquivalentTo(subtree, true, options => options, because, becauseArgs); } + private static JToken Parse(string json, string paramName) + { + try + { + return JToken.Parse(json); + } + catch (Exception ex) + { + throw new ArgumentException( + $"Unable to parse {paramName} JSON string:{Environment.NewLine}" + + $"{json}{Environment.NewLine}" + + "Check inner exception for more details.", + paramName, ex); + } + } + #pragma warning disable CA1822 // Making this method static is a breaking chan public string Format(JToken value, bool useLineBreaks = false) { diff --git a/Tests/FluentAssertions.Json.Specs/JTokenAssertionsSpecs.cs b/Tests/FluentAssertions.Json.Specs/JTokenAssertionsSpecs.cs index 21e0682..6f8d19a 100644 --- a/Tests/FluentAssertions.Json.Specs/JTokenAssertionsSpecs.cs +++ b/Tests/FluentAssertions.Json.Specs/JTokenAssertionsSpecs.cs @@ -943,7 +943,7 @@ public void When_checking_subtree_with_an_invalid_expected_string_it_should_prov // Act & Assert actualJson.Should().Invoking(x => x.ContainSubtree(invalidSubtree)) .Should().Throw() - .WithMessage($"Unable to parse expected JSON string:{invalidSubtree}*") + .WithMessage($"Unable to parse subtree JSON string:{invalidSubtree}*") .WithInnerException(); } From 45e9db15e132ef987f9c6a8fd36c3edf894e734d Mon Sep 17 00:00:00 2001 From: "sam.segers" Date: Mon, 26 Jan 2026 12:09:04 +0100 Subject: [PATCH 2/4] fix example documentation in JTokenAssertions --- Src/FluentAssertions.Json/JTokenAssertions.cs | 26 +++++++++++-------- 1 file changed, 15 insertions(+), 11 deletions(-) diff --git a/Src/FluentAssertions.Json/JTokenAssertions.cs b/Src/FluentAssertions.Json/JTokenAssertions.cs index a3f8eb1..e595b78 100644 --- a/Src/FluentAssertions.Json/JTokenAssertions.cs +++ b/Src/FluentAssertions.Json/JTokenAssertions.cs @@ -361,7 +361,7 @@ public AndWhichConstraint NotHaveElement(string unexpe /// is needed. If the phrase does not start with the word because, it is prepended automatically. /// /// - /// Zero or more objects to format using the placeholders in . + /// Zero or more objects to format using the placeholders in . /// public AndWhichConstraint ContainSingleItem(string because = "", params object[] becauseArgs) { @@ -383,7 +383,7 @@ public AndWhichConstraint ContainSingleItem(string bec /// is needed. If the phrase does not start with the word because, it is prepended automatically. /// /// - /// Zero or more objects to format using the placeholders in . + /// Zero or more objects to format using the placeholders in . /// public AndConstraint HaveCount(int expected, string because = "", params object[] becauseArgs) { @@ -405,7 +405,7 @@ public AndConstraint HaveCount(int expected, string because = /// is needed. If the phrase does not start with the word because, it is prepended automatically. /// /// - /// Zero or more objects to format using the placeholders in . + /// Zero or more objects to format using the placeholders in . /// /// Use this method to match the current against an arbitrary subtree, /// permitting it to contain any additional properties or elements. This way we can test multiple properties on a at once, @@ -413,15 +413,17 @@ public AndConstraint HaveCount(int expected, string because = /// /// This example asserts the values of multiple properties of a child object within a JSON document. /// - /// var json = JToken.Parse("{ success: true, data: { id: 123, type: 'my-type', name: 'Noone' } }"); - /// json.Should().ContainSubtree(JToken.Parse("{ success: true, data: { type: 'my-type', name: 'Noone' } }")); + /// var json = JToken.Parse("{ success: true, data: { id: 123, type: 'my-type', name: 'John' } }"); + /// json.Should().ContainSubtree("{ success: true, data: { type: 'my-type', name: 'John' } }"); /// /// - /// This example asserts that a within a has at least one element with at least the given properties + /// + /// This example asserts that a within a has at least one element with at least the given properties /// /// var json = JToken.Parse("{ id: 1, items: [ { id: 2, type: 'my-type', name: 'Alpha' }, { id: 3, type: 'other-type', name: 'Bravo' } ] }"); - /// json.Should().ContainSubtree(JToken.Parse("{ items: [ { type: 'my-type', name: 'Alpha' } ] }")); + /// json.Should().ContainSubtree("{ items: [ { type: 'my-type', name: 'Alpha' } ] }"); /// + /// public AndConstraint ContainSubtree(string subtree, string because = "", params object[] becauseArgs) { JToken subtreeToken = Parse(subtree, nameof(subtree)); @@ -438,7 +440,7 @@ public AndConstraint ContainSubtree(string subtree, string bec /// is needed. If the phrase does not start with the word because, it is prepended automatically. /// /// - /// Zero or more objects to format using the placeholders in . + /// Zero or more objects to format using the placeholders in . /// /// Use this method to match the current against an arbitrary subtree, /// permitting it to contain any additional properties or elements. This way we can test multiple properties on a at once, @@ -446,15 +448,17 @@ public AndConstraint ContainSubtree(string subtree, string bec /// /// This example asserts the values of multiple properties of a child object within a JSON document. /// - /// var json = JToken.Parse("{ success: true, data: { id: 123, type: 'my-type', name: 'Noone' } }"); - /// json.Should().ContainSubtree(JToken.Parse("{ success: true, data: { type: 'my-type', name: 'Noone' } }")); + /// var json = JToken.Parse("{ success: true, data: { id: 123, type: 'my-type', name: 'John' } }"); + /// json.Should().ContainSubtree(JToken.Parse("{ success: true, data: { type: 'my-type', name: 'John' } }")); /// /// - /// This example asserts that a within a has at least one element with at least the given properties + /// + /// This example asserts that a within a has at least one element with at least the given properties /// /// var json = JToken.Parse("{ id: 1, items: [ { id: 2, type: 'my-type', name: 'Alpha' }, { id: 3, type: 'other-type', name: 'Bravo' } ] }"); /// json.Should().ContainSubtree(JToken.Parse("{ items: [ { type: 'my-type', name: 'Alpha' } ] }")); /// + /// public AndConstraint ContainSubtree(JToken subtree, string because = "", params object[] becauseArgs) { return BeEquivalentTo(subtree, true, options => options, because, becauseArgs); From 957ff7464f88b3a782f5f6a0912a99bdef4fe683 Mon Sep 17 00:00:00 2001 From: "sam.segers" Date: Mon, 26 Jan 2026 11:50:37 +0100 Subject: [PATCH 3/4] add ContainSubtree that takes a config --- Src/FluentAssertions.Json/JTokenAssertions.cs | 84 +++++++++++++++++++ .../JTokenAssertionsSpecs.cs | 29 +++++++ 2 files changed, 113 insertions(+) diff --git a/Src/FluentAssertions.Json/JTokenAssertions.cs b/Src/FluentAssertions.Json/JTokenAssertions.cs index e595b78..d45edcd 100644 --- a/Src/FluentAssertions.Json/JTokenAssertions.cs +++ b/Src/FluentAssertions.Json/JTokenAssertions.cs @@ -431,6 +431,49 @@ public AndConstraint ContainSubtree(string subtree, string bec return ContainSubtree(subtreeToken, because, becauseArgs); } + /// + /// Recursively asserts that the current contains at least the properties or elements of the specified . + /// + /// The subtree to search for + /// The options to consider while asserting values + /// + /// A formatted phrase as is supported by explaining why the assertion + /// is needed. If the phrase does not start with the word because, it is prepended automatically. + /// + /// + /// Zero or more objects to format using the placeholders in . + /// + /// Use this method to match the current against an arbitrary subtree, + /// permitting it to contain any additional properties or elements. This way we can test multiple properties on a at once, + /// or test if a contains any items that match a set of properties, assert that a JSON document has a given shape, etc. + /// + /// This example asserts the values of multiple properties of a child object within a JSON document using a specified double precision. + /// + /// var json = JToken.Parse("{ success: true, data: { id: 123, type: 'my-type', value: 0.99 } }"); + /// json.Should().ContainSubtree(JToken.Parse("{ success: true, data: { type: 'my-type', value: 1.0 } }"), options => options + /// .Using<double>(d => d.Subject.Should().BeApproximately(d.Expectation, 1e-1)) + /// .WhenTypeIs<double>()); + /// + /// + /// + /// This example asserts that a within a has at least one element with at least the given properties, using a specified double precision. + /// + /// var json = JToken.Parse("{ id: 1, items: [ { id: 2, type: 'my-type', value: 0.99 }, { id: 3, type: 'other-type', value: 3 } ] }"); + /// json.Should().ContainSubtree("{ items: [ { type: 'my-type', value: 1 } ] }", options => options + /// .Using<double>(d => d.Subject.Should().BeApproximately(d.Expectation, 1e-1)) + /// .WhenTypeIs<double>()); + /// + /// + public AndConstraint ContainSubtree(string subtree, + Func, IJsonAssertionOptions> config, + string because = "", + params object[] becauseArgs) + { + JToken subtreeToken = Parse(subtree, nameof(subtree)); + + return BeEquivalentTo(subtreeToken, true, config, because, becauseArgs); + } + /// /// Recursively asserts that the current contains at least the properties or elements of the specified . /// @@ -464,6 +507,47 @@ public AndConstraint ContainSubtree(JToken subtree, string bec return BeEquivalentTo(subtree, true, options => options, because, becauseArgs); } + /// + /// Recursively asserts that the current contains at least the properties or elements of the specified . + /// + /// The subtree to search for + /// The options to consider while asserting values + /// + /// A formatted phrase as is supported by explaining why the assertion + /// is needed. If the phrase does not start with the word because, it is prepended automatically. + /// + /// + /// Zero or more objects to format using the placeholders in . + /// + /// Use this method to match the current against an arbitrary subtree, + /// permitting it to contain any additional properties or elements. This way we can test multiple properties on a at once, + /// or test if a contains any items that match a set of properties, assert that a JSON document has a given shape, etc. + /// + /// This example asserts the values of multiple properties of a child object within a JSON document, using a specified double precision. + /// + /// var json = JToken.Parse("{ success: true, data: { id: 123, type: 'my-type', value: 0.99 } }"); + /// json.Should().ContainSubtree(JToken.Parse("{ success: true, data: { type: 'my-type', value: 1.0 } }"), options => options + /// .Using<double>(d => d.Subject.Should().BeApproximately(d.Expectation, 1e-1)) + /// .WhenTypeIs<double>()); + /// + /// + /// + /// This example asserts that a within a has at least one element with at least the given properties, using a specified double precision. + /// + /// var json = JToken.Parse("{ id: 1, items: [ { id: 2, type: 'my-type', value: 0.99 }, { id: 3, type: 'other-type', value: 3 } ] }"); + /// json.Should().ContainSubtree(JToken.Parse("{ items: [ { type: 'my-type', value: 1 } ] }"), options => options + /// .Using<double>(d => d.Subject.Should().BeApproximately(d.Expectation, 1e-1)) + /// .WhenTypeIs<double>()); + /// + /// + public AndConstraint ContainSubtree(JToken subtree, + Func, IJsonAssertionOptions> config, + string because = "", + params object[] becauseArgs) + { + return BeEquivalentTo(subtree, true, config, because, becauseArgs); + } + private static JToken Parse(string json, string paramName) { try diff --git a/Tests/FluentAssertions.Json.Specs/JTokenAssertionsSpecs.cs b/Tests/FluentAssertions.Json.Specs/JTokenAssertionsSpecs.cs index 6f8d19a..303788f 100644 --- a/Tests/FluentAssertions.Json.Specs/JTokenAssertionsSpecs.cs +++ b/Tests/FluentAssertions.Json.Specs/JTokenAssertionsSpecs.cs @@ -947,6 +947,35 @@ public void When_checking_subtree_with_an_invalid_expected_string_it_should_prov .WithInnerException(); } + [Fact] + public void Assert_property_with_approximation_succeeds() + { + // Arrange + var actual = JToken.Parse("{ \"id\": 1.1232 }"); + var expected = JToken.Parse("{ \"id\": 1.1235 }"); + + // Act & Assert + actual.Should().ContainSubtree(expected, options => options + .Using(d => d.Subject.Should().BeApproximately(d.Expectation, 1e-3)) + .WhenTypeIs()); + } + + [Fact] + public void Can_assert_on_a_field_with_approximation() + { + // Arrange + var actual = JToken.Parse("{ \"id\": 1.1232 }"); + var expected = JToken.Parse("{ \"id\": 1.1235 }"); + + // Act & Assert + actual.Should(). + Invoking(x => x.ContainSubtree(expected, options => options + .Using(d => d.Subject.Should().BeApproximately(d.Expectation, 1e-5)) + .WhenTypeIs())) + .Should().Throw() + .WithMessage("JSON document has a different value at $.id.*"); + } + #endregion private static string Format(JToken value, bool useLineBreaks = false) From 2e69b25512f29d162b88ec99377d7734d3c07464 Mon Sep 17 00:00:00 2001 From: "sam.segers" Date: Mon, 26 Jan 2026 12:32:18 +0100 Subject: [PATCH 4/4] update ApprovedApi --- .../ApprovedApi/FluentAssertions.Json/net47.verified.txt | 2 ++ .../FluentAssertions.Json/netstandard2.0.verified.txt | 2 ++ 2 files changed, 4 insertions(+) diff --git a/Tests/Approval.Tests/ApprovedApi/FluentAssertions.Json/net47.verified.txt b/Tests/Approval.Tests/ApprovedApi/FluentAssertions.Json/net47.verified.txt index 7b659d2..0e2bec3 100644 --- a/Tests/Approval.Tests/ApprovedApi/FluentAssertions.Json/net47.verified.txt +++ b/Tests/Approval.Tests/ApprovedApi/FluentAssertions.Json/net47.verified.txt @@ -21,6 +21,8 @@ namespace FluentAssertions.Json public FluentAssertions.AndWhichConstraint ContainSingleItem(string because = "", params object[] becauseArgs) { } public FluentAssertions.AndConstraint ContainSubtree(Newtonsoft.Json.Linq.JToken subtree, string because = "", params object[] becauseArgs) { } public FluentAssertions.AndConstraint ContainSubtree(string subtree, string because = "", params object[] becauseArgs) { } + public FluentAssertions.AndConstraint ContainSubtree(Newtonsoft.Json.Linq.JToken subtree, System.Func, FluentAssertions.Json.IJsonAssertionOptions> config, string because = "", params object[] becauseArgs) { } + public FluentAssertions.AndConstraint ContainSubtree(string subtree, System.Func, FluentAssertions.Json.IJsonAssertionOptions> config, string because = "", params object[] becauseArgs) { } public string Format(Newtonsoft.Json.Linq.JToken value, bool useLineBreaks = false) { } public FluentAssertions.AndConstraint HaveCount(int expected, string because = "", params object[] becauseArgs) { } public FluentAssertions.AndWhichConstraint HaveElement(string expected) { } diff --git a/Tests/Approval.Tests/ApprovedApi/FluentAssertions.Json/netstandard2.0.verified.txt b/Tests/Approval.Tests/ApprovedApi/FluentAssertions.Json/netstandard2.0.verified.txt index 7b659d2..0e2bec3 100644 --- a/Tests/Approval.Tests/ApprovedApi/FluentAssertions.Json/netstandard2.0.verified.txt +++ b/Tests/Approval.Tests/ApprovedApi/FluentAssertions.Json/netstandard2.0.verified.txt @@ -21,6 +21,8 @@ namespace FluentAssertions.Json public FluentAssertions.AndWhichConstraint ContainSingleItem(string because = "", params object[] becauseArgs) { } public FluentAssertions.AndConstraint ContainSubtree(Newtonsoft.Json.Linq.JToken subtree, string because = "", params object[] becauseArgs) { } public FluentAssertions.AndConstraint ContainSubtree(string subtree, string because = "", params object[] becauseArgs) { } + public FluentAssertions.AndConstraint ContainSubtree(Newtonsoft.Json.Linq.JToken subtree, System.Func, FluentAssertions.Json.IJsonAssertionOptions> config, string because = "", params object[] becauseArgs) { } + public FluentAssertions.AndConstraint ContainSubtree(string subtree, System.Func, FluentAssertions.Json.IJsonAssertionOptions> config, string because = "", params object[] becauseArgs) { } public string Format(Newtonsoft.Json.Linq.JToken value, bool useLineBreaks = false) { } public FluentAssertions.AndConstraint HaveCount(int expected, string because = "", params object[] becauseArgs) { } public FluentAssertions.AndWhichConstraint HaveElement(string expected) { }