Skip to content

Commit 05083ba

Browse files
authored
Merge pull request #38 from IvanMurzak/refactoring/reflection-convertors
Add TypeJsonConverter and refactor deserialization methods for improved type handling
2 parents 6444287 + 6705c05 commit 05083ba

6 files changed

Lines changed: 161 additions & 26 deletions

File tree

Lines changed: 66 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,66 @@
1+
/*
2+
* ReflectorNet
3+
* Author: Ivan Murzak (https://github.com/IvanMurzak)
4+
* Copyright (c) 2025 Ivan Murzak
5+
* Licensed under the Apache License, Version 2.0. See LICENSE file in the project root for full license information.
6+
*/
7+
8+
using System;
9+
using System.Text.Json;
10+
using System.Text.Json.Nodes;
11+
using com.IvanMurzak.ReflectorNet.Utils;
12+
13+
namespace com.IvanMurzak.ReflectorNet.Json
14+
{
15+
/// <summary>
16+
/// JsonConverter that handles conversion between JSON string values and System.Type.
17+
/// Supports nullable Type and uses fully qualified type names for serialization.
18+
/// </summary>
19+
public class TypeJsonConverter : JsonSchemaConverter<Type>, IJsonSchemaConverter
20+
{
21+
public static JsonNode Schema => new JsonObject
22+
{
23+
[JsonSchema.Type] = JsonSchema.String
24+
};
25+
public static JsonNode SchemaRef => new JsonObject
26+
{
27+
[JsonSchema.Ref] = JsonSchema.RefValue + StaticId
28+
};
29+
30+
public override JsonNode GetSchemaRef() => SchemaRef;
31+
public override JsonNode GetSchema() => Schema;
32+
33+
public override bool CanConvert(Type typeToConvert)
34+
{
35+
var underlyingType = Nullable.GetUnderlyingType(typeToConvert) ?? typeToConvert;
36+
return underlyingType == typeof(Type);
37+
}
38+
39+
public override Type? Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options)
40+
{
41+
// Handle null values for nullable types
42+
if (reader.TokenType == JsonTokenType.Null)
43+
return null;
44+
45+
// Handle string tokens
46+
if (reader.TokenType == JsonTokenType.String)
47+
{
48+
var stringValue = reader.GetString();
49+
return TypeUtils.GetType(stringValue);
50+
}
51+
52+
throw new JsonException($"Expected string which represents System.Type in the format `System.Int32`");
53+
}
54+
55+
public override void Write(Utf8JsonWriter writer, Type value, JsonSerializerOptions options)
56+
{
57+
if (value == null)
58+
{
59+
writer.WriteNullValue();
60+
return;
61+
}
62+
63+
writer.WriteStringValue(value.GetTypeName(pretty: false));
64+
}
65+
}
66+
}

ReflectorNet/src/Convertor/Reflection/ArrayReflectionConvertor.cs

