Skip to content

Commit a0b1b28

Browse files
committed
Enhance ArrayReflectionConvertor to create properly typed List<T> during deserialization
1 parent 7303cff commit a0b1b28

2 files changed

Lines changed: 256 additions & 3 deletions

File tree

Lines changed: 241 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,241 @@
1+
using System;
2+
using System.Collections.Generic;
3+
using Xunit;
4+
using Xunit.Abstractions;
5+
using com.IvanMurzak.ReflectorNet.Utils;
6+
using com.IvanMurzak.ReflectorNet.Model;
7+
8+
namespace com.IvanMurzak.ReflectorNet.Tests.Utils
9+
{
10+
/// <summary>
11+
/// Tests to verify that deserialized lists maintain proper generic type information
12+
/// and are not incorrectly deserialized as List&lt;object?&gt;
13+
/// </summary>
14+
public class TypedListDeserializationTests : BaseTest
15+
{
16+
public TypedListDeserializationTests(ITestOutputHelper output) : base(output) { }
17+
18+
public class ClassWithListProperty
19+
{
20+
public List<int>? Numbers { get; set; }
21+
public List<string>? Names { get; set; }
22+
}
23+
24+
[Fact]
25+
public void List_Int_ShouldDeserializeAsTypedList()
26+
{
27+
// Arrange
28+
var original = new List<int> { 1, 2, 3, 4, 5 };
29+
var reflector = new Reflector();
30+
31+
// Act
32+
var serialized = reflector.Serialize(original);
33+
var deserialized = reflector.Deserialize(serialized);
34+
35+
// Assert
36+
Assert.NotNull(deserialized);
37+
38+
// Critical: The deserialized object should be List<int>, not List<object?>
39+
Assert.IsType<List<int>>(deserialized);
40+
41+
var typedList = (List<int>)deserialized;
42+
Assert.Equal(original.Count, typedList.Count);
43+
Assert.Equal(original[0], typedList[0]);
44+
Assert.Equal(original[4], typedList[4]);
45+
}
46+
47+
[Fact]
48+
public void List_String_ShouldDeserializeAsTypedList()
49+
{
50+
// Arrange
51+
var original = new List<string> { "hello", "world", "test" };
52+
var reflector = new Reflector();
53+
54+
// Act
55+
var serialized = reflector.Serialize(original);
56+
var deserialized = reflector.Deserialize(serialized);
57+
58+
// Assert
59+
Assert.NotNull(deserialized);
60+
61+
// Critical: The deserialized object should be List<string>, not List<object?>
62+
Assert.IsType<List<string>>(deserialized);
63+
64+
var typedList = (List<string>)deserialized;
65+
Assert.Equal(original.Count, typedList.Count);
66+
Assert.Equal(original[0], typedList[0]);
67+
Assert.Equal(original[2], typedList[2]);
68+
}
69+
70+
[Fact]
71+
public void List_Double_ShouldDeserializeAsTypedList()
72+
{
73+
// Arrange
74+
var original = new List<double> { 1.5, 2.7, 3.14159 };
75+
var reflector = new Reflector();
76+
77+
// Act
78+
var serialized = reflector.Serialize(original);
79+
var deserialized = reflector.Deserialize(serialized);
80+
81+
// Assert
82+
Assert.NotNull(deserialized);
83+
84+
// Critical: The deserialized object should be List<double>, not List<object?>
85+
Assert.IsType<List<double>>(deserialized);
86+
87+
var typedList = (List<double>)deserialized;
88+
Assert.Equal(original.Count, typedList.Count);
89+
Assert.Equal(original[0], typedList[0]);
90+
Assert.Equal(original[2], typedList[2]);
91+
}
92+
93+
public class TestPerson
94+
{
95+
public string? Name { get; set; }
96+
public int Age { get; set; }
97+
}
98+
99+
[Fact]
100+
public void List_ComplexType_ShouldDeserializeAsTypedList()
101+
{
102+
// Arrange
103+
var original = new List<TestPerson>
104+
{
105+
new TestPerson { Name = "Alice", Age = 30 },
106+
new TestPerson { Name = "Bob", Age = 25 }
107+
};
108+
var reflector = new Reflector();
109+
110+
// Act
111+
var serialized = reflector.Serialize(original);
112+
var deserialized = reflector.Deserialize(serialized);
113+
114+
// Assert
115+
Assert.NotNull(deserialized);
116+
117+
// Critical: The deserialized object should be List<TestPerson>, not List<object?>
118+
Assert.IsType<List<TestPerson>>(deserialized);
119+
120+
var typedList = (List<TestPerson>)deserialized;
121+
Assert.Equal(original.Count, typedList.Count);
122+
Assert.Equal(original[0].Name, typedList[0].Name);
123+
Assert.Equal(original[1].Age, typedList[1].Age);
124+
}
125+
126+
[Fact]
127+
public void List_GenericType_ShouldMaintainTypeInformation()
128+
{
129+
// Arrange
130+
var original = new List<int> { 10, 20, 30 };
131+
var reflector = new Reflector();
132+
133+
// Act
134+
var serialized = reflector.Serialize(original);
135+
var deserialized = reflector.Deserialize(serialized);
136+
137+
// Assert
138+
Assert.NotNull(deserialized);
139+
140+
// Verify the runtime type has the correct generic argument
141+
var deserializedType = deserialized.GetType();
142+
Assert.True(deserializedType.IsGenericType);
143+
Assert.Equal(typeof(List<>), deserializedType.GetGenericTypeDefinition());
144+
145+
var genericArgs = deserializedType.GetGenericArguments();
146+
Assert.Single(genericArgs);
147+
Assert.Equal(typeof(int), genericArgs[0]);
148+
}
149+
150+
[Fact]
151+
public void NestedList_ShouldDeserializeAsTypedList()
152+
{
153+
// Arrange
154+
var original = new List<List<int>>
155+
{
156+
new List<int> { 1, 2, 3 },
157+
new List<int> { 4, 5, 6 }
158+
};
159+
var reflector = new Reflector();
160+
161+
// Act
162+
var serialized = reflector.Serialize(original);
163+
var deserialized = reflector.Deserialize(serialized);
164+
165+
// Assert
166+
Assert.NotNull(deserialized);
167+
168+
// Critical: The deserialized object should be List<List<int>>, not List<object?>
169+
Assert.IsType<List<List<int>>>(deserialized);
170+
171+
var typedList = (List<List<int>>)deserialized;
172+
Assert.Equal(original.Count, typedList.Count);
173+
Assert.Equal(original[0][0], typedList[0][0]);
174+
Assert.Equal(original[1][2], typedList[1][2]);
175+
}
176+
177+
[Fact]
178+
public void ClassWithListProperty_ShouldDeserializeListPropertyAsTypedList()
179+
{
180+
// Arrange
181+
var original = new ClassWithListProperty
182+
{
183+
Numbers = new List<int> { 10, 20, 30 },
184+
Names = new List<string> { "Alice", "Bob", "Charlie" }
185+
};
186+
var reflector = new Reflector();
187+
188+
// Act
189+
var serialized = reflector.Serialize(original);
190+
_output.WriteLine($"Serialized: {serialized.ToJson(reflector)}");
191+
192+
var deserialized = reflector.Deserialize<ClassWithListProperty>(serialized);
193+
194+
// Assert
195+
Assert.NotNull(deserialized);
196+
Assert.NotNull(deserialized.Numbers);
197+
Assert.NotNull(deserialized.Names);
198+
199+
// Critical: The deserialized properties should be List<int> and List<string>, not List<object?>
200+
Assert.IsType<List<int>>(deserialized.Numbers);
201+
Assert.IsType<List<string>>(deserialized.Names);
202+
203+
Assert.Equal(original.Numbers.Count, deserialized.Numbers.Count);
204+
Assert.Equal(original.Names.Count, deserialized.Names.Count);
205+
Assert.Equal(original.Numbers[0], deserialized.Numbers[0]);
206+
Assert.Equal(original.Names[0], deserialized.Names[0]);
207+
}
208+
209+
[Fact]
210+
public void DirectListDeserializationViaSerializedMember_ShouldBeTyped()
211+
{
212+
// This test directly exercises TryDeserializeValueListInternal
213+
// by creating a SerializedMember with a JSON array
214+
215+
var reflector = new Reflector();
216+
217+
// Create a serialized member representing a List<int> with JSON array
218+
var jsonArray = System.Text.Json.JsonSerializer.SerializeToElement(new[] { 1, 2, 3, 4, 5 });
219+
var serializedMember = new SerializedMember
220+
{
221+
typeName = "System.Collections.Generic.List`1[[System.Int32]]",
222+
valueJsonElement = jsonArray
223+
};
224+
225+
// Act
226+
var deserialized = reflector.Deserialize(serializedMember);
227+
228+
// Assert
229+
Assert.NotNull(deserialized);
230+
_output.WriteLine($"Deserialized type: {deserialized.GetType().FullName}");
231+
232+
// Critical: Should be List<int>, not List<object?>
233+
Assert.IsType<List<int>>(deserialized);
234+
235+
var typedList = (List<int>)deserialized;
236+
Assert.Equal(5, typedList.Count);
237+
Assert.Equal(1, typedList[0]);
238+
Assert.Equal(5, typedList[4]);
239+
}
240+
}
241+
}

