-
Notifications
You must be signed in to change notification settings - Fork 3
Expand file tree
/
Copy pathBaseReflectionConverter.Deserialize.cs
More file actions
383 lines (335 loc) · 18.4 KB
/
BaseReflectionConverter.Deserialize.cs
File metadata and controls
383 lines (335 loc) · 18.4 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
/*
* ReflectorNet
* Author: Ivan Murzak (https://github.com/IvanMurzak)
* Copyright (c) 2025 Ivan Murzak
* Licensed under the Apache License, Version 2.0. See LICENSE file in the project root for full license information.
*/
using System;
using System.Reflection;
using com.IvanMurzak.ReflectorNet.Model;
using com.IvanMurzak.ReflectorNet.Utils;
using Microsoft.Extensions.Logging;
using System.Text.Json;
namespace com.IvanMurzak.ReflectorNet.Converter
{
public abstract partial class BaseReflectionConverter<T> : IReflectionConverter
{
/// <summary>
/// Performs comprehensive deserialization of SerializedMember data into strongly-typed objects.
/// This method serves as the main entry point for converting serialized representations back into
/// live .NET objects with full type preservation and validation.
///
/// Deserialization Process:
/// 1. Value Deserialization: Attempts to deserialize the core value using TryDeserializeValue
/// 2. Field Modification: Iterates through serialized fields and applies them to the target object
/// 3. Property Modification: Iterates through serialized properties and applies them to the target object
/// 4. Type Validation: Ensures field/property types are compatible with target object
/// 5. Instance Creation: Creates object instances as needed during the deserialization process
/// 6. Error Handling: Provides comprehensive error reporting with hierarchical formatting
///
/// Field and Property Handling:
/// - Uses reflection to locate corresponding fields/properties on the target type
/// - Supports both public and non-public members based on BindingFlags
/// - Validates writability for properties before attempting to set values
/// - Provides detailed warnings for missing or incompatible members
/// - Recursive deserialization for complex nested objects
///
/// Error Recovery:
/// - Continues processing remaining members even if individual members fail
/// - Provides detailed error messages with proper indentation for nested structures
/// - Logs warnings for non-critical issues while preserving overall deserialization
/// </summary>
/// <param name="reflector">The Reflector instance used for recursive deserialization operations.</param>
/// <param name="data">SerializedMember containing the data to deserialize.</param>
/// <param name="fallbackType">Optional type to use when type information is missing from data.</param>
/// <param name="fallbackName">Optional name to use for logging when name is missing from data.</param>
/// <param name="depth">Current depth in the object hierarchy for proper error message indentation.</param>
/// <param name="stringBuilder">Optional StringBuilder for accumulating detailed operation logs.</param>
/// <param name="logger">Optional logger for tracing deserialization operations.</param>
/// <returns>The deserialized object instance, or null if deserialization fails.</returns>
public virtual object? Deserialize(
Reflector reflector,
SerializedMember data,
Type? fallbackType = null,
string? fallbackName = null,
int depth = 0,
Logs? logs = null,
ILogger? logger = null,
DeserializationContext? context = null)
{
if (reflector == null) throw new ArgumentNullException(nameof(reflector));
if (data == null) throw new ArgumentNullException(nameof(data));
if (!TryDeserializeValue(
reflector,
data: data,
result: out var result,
type: out var type,
fallbackType: fallbackType,
depth: depth,
logs: logs,
logger: logger))
{
return result;
}
var padding = StringUtils.GetPadding(depth);
// Register the object early (before deserializing children) so child references can resolve
if (result != null && context != null)
context.Register(result);
if (data.fields != null)
{
if (data.fields.Count > 0)
result ??= CreateInstance(reflector, type!);
if (logger?.IsEnabled(LogLevel.Trace) == true)
logger.LogTrace($"{padding}{Consts.Emoji.Field} Deserialize '{nameof(SerializedMember.fields)}' type='{type?.GetTypeId().ValueOrNull()}' name='{(StringUtils.IsNullOrEmpty(data.name) ? fallbackName : data.name).ValueOrNull()}'.");
foreach (var field in data.fields)
{
if (string.IsNullOrEmpty(field.name))
{
if (logger?.IsEnabled(LogLevel.Warning) == true)
logger.LogWarning($"{padding}{Consts.Emoji.Warn} Field name is null or empty in serialized data: '{(StringUtils.IsNullOrEmpty(data.name) ? fallbackName : data.name).ValueOrNull()}'. Skipping.");
logs?.Warning($"Field name is null or empty in serialized data: '{(StringUtils.IsNullOrEmpty(data.name) ? fallbackName : data.name).ValueOrNull()}'. Skipping.", depth);
continue;
}
var fieldInfo = type!.GetField(field.name, BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.Instance);
if (fieldInfo == null)
{
if (logger?.IsEnabled(LogLevel.Warning) == true)
logger.LogWarning($"{padding}{Consts.Emoji.Warn} Field '{field.name}' not found on type '{type.GetTypeId()}'.");
logs?.Warning($"Field '{field.name}' not found on type '{type.GetTypeId()}'.", depth);
continue;
}
var fieldValue = reflector.Deserialize(
data: field,
fallbackType: fieldInfo.FieldType,
depth: depth + 1,
logs: logs,
logger: logger,
context: context);
fieldInfo.SetValue(result, fieldValue);
}
}
if (data.props != null)
{
if (data.props.Count > 0)
result ??= CreateInstance(reflector, type!);
if (logger?.IsEnabled(LogLevel.Trace) == true)
logger.LogTrace($"{padding}{Consts.Emoji.Property} Deserialize '{nameof(SerializedMember.props)}' type='{type?.GetTypeId().ValueOrNull()}' name='{(StringUtils.IsNullOrEmpty(data.name) ? fallbackName : data.name).ValueOrNull()}'.");
foreach (var property in data.props)
{
if (string.IsNullOrEmpty(property.name))
{
if (logger?.IsEnabled(LogLevel.Warning) == true)
logger.LogWarning($"{padding}{Consts.Emoji.Warn} Property name is null or empty in serialized data: '{(StringUtils.IsNullOrEmpty(data.name) ? fallbackName : data.name).ValueOrNull()}'. Skipping.");
logs?.Warning($"Property name is null or empty in serialized data: '{(StringUtils.IsNullOrEmpty(data.name) ? fallbackName : data.name).ValueOrNull()}'. Skipping.", depth);
continue;
}
var propertyInfo = type!.GetProperty(property.name, BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.Instance);
if (propertyInfo == null)
{
if (logger?.IsEnabled(LogLevel.Warning) == true)
logger.LogWarning($"{padding}{Consts.Emoji.Warn} Property '{property.name}' not found on type '{type.GetTypeId()}'.");
logs?.Warning($"Property '{property.name}' not found on type '{type.GetTypeId()}'.", depth);
continue;
}
if (!propertyInfo.CanWrite)
{
if (logger?.IsEnabled(LogLevel.Warning) == true)
logger.LogWarning($"{padding}{Consts.Emoji.Warn} Property '{property.name}' on type '{type.GetTypeId()}' is read-only and cannot be set.");
logs?.Warning($"Property '{property.name}' on type '{type.GetTypeId()}' is read-only and cannot be set.", depth);
continue;
}
var propertyValue = reflector.Deserialize(
property,
fallbackType: propertyInfo.PropertyType,
depth: depth + 1,
logs: logs,
logger: logger,
context: context);
propertyInfo.SetValue(result, propertyValue);
}
}
return result;
}
/// <summary>
/// Attempts to deserialize the value portion of a SerializedMember with comprehensive type resolution and validation.
/// This method orchestrates the value deserialization process including type resolution, converter selection,
/// and delegation to appropriate internal deserialization methods.
///
/// Type Resolution Process:
/// 1. Prioritizes type information from SerializedMember.typeName
/// 2. Falls back to provided fallbackType parameter
/// 3. Validates type compatibility and existence
/// 4. Handles nullable type unwrapping automatically
///
/// Deserialization Strategies:
/// - Cascade mode: Attempts to deserialize as SerializedMember structure for complex objects
/// - Direct mode: Deserializes directly from JSON for primitive and simple types
/// - Error recovery: Provides meaningful error messages and fallback values
/// - Logging integration: Comprehensive trace logging for debugging and monitoring
///
/// Success/Failure Handling:
/// - Returns true for successful deserialization with valid result
/// - Returns false for failed deserialization with appropriate error logging
/// - Provides detailed error information in StringBuilder for analysis
/// - Maintains type safety throughout the deserialization process
/// </summary>
/// <param name="reflector">The Reflector instance used for type resolution and recursive operations.</param>
/// <param name="data">The SerializedMember containing the data to deserialize.</param>
/// <param name="result">Output parameter containing the deserialized object on success.</param>
/// <param name="type">Output parameter containing the resolved target type.</param>
/// <param name="fallbackType">Optional fallback type when type resolution from data fails.</param>
/// <param name="depth">Current depth in object hierarchy for proper error message indentation.</param>
/// <param name="stringBuilder">Optional StringBuilder for accumulating detailed operation logs.</param>
/// <param name="logger">Optional logger for tracing deserialization operations.</param>
/// <returns>True if deserialization succeeded, false otherwise.</returns>
protected virtual bool TryDeserializeValue(
Reflector reflector,
SerializedMember? data,
out object? result,
out Type? type,
Type? fallbackType = null,
int depth = 0,
Logs? logs = null,
ILogger? logger = null)
{
if (reflector == null) throw new ArgumentNullException(nameof(reflector));
if (data == null)
{
result = null;
type = null;
return false;
}
var padding = StringUtils.GetPadding(depth);
// Get the most appropriate type for deserialization
type = TypeUtils.GetTypeWithNamePriority(data, fallbackType, out var error);
if (type == null)
{
result = null;
logs?.Error(error ?? "Unknown error", depth);
if (logger?.IsEnabled(LogLevel.Error) == true)
logger.LogError($"{padding}{error}");
return false;
}
if (logger?.IsEnabled(LogLevel.Trace) == true)
logger.LogTrace($"{padding}{Consts.Emoji.Start} Deserialize 'value', type='{type.GetTypeId()}' name='{data.name.ValueOrNull()}'.");
var success = TryDeserializeValueInternal(
reflector,
data: data,
result: out result,
type: type,
depth: depth,
logs: logs,
logger: logger);
if (success)
{
if (logger?.IsEnabled(LogLevel.Trace) == true)
logger.LogTrace($"{padding}{Consts.Emoji.Done} Deserialized '{type.GetTypeId()}'.");
}
else
{
if (logger?.IsEnabled(LogLevel.Error) == true)
logger.LogError($"{padding}{Consts.Emoji.Fail} Deserialization '{type.GetTypeId()}' failed. Converter: {GetType().GetTypeShortName()}");
}
return success;
}
protected virtual bool TryDeserializeValueInternal(
Reflector reflector,
SerializedMember data,
out object? result,
Type type,
int depth = 0,
Logs? logs = null,
ILogger? logger = null)
{
if (reflector == null) throw new ArgumentNullException(nameof(reflector));
if (data == null) throw new ArgumentNullException(nameof(data));
if (type == null) throw new ArgumentNullException(nameof(type));
var padding = StringUtils.GetPadding(depth);
if (AllowCascadeSerialization)
{
try
{
if (data.valueJsonElement == null)
{
if (logger?.IsEnabled(LogLevel.Trace) == true)
logger.LogTrace($"{padding}'value' is null. Converter: {GetType().GetTypeShortName()}");
result = GetDefaultValue(reflector, type);
return true;
}
if (data.valueJsonElement.Value.ValueKind != JsonValueKind.Object)
{
if (logger?.IsEnabled(LogLevel.Error) == true)
logger.LogError($"{padding}'value' is not an object. It is '{data.valueJsonElement?.ValueKind}'. Converter: {GetType().GetTypeShortName()}");
logs?.Error("'value' is not an object. Attempting to deserialize as SerializedMember.", depth);
result = reflector.GetDefaultValue(type);
return false;
}
result = data.valueJsonElement.DeserializeValueSerializedMember(
reflector,
type: type,
name: data.name,
depth: depth + 1,
logs: logs,
logger: logger);
return true;
}
catch (JsonException ex)
{
if (logger?.IsEnabled(LogLevel.Warning) == true)
logger.LogWarning($"{padding}{Consts.Emoji.Warn} Deserialize 'value', type='{type.GetTypeId()}' name='{data.name.ValueOrNull()}':\n{padding}{ex.Message}\n{ex.StackTrace}");
logs?.Warning($"Failed to deserialize member '{data.name.ValueOrNull()}' of type '{type.GetTypeId()}':\n{ex.Message}", depth);
}
catch (NotSupportedException ex)
{
if (logger?.IsEnabled(LogLevel.Warning) == true)
logger.LogWarning($"{padding}{Consts.Emoji.Warn} Deserialize 'value', type='{type.GetTypeId()}' name='{data.name.ValueOrNull()}':\n{padding}{ex.Message}\n{ex.StackTrace}");
logs?.Warning($"Unsupported type '{type.GetTypeId()}' for member '{data.name.ValueOrNull()}':\n{ex.Message}", depth);
}
result = reflector.GetDefaultValue(type);
return false;
}
else
{
try
{
if (logger?.IsEnabled(LogLevel.Trace) == true)
logger.LogTrace($"{padding}Deserialize as json. Converter: {GetType().GetTypeShortName()}");
result = DeserializeValueAsJsonElement(
reflector: reflector,
data: data,
type: type,
depth: depth,
logs: logs,
logger: logger);
if (logger?.IsEnabled(LogLevel.Trace) == true)
logger.LogTrace($"{padding}{Consts.Emoji.Done} Deserialized as json: {data.valueJsonElement}");
return true;
}
catch (Exception ex)
{
logs?.Error($"Failed to deserialize value'{data.name.ValueOrNull()}' of type '{type.GetTypeId()}':\n{ex.Message}", depth);
if (logger?.IsEnabled(LogLevel.Critical) == true)
logger.LogCritical($"{padding}{Consts.Emoji.Fail} Deserialize 'value', type='{type.GetTypeId()}' name='{data.name.ValueOrNull()}':\n{padding}{ex.Message}\n{ex.StackTrace}");
result = reflector.GetDefaultValue(type);
return false;
}
}
}
protected virtual object? DeserializeValueAsJsonElement(
Reflector reflector,
SerializedMember data,
Type type,
int depth = 0,
Logs? logs = null,
ILogger? logger = null)
{
if (reflector == null) throw new ArgumentNullException(nameof(reflector));
if (data == null) throw new ArgumentNullException(nameof(data));
if (type == null) throw new ArgumentNullException(nameof(type));
return reflector.JsonSerializer.Deserialize(
reflector,
data.valueJsonElement,
type);
}
}
}