Skip to content

Commit a66f8b2

Browse files
v1.2.0 (#15)
- Add new override BeJsonSerializableInto() method to test polymorphism serialization with discriminator JSON property. (fixes #14). - Add the support to assert the deserialization of root JSON array. (fixes #13). - Add the support of the "object" property. (fixes #12).
1 parent 41a89f0 commit a66f8b2

File tree

10 files changed

+1106
-134
lines changed

10 files changed

+1106
-134
lines changed

README.md

Lines changed: 78 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -137,6 +137,84 @@ public void Deserialization()
137137
}
138138
```
139139

140+
## Test polymorphisme serialization with property discriminator
141+
With the .NET 7.0 version of the `System.Text.Json` it is possible to serialize and deserialize JSON object
142+
with property discriminators for polymorphism scenario
143+
(See the [How to serialize properties of derived classes with System.Text.Json](https://learn.microsoft.com/en-us/dotnet/standard/serialization/system-text-json/polymorphism?pivots=dotnet-7-0))
144+
topic for more information.
145+
> It is possible also to use the polymorphism JSON serialization with previous version of .NET using a custom `JsonConverter`.
146+
See the [How to serialize properties of derived classes with System.Text.Json (.NET 6.0)](https://learn.microsoft.com/en-us/dotnet/standard/serialization/system-text-json/polymorphism?pivots=dotnet-6-0)
147+
for more information.
148+
149+
Imagine you have the following classes hierarchy:
150+
151+
```csharp
152+
[JsonPolymorphic(TypeDiscriminatorPropertyName = "type")]
153+
[JsonDerivedType(typeof(ThreeDimensionalPoint), typeDiscriminator: "3d")]
154+
private class BasePoint
155+
{
156+
[JsonPropertyOrder(1)]
157+
public int X { get; set; }
158+
159+
[JsonPropertyOrder(2)]
160+
public int Y { get; set; }
161+
}
162+
163+
private class ThreeDimensionalPoint : BasePoint
164+
{
165+
[JsonPropertyOrder(3)]
166+
public int Z { get; set; }
167+
}
168+
```
169+
170+
And you would like to assert that the serialization of `ThreeDimensionalPoint` instance generate the following JSON content
171+
when calling the `JsonSerializer.Serialize<T>()` method with `BasePoint` as generic argument:
172+
173+
```csharp
174+
var point = new ThreeDimensionalPoint()
175+
{
176+
X = 1,
177+
Y = 2,
178+
Z = 3,
179+
}
180+
181+
var json = JsonSerializer.Serialize<BasePoint>();
182+
```
183+
184+
```json
185+
{
186+
"type": "3d",
187+
"X": 1,
188+
"Y": 2,
189+
"Z": 3,
190+
}
191+
```
192+
193+
This is the assertion to write using the [PosInformatique.FluentAssertions.Json library](https://www.nuget.org/packages/PosInformatique.FluentAssertions.Json/):
194+
195+
196+
```csharp
197+
point.Should().BeJsonSerializableInto<BasePoint>(new
198+
{
199+
myType = "3d",
200+
X = 1,
201+
Y = 2,
202+
Z = 3,
203+
});
204+
```
205+
206+
> **NOTE:** If you don't specify the `BasePoint` generic argument, the library will use the default behavior of the `JsonSerializer.Serialize()`
207+
(with no generic argument), which will generate (and assert!) the following JSON object:
208+
>```json
209+
>{
210+
> "X": 1,
211+
> "Y": 2,
212+
> "Z": 3,
213+
>}
214+
>```
215+
> As you can see there is no discriminator property generate because the .NET `JsonSerializer.Serialize()` will use the type of the instance
216+
> instead of an explicit type of derived class.
217+
140218
## Change the JsonSerializer options
141219
142220
### Change globally the JsonSerializer options

build/azure-pipelines-release.yaml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@ parameters:
22
- name: VersionPrefix
33
displayName: The version of the library
44
type: string
5-
default: 1.1.0
5+
default: 1.2.0
66
- name: VersionSuffix
77
displayName: The version suffix of the library (rc.1). Use a space ' ' if no suffix.
88
type: string

src/FluentAssertions.Json/FluentAssertions.Json.csproj

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,9 +11,13 @@
1111
<PackageProjectUrl>https://github.com/PosInformatique/PosInformatique.FluentAssertions.Json</PackageProjectUrl>
1212
<PackageReadmeFile>README.md</PackageReadmeFile>
1313
<PackageReleaseNotes>
14+
1.2.0
15+
- Add new override BeJsonSerializableInto() method to test polymorphism serialization with discriminator JSON property.
16+
- Add the support to assert the deserialization of root JSON array.
17+
1418
1.1.0
1519
- Allows to configure the JsonSerializationOptions globally and in specific assertions calls.
16-
20+
1721
1.0.5
1822
- Use the FluentAssertions library version 6.0.0 instead of 5.0.0 to fix breaking changes of the API.
1923

Lines changed: 195 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,195 @@
1+
//-----------------------------------------------------------------------
2+
// <copyright file="JsonElementComparer.cs" company="P.O.S Informatique">
3+
// Copyright (c) P.O.S Informatique. All rights reserved.
4+
// </copyright>
5+
//-----------------------------------------------------------------------
6+
7+
namespace PosInformatique.FluentAssertions.Json
8+
{
9+
using System.Collections;
10+
using System.Reflection;
11+
using System.Text.Json;
12+
13+
internal static class JsonElementComparer
14+
{
15+
public static IReadOnlyList<string> Compare(JsonDocument document, object? expected)
16+
{
17+
return Compare(document.RootElement, expected, "$");
18+
}
19+
20+
public static IReadOnlyList<string> Compare(JsonElement element, object? expected, string initialPath)
21+
{
22+
var errors = new List<string>();
23+
24+
var path = new Stack<string>();
25+
26+
path.Push(initialPath);
27+
28+
Compare(element, expected, path, errors);
29+
30+
path.Pop();
31+
32+
return errors;
33+
}
34+
35+
private static void Compare(JsonElement element, object? expected, Stack<string> path, List<string> errors)
36+
{
37+
if (element.ValueKind == JsonValueKind.String)
38+
{
39+
var value = element.GetString();
40+
41+
if (expected is not string expectedStringValue)
42+
{
43+
errors.Add($"{GetPath(path)}: Expected property to be '{ReflectionHelper.GetJsonKind(expected)}' type instead of '{element.ValueKind}' type.");
44+
return;
45+
}
46+
47+
if (value != expectedStringValue)
48+
{
49+
errors.Add($"{GetPath(path)}: Expected '{expected}' instead of '{value}'.");
50+
return;
51+
}
52+
}
53+
else if (element.ValueKind == JsonValueKind.Number)
54+
{
55+
var value = element.GetDouble();
56+
57+
if (expected == null || !ReflectionHelper.IsNumeric(expected))
58+
{
59+
errors.Add($"{GetPath(path)}: Expected property to be '{ReflectionHelper.GetJsonKind(expected)}' type instead of '{element.ValueKind}' type.");
60+
return;
61+
}
62+
63+
var expectedDoubleValue = Convert.ToDouble(expected);
64+
65+
if (value != expectedDoubleValue)
66+
{
67+
errors.Add($"{GetPath(path)}: Expected '{expected}' instead of '{value}'.");
68+
return;
69+
}
70+
}
71+
else if (element.ValueKind == JsonValueKind.Object)
72+
{
73+
if (expected is null || expected is string || ReflectionHelper.IsNumeric(expected) || expected is IEnumerable || expected is bool)
74+
{
75+
errors.Add($"{GetPath(path)}: Expected property to be '{ReflectionHelper.GetJsonKind(expected)}' type instead of '{element.ValueKind}' type.");
76+
return;
77+
}
78+
79+
var expectedPropertyEnumerator = expected.GetType().GetProperties().Cast<PropertyInfo>().GetEnumerator();
80+
81+
foreach (var property in element.EnumerateObject())
82+
{
83+
if (!expectedPropertyEnumerator.MoveNext())
84+
{
85+
errors.Add($"{GetPath(path)}: Expected no property but found '{property.Name}' property.");
86+
continue;
87+
}
88+
89+
var expectedProperty = expectedPropertyEnumerator.Current;
90+
91+
if (property.Name != expectedProperty.Name)
92+
{
93+
errors.Add($"{GetPath(path)}: Expected property with the '{expectedProperty.Name}' name but found '{property.Name}' instead.");
94+
continue;
95+
}
96+
97+
path.Push("." + property.Name);
98+
99+
var expectedValueOfProperty = expectedProperty.GetValue(expected);
100+
101+
Compare(property.Value, expectedValueOfProperty, path, errors);
102+
103+
path.Pop();
104+
}
105+
106+
if (expectedPropertyEnumerator.MoveNext())
107+
{
108+
errors.Add($"{GetPath(path)}: Expected '{expectedPropertyEnumerator.Current.Name}' property but found no property.");
109+
return;
110+
}
111+
}
112+
else if (element.ValueKind == JsonValueKind.Array)
113+
{
114+
if (expected is string || expected is not IEnumerable expectedEnumerableValue)
115+
{
116+
errors.Add($"{GetPath(path)}: Expected property to be '{ReflectionHelper.GetJsonKind(expected)}' type instead of '{element.ValueKind}' type.");
117+
return;
118+
}
119+
120+
var expectedArrayEnumerator = expectedEnumerableValue.GetEnumerator();
121+
var index = 0;
122+
123+
foreach (var item in element.EnumerateArray())
124+
{
125+
if (!expectedArrayEnumerator.MoveNext())
126+
{
127+
var actualCount = element.EnumerateArray().Count();
128+
129+
errors.Add($"{GetPath(path)}: Expected {index} item(s) but found {actualCount}.");
130+
continue;
131+
}
132+
133+
var expectedItem = expectedArrayEnumerator.Current;
134+
135+
path.Push($"[{index}]");
136+
137+
Compare(item, expectedItem, path, errors);
138+
139+
path.Pop();
140+
141+
index++;
142+
}
143+
144+
if (expectedArrayEnumerator.MoveNext())
145+
{
146+
var expectedCount = ((IEnumerable)expected).Cast<object>().Count();
147+
148+
errors.Add($"{GetPath(path)}: Expected {expectedCount} item(s) but found {index}.");
149+
return;
150+
}
151+
}
152+
else if (element.ValueKind == JsonValueKind.True)
153+
{
154+
if (expected is not bool expectedBooleanValue)
155+
{
156+
errors.Add($"{GetPath(path)}: Expected property to be '{ReflectionHelper.GetJsonKind(expected)}' type instead of '{element.ValueKind}' type.");
157+
return;
158+
}
159+
160+
if (expectedBooleanValue != true)
161+
{
162+
errors.Add($"{GetPath(path)}: Expected '{expected}' instead of '{true}'.");
163+
return;
164+
}
165+
}
166+
else if (element.ValueKind == JsonValueKind.False)
167+
{
168+
if (expected is not bool expectedBooleanValue)
169+
{
170+
errors.Add($"{GetPath(path)}: Expected property to be '{ReflectionHelper.GetJsonKind(expected)}' type instead of '{element.ValueKind}' type.");
171+
return;
172+
}
173+
174+
if (expectedBooleanValue != false)
175+
{
176+
errors.Add($"{GetPath(path)}: Expected '{expected}' instead of '{false}'.");
177+
return;
178+
}
179+
}
180+
else
181+
{
182+
if (expected is not null)
183+
{
184+
errors.Add($"{GetPath(path)}: Expected property to be '{ReflectionHelper.GetJsonKind(expected)}' type instead of '{element.ValueKind}' type.");
185+
return;
186+
}
187+
}
188+
}
189+
190+
private static string GetPath(IEnumerable<string> path)
191+
{
192+
return string.Concat(path.Reverse());
193+
}
194+
}
195+
}

0 commit comments

Comments
 (0)