Lines changed: 39 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -169,20 +169,29 @@ public override bool SetField(
169169
ILogger? logger = null)
170170
{
171171
var padding = StringUtils.GetPadding(depth);
172-
if (logger?.IsEnabled(LogLevel.Trace) == true)
173-
logger.LogTrace($"{padding}Set field type='{fieldInfo.FieldType.GetTypeShortName()}', name='{fieldInfo.Name}'. Convertor='{GetType().GetTypeShortName()}'.");
172+
// if (logger?.IsEnabled(LogLevel.Trace) == true)
173+
// logger.LogTrace($"{padding}Set field type='{fieldInfo.FieldType.GetTypeShortName()}', name='{fieldInfo.Name}'. Convertor='{GetType().GetTypeShortName()}'.");
174174

175-
if (!TryDeserializeValue(reflector,
176-
serializedMember: value,
177-
type: out var parsedValue,
178-
result: out var type,
179-
fallbackType: fallbackType,
180-
depth: depth,
181-
stringBuilder: stringBuilder,
182-
logger: logger))
175+
if (!TryDeserializeValue(
176+
reflector,
177+
data: value,
178+
result: out var parsedValue,
179+
type: out var type,
180+
fallbackType: fallbackType,
181+
depth: depth,
182+
stringBuilder: stringBuilder,
183+
logger: logger))
184+
{
185+
return false;
186+
}
187+
188+
// Check if field type matches parsed value type
189+
if (!TypeUtils.IsCastable(type, fieldInfo.FieldType))
183190
{
191+
stringBuilder?.AppendLine($"{padding}[Error] Parsed value type '{type.GetTypeName(pretty: false)}' is not assignable to field type '{fieldInfo.FieldType.GetTypeName(pretty: false)}' for field '{fieldInfo.Name}'.");
184192
return false;
185193
}
194+
186195
// TODO: Print previous and new value in stringBuilder
187196
fieldInfo.SetValue(obj, parsedValue);
188197
return true;
@@ -200,20 +209,35 @@ public override bool SetProperty(
200209
ILogger? logger = null)
201210
{
202211
var padding = StringUtils.GetPadding(depth);
203-
if (logger?.IsEnabled(LogLevel.Trace) == true)
204-
logger.LogTrace($"{padding}Set property type='{propertyInfo.PropertyType.GetTypeShortName()}', name='{propertyInfo.Name}'. Convertor='{GetType().GetTypeShortName()}'.");
212+
// if (logger?.IsEnabled(LogLevel.Trace) == true)
213+
// logger.LogTrace($"{padding}Set property type='{propertyInfo.PropertyType.GetTypeShortName()}', name='{propertyInfo.Name}'. Convertor='{GetType().GetTypeShortName()}'.");
214+
215+
// Check if property is writable
216+
if (!propertyInfo.CanWrite)
217+
{
218+
stringBuilder?.AppendLine($"{padding}[Error] Property '{propertyInfo.Name}' is read-only.");
219+
return false;
220+
}
205221

206222
if (!TryDeserializeValue(reflector,
207-
serializedMember: value,
208-
type: out var parsedValue,
209-
result: out var type,
223+
data: value,
224+
result: out var parsedValue,
225+
type: out var type,
210226
fallbackType: fallbackType,
211227
depth: depth,
212228
stringBuilder: stringBuilder,
213229
logger: logger))
214230
{
215231
return false;
216232
}
233+
234+
// Check if property type matches parsed value type
235+
if (!TypeUtils.IsCastable(type, propertyInfo.PropertyType))
236+
{
237+
stringBuilder?.AppendLine($"{padding}[Error] Parsed value type '{type.GetTypeName(pretty: false)}' is not assignable to property type '{propertyInfo.PropertyType.GetTypeName(pretty: false)}' for property '{propertyInfo.Name}'.");
238+
return false;
239+
}
240+
217241
// TODO: Print previous and new value in stringBuilder
218242
propertyInfo.SetValue(obj, parsedValue);
219243
return true;

ReflectorNet/src/Convertor/Reflection/Base/BaseReflectionConvertor.Deserialize.cs

Lines changed: 9 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -60,8 +60,9 @@ public abstract partial class BaseReflectionConvertor<T> : IReflectionConvertor
6060
ILogger? logger = null,
6161
DeserializationContext? context = null)
6262
{
63-
if (!TryDeserializeValue(reflector,
64-
serializedMember: data,
63+
if (!TryDeserializeValue(
64+
reflector,
65+
data: data,
6566
result: out var result,
6667
type: out var type,
6768
fallbackType: fallbackType,
@@ -200,7 +201,7 @@ public abstract partial class BaseReflectionConvertor<T> : IReflectionConvertor
200201
/// - Maintains type safety throughout the deserialization process
201202
/// </summary>
202203
/// <param name="reflector">The Reflector instance used for type resolution and recursive operations.</param>
203-
/// <param name="serializedMember">The SerializedMember containing the data to deserialize.</param>
204+
/// <param name="data">The SerializedMember containing the data to deserialize.</param>
204205
/// <param name="result">Output parameter containing the deserialized object on success.</param>
205206
/// <param name="type">Output parameter containing the resolved target type.</param>
206207
/// <param name="fallbackType">Optional fallback type when type resolution from data fails.</param>
@@ -210,15 +211,15 @@ public abstract partial class BaseReflectionConvertor<T> : IReflectionConvertor
210211
/// <returns>True if deserialization succeeded, false otherwise.</returns>
211212
protected virtual bool TryDeserializeValue(
212213
Reflector reflector,
213-
SerializedMember? serializedMember,
214+
SerializedMember? data,
214215
out object? result,
215216
out Type? type,
216217
Type? fallbackType = null,
217218
int depth = 0,
218219
StringBuilder? stringBuilder = null,
219220
ILogger? logger = null)
220221
{
221-
if (serializedMember == null)
222+
if (data == null)
222223
{
223224
result = null;
224225
type = null;
@@ -228,7 +229,7 @@ protected virtual bool TryDeserializeValue(
228229
var padding = StringUtils.GetPadding(depth);
229230

230231
// Get the most appropriate type for deserialization
231-
type = TypeUtils.GetTypeWithNamePriority(serializedMember, fallbackType, out var error);
232+
type = TypeUtils.GetTypeWithNamePriority(data, fallbackType, out var error);
232233
if (type == null)
233234
{
234235
result = null;
@@ -238,11 +239,11 @@ protected virtual bool TryDeserializeValue(
238239
}
239240

240241
if (logger?.IsEnabled(LogLevel.Trace) == true)
241-
logger.LogTrace($"{padding}{Consts.Emoji.Start} Deserialize 'value', type='{type.GetTypeShortName()}' name='{serializedMember.name.ValueOrNull()}'.");
242+
logger.LogTrace($"{padding}{Consts.Emoji.Start} Deserialize 'value', type='{type.GetTypeShortName()}' name='{data.name.ValueOrNull()}'.");
242243

243244
var success = TryDeserializeValueInternal(
244245
reflector,
245-
data: serializedMember,
246+
data: data,
246247
result: out result,
247248
type: type,
248249
depth: depth,

ReflectorNet/src/Convertor/Reflection/GenericReflectionConvertor.cs

Lines changed: 25 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -94,7 +94,7 @@ public override bool SetField(
9494

9595
if (!TryDeserializeValue(
9696
reflector,
97-
serializedMember: value,
97+
data: value,
9898
result: out var parsedValue,
9999
type: out var type,
100100
fallbackType: fallbackType,
@@ -105,6 +105,14 @@ public override bool SetField(
105105
stringBuilder?.AppendLine($"{padding}[Error] Failed to deserialize value for field '{fieldInfo.Name}'.");
106106
return false;
107107
}
108+
109+
// Check if field type matches parsed value type
110+
if (!TypeUtils.IsCastable(type, fieldInfo.FieldType))
111+
{
112+
stringBuilder?.AppendLine($"{padding}[Error] Parsed value type '{type.GetTypeName(pretty: false)}' is not assignable to field type '{fieldInfo.FieldType.GetTypeName(pretty: false)}' for field '{fieldInfo.Name}'.");
113+
return false;
114+
}
115+
108116
// TODO: Print previous and new value in stringBuilder
109117
fieldInfo.SetValue(obj, parsedValue);
110118
if (stringBuilder != null)
@@ -125,9 +133,16 @@ public override bool SetProperty(
125133
{
126134
var padding = StringUtils.GetPadding(depth);
127135

136+
// Check if property is writable
137+
if (!propertyInfo.CanWrite)
138+
{
139+
stringBuilder?.AppendLine($"{padding}[Error] Property '{propertyInfo.Name}' is read-only.");
140+
return false;
141+
}
142+
128143
if (!TryDeserializeValue(
129144
reflector,
130-
serializedMember: value,
145+
data: value,
131146
result: out var parsedValue,
132147
type: out var type,
133148
fallbackType: fallbackType,
@@ -138,6 +153,14 @@ public override bool SetProperty(
138153
stringBuilder?.AppendLine($"{padding}[Error] Failed to deserialize value for property '{propertyInfo.Name}'.");
139154
return false;
140155
}
156+
157+
// Check if property type matches parsed value type
158+
if (!TypeUtils.IsCastable(type, propertyInfo.PropertyType))
159+
{
160+
stringBuilder?.AppendLine($"{padding}[Error] Parsed value type '{type.GetTypeName(pretty: false)}' is not assignable to property type '{propertyInfo.PropertyType.GetTypeName(pretty: false)}' for property '{propertyInfo.Name}'.");
161+
return false;
162+
}
163+
141164
// TODO: Print previous and new value in stringBuilder
142165
propertyInfo.SetValue(obj, parsedValue);
143166
if (stringBuilder != null)

ReflectorNet/src/Utils/Json/JsonSerializer.cs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -105,6 +105,7 @@ public JsonSerializer(Reflector reflector)
105105
new DecimalJsonConverter(),
106106
new GuidJsonConverter(),
107107
new TimeSpanJsonConverter(),
108+
new TypeJsonConverter(),
108109

109110
// Json converters
110111
new JsonElementJsonConverter(),

ReflectorNet/src/Utils/TypeUtils.cs

Lines changed: 21 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -184,7 +184,27 @@ private static string ToPascalCase(string camelCase)
184184
return fieldInfo != null ? GetFieldDescription(fieldInfo) : null;
185185
}
186186

187-
public static bool IsCastable(Type type, Type to)
187+
/// <summary>
188+
/// Checks if an object's runtime type is assignable to the target type.
189+
/// This is a cross-platform alternative to Type.IsAssignableTo which is only available in .NET 5+.
190+
/// </summary>
191+
/// <param name="obj">The object to check (can be null)</param>
192+
/// <param name="targetType">The target type to check assignability to</param>
193+
/// <returns>True if the object can be assigned to the target type</returns>
194+
public static bool IsAssignableTo(object? obj, Type targetType)
195+
{
196+
if (targetType == null)
197+
return false;
198+
199+
// Null is assignable to any reference type or nullable value type
200+
if (obj == null)
201+
return !targetType.IsValueType || Nullable.GetUnderlyingType(targetType) != null;
202+
203+
// Check if the object's type is assignable to the target type
204+
return targetType.IsAssignableFrom(obj.GetType());
205+
}
206+
207+
public static bool IsCastable(Type? type, Type to)
188208
{
189209
if (type == null || to == null)
190210
return false;

0 commit comments

Comments
 (0)