ReflectorNet/src/Convertor/Reflection/ArrayReflectionConvertor.Deserialize.cs

Lines changed: 15 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -319,7 +319,20 @@ protected virtual bool TryDeserializeValueListInternal(
319319
return false;
320320
}
321321

322-
var list = new List<object?>();
322+
// Create a properly typed List<T> instead of List<object?>
323+
var listType = typeof(List<>).MakeGenericType(itemType);
324+
var list = (IList?)Activator.CreateInstance(listType);
325+
if (list == null)
326+
{
327+
result = null;
328+
if (logger?.IsEnabled(LogLevel.Error) == true)
329+
logger.LogError($"{padding}Failed to create list instance for type '{type.GetTypeShortName()}'.");
330+
331+
if (stringBuilder != null)
332+
stringBuilder.AppendLine($"{padding}[Error] Failed to create list instance for type '{type.GetTypeShortName()}'.");
333+
return false;
334+
}
335+
323336
int i = 0;
324337
foreach (var element in jsonArray.EnumerateArray())
325338
{
@@ -359,8 +372,7 @@ protected virtual bool TryDeserializeValueListInternal(
359372
}
360373
else
361374
{
362-
// Note: This creates a List<object?>. If the target is List<T>, this might need adjustment
363-
// but TryDeserializeValueListInternal returns IEnumerable, so it's usually iterated or cast later.
375+
// Return the properly typed List<T>
364376
result = list;
365377

366378
if (logger?.IsEnabled(LogLevel.Trace) == true)

0 commit comments

Comments
 (0)