diff --git a/ReflectorNet.Tests/src/ReflectorTests/SerializePopulateTests.cs b/ReflectorNet.Tests/src/ReflectorTests/SerializePopulateTests.cs index bc58ef8..4f5b0f6 100644 --- a/ReflectorNet.Tests/src/ReflectorTests/SerializePopulateTests.cs +++ b/ReflectorNet.Tests/src/ReflectorTests/SerializePopulateTests.cs @@ -30,13 +30,15 @@ void ActAssertPopulate(object? original, Type? fallbackType = null) var targetObject = reflector.CreateInstance(type); var populateLogger = new StringBuilderLogger(); - var populateOutput = reflector.TryPopulate( + var success = reflector.TryPopulate( obj: ref targetObject, data: serialized, fallbackObjType: type, logs: new Logs(), logger: populateLogger); + Assert.True(success); + _output.WriteLine($"Population:\n{populateLogger}"); // Assert @@ -53,6 +55,8 @@ void ActAssertPopulate(object? original, Type? fallbackType = null) _output.WriteLine($"Serialized JSON:\n{serialized.ToJson(reflector)}\n"); Assert.Equal(originalJson, populatedJson); + + _output.WriteLine($"============================\n"); } [Fact] diff --git a/ReflectorNet.Tests/src/SchemaTests/SchemaSerializationValidationTests.cs b/ReflectorNet.Tests/src/SchemaTests/SchemaSerializationValidationTests.cs index f31ea54..2e4bd34 100644 --- a/ReflectorNet.Tests/src/SchemaTests/SchemaSerializationValidationTests.cs +++ b/ReflectorNet.Tests/src/SchemaTests/SchemaSerializationValidationTests.cs @@ -103,7 +103,7 @@ private void ValidateBasicSchemaConformance(JsonNode schema, object serialized, // Convert serialized object to JSON and extract the "value" field var json = serialized.ToJson(reflector); - var jsonNode = JsonNode.Parse(json); + var jsonNode = JsonNode.Parse(json!); // SerializedMember has structure: { "name": "...", "typeName": "...", "value": ... } // We need to extract just the "value" field to compare against the schema diff --git a/ReflectorNet.Tests/src/SchemaTests/SerializedMemberSchemaTests.cs b/ReflectorNet.Tests/src/SchemaTests/SerializedMemberSchemaTests.cs index 5b60a1f..1942f6d 100644 --- a/ReflectorNet.Tests/src/SchemaTests/SerializedMemberSchemaTests.cs +++ b/ReflectorNet.Tests/src/SchemaTests/SerializedMemberSchemaTests.cs @@ -59,7 +59,7 @@ public void SerializedMember_Should_Serialize_PrimitiveTypes_Correctly(object va var json = serialized.ToJson(reflector); // Parse and validate JSON structure - var jsonNode = JsonNode.Parse(json); + var jsonNode = JsonNode.Parse(json!); Assert.NotNull(jsonNode); var jsonObj = jsonNode!.AsObject(); @@ -85,7 +85,7 @@ public void SerializedMember_Should_Serialize_Null_Correctly() var json = serialized.ToJson(reflector); // Parse and validate JSON structure - var jsonNode = JsonNode.Parse(json); + var jsonNode = JsonNode.Parse(json!); Assert.NotNull(jsonNode); var jsonObj = jsonNode!.AsObject(); @@ -111,7 +111,7 @@ public void SerializedMember_Should_Serialize_Array_Correctly() var json = serialized.ToJson(reflector); // Parse and validate JSON structure - var jsonNode = JsonNode.Parse(json); + var jsonNode = JsonNode.Parse(json!); Assert.NotNull(jsonNode); var jsonObj = jsonNode!.AsObject(); @@ -136,7 +136,7 @@ public void SerializedMember_Should_Serialize_Object_Correctly() var json = serialized.ToJson(reflector); // Parse and validate JSON structure - var jsonNode = JsonNode.Parse(json); + var jsonNode = JsonNode.Parse(json!); Assert.NotNull(jsonNode); var jsonObj = jsonNode!.AsObject(); diff --git a/ReflectorNet/src/Converter/IReflectionConverter.cs b/ReflectorNet/src/Converter/IReflectionConverter.cs index 0692b9d..c1f38df 100644 --- a/ReflectorNet/src/Converter/IReflectionConverter.cs +++ b/ReflectorNet/src/Converter/IReflectionConverter.cs @@ -45,7 +45,7 @@ bool TryPopulate( Reflector reflector, ref object? obj, SerializedMember data, - Type? fallbackType = null, + Type type, int depth = 0, Logs? logs = null, BindingFlags flags = BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.Instance, diff --git a/ReflectorNet/src/Converter/Json/BigIntegerJsonConverter.cs b/ReflectorNet/src/Converter/Json/BigIntegerJsonConverter.cs index 7130c6a..eda400a 100644 --- a/ReflectorNet/src/Converter/Json/BigIntegerJsonConverter.cs +++ b/ReflectorNet/src/Converter/Json/BigIntegerJsonConverter.cs @@ -22,7 +22,7 @@ public class BigIntegerJsonConverter : JsonSchemaConverter, IJsonSch public static JsonNode Schema => new JsonObject { [JsonSchema.Type] = JsonSchema.String, - ["description"] = "A large integer represented as a string" + [JsonSchema.Description] = "A large integer represented as a string" }; public static JsonNode SchemaRef => new JsonObject diff --git a/ReflectorNet/src/Converter/Json/VersionJsonConverter.cs b/ReflectorNet/src/Converter/Json/VersionJsonConverter.cs index 3913d46..29dc5ac 100644 --- a/ReflectorNet/src/Converter/Json/VersionJsonConverter.cs +++ b/ReflectorNet/src/Converter/Json/VersionJsonConverter.cs @@ -7,7 +7,7 @@ using System; using System.Text.Json; -using System.Text.Json.Serialization; +using System.Text.Json.Nodes; using com.IvanMurzak.ReflectorNet.Utils; namespace com.IvanMurzak.ReflectorNet.Json @@ -15,8 +15,20 @@ namespace com.IvanMurzak.ReflectorNet.Json /// /// JsonConverter that handles conversion between JSON strings and System.Version. /// - public class VersionJsonConverter : JsonConverter + public class VersionJsonConverter : JsonSchemaConverter, IJsonSchemaConverter { + public static JsonNode Schema => new JsonObject + { + [JsonSchema.Type] = JsonSchema.String, + }; + public static JsonNode SchemaRef => new JsonObject + { + [JsonSchema.Ref] = JsonSchema.RefValue + StaticId + }; + + public override JsonNode GetSchema() => Schema; + public override JsonNode GetSchemaRef() => SchemaRef; + public override Version? Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) { if (reader.TokenType == JsonTokenType.Null) diff --git a/ReflectorNet/src/Converter/Reflection/ArrayReflectionConverter.cs b/ReflectorNet/src/Converter/Reflection/ArrayReflectionConverter.cs index 44f6803..22c4f0f 100644 --- a/ReflectorNet/src/Converter/Reflection/ArrayReflectionConverter.cs +++ b/ReflectorNet/src/Converter/Reflection/ArrayReflectionConverter.cs @@ -87,7 +87,7 @@ protected override SerializedMember InternalSerialize( if (logger?.IsEnabled(LogLevel.Trace) == true) logger.LogTrace("{padding} Serializing item '{index}' of type '{type}' in '{objType}'.\nPath: {path}", - StringUtils.GetPadding(depth), index, currentType?.GetTypeId(), obj.GetType().GetTypeId(), context?.GetPath(obj)); + StringUtils.GetPadding(depth), index, currentType?.GetTypeId().ValueOrNull(), obj.GetType().GetTypeId().ValueOrNull(), context?.GetPath(obj)); if (thisElementType != null && reflector.Converters.IsTypeBlacklisted(thisElementType)) { @@ -119,7 +119,7 @@ protected override SerializedMember InternalSerialize( // Handle non-recursive serialization return SerializedMember.FromJson( type: type, - json: obj.ToJson(reflector), + json: obj.ToJson(reflector, depth: depth, logger: logger), name: name); } } diff --git a/ReflectorNet/src/Converter/Reflection/Base/BaseReflectionConverter.Populate.cs b/ReflectorNet/src/Converter/Reflection/Base/BaseReflectionConverter.Populate.cs index ab4958c..73b9569 100644 --- a/ReflectorNet/src/Converter/Reflection/Base/BaseReflectionConverter.Populate.cs +++ b/ReflectorNet/src/Converter/Reflection/Base/BaseReflectionConverter.Populate.cs @@ -22,7 +22,7 @@ public virtual bool TryPopulate( Reflector reflector, ref object? obj, SerializedMember data, - Type? fallbackType = null, + Type type, int depth = 0, Logs? logs = null, BindingFlags flags = BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.Instance, @@ -30,24 +30,12 @@ public virtual bool TryPopulate( { var padding = StringUtils.GetPadding(depth); - var objType = TypeUtils.GetTypeWithNamePriority(data, fallbackType, out var typeError) ?? obj?.GetType(); - if (objType == null) - { - if (logger?.IsEnabled(LogLevel.Error) == true) - logger.LogError($"{padding}Failed to determine type for object '{data.name.ValueOrNull()}'. {typeError}"); - - if (logs != null) - logs.Error($"Failed to determine type for object '{data.name.ValueOrNull()}'. {typeError}", depth); - - return false; - } - if (obj == null) { // obj = CreateInstance(reflector, objType); obj = reflector.Deserialize( data: data, - fallbackType: objType, + fallbackType: type, depth: depth, logs: logs, logger: logger); @@ -55,24 +43,24 @@ public virtual bool TryPopulate( if (obj == null) { if (logger?.IsEnabled(LogLevel.Error) == true) - logger.LogError($"{padding}Object '{data.name.ValueOrNull()}' population failed: Object is null. Instance creation failed for type '{objType.GetTypeId()}'."); + logger.LogError($"{padding}Object '{data.name.ValueOrNull()}' population failed: Object is null. Instance creation failed for type '{type.GetTypeId()}'."); if (logs != null) - logs.Error($"Object '{data.name.ValueOrNull()}' population failed: Object is null. Instance creation failed for type '{objType.GetTypeId()}'.", depth); + logs.Error($"Object '{data.name.ValueOrNull()}' population failed: Object is null. Instance creation failed for type '{type.GetTypeId()}'.", depth); return false; } if (logger?.IsEnabled(LogLevel.Trace) == true) - logger.LogTrace($"{padding}Object '{data.name.ValueOrNull()}' populated with type '{objType.GetTypeId()}'."); + logger.LogTrace($"{padding}Object '{data.name.ValueOrNull()}' populated with type '{type.GetTypeId()}'."); if (logs != null) - logs.Success($"Object '{data.name.ValueOrNull()}' populated with type '{objType.GetTypeId()}'.", depth); + logs.Success($"Object '{data.name.ValueOrNull()}' populated with type '{type.GetTypeId()}'.", depth); return true; } - if (!TypeUtils.IsCastable(obj.GetType(), objType)) + if (!TypeUtils.IsCastable(obj.GetType(), type)) { if (logger?.IsEnabled(LogLevel.Error) == true) logger.LogError($"{padding}Type mismatch: '{data.typeName}' vs '{obj.GetType().GetTypeId().ValueOrNull()}'."); @@ -91,7 +79,7 @@ public virtual bool TryPopulate( var success = SetValue( reflector: reflector, obj: ref obj, - type: objType, + type: type, value: data.valueJsonElement, depth: depth, logs: logs, @@ -136,7 +124,7 @@ public virtual bool TryPopulate( var success = TryPopulateField( reflector, obj: ref obj, - objType: objType, + objType: type, fieldValue: field, depth: nextDepth, logs: logs, @@ -180,7 +168,7 @@ public virtual bool TryPopulate( var success = TryPopulateProperty( reflector, obj: ref obj, - objType: objType, + objType: type, propertyValue: property, depth: nextDepth, logs: logs, diff --git a/ReflectorNet/src/Converter/Reflection/Base/BaseReflectionConverter.Serialize.cs b/ReflectorNet/src/Converter/Reflection/Base/BaseReflectionConverter.Serialize.cs index 82e50bb..8b691d4 100644 --- a/ReflectorNet/src/Converter/Reflection/Base/BaseReflectionConverter.Serialize.cs +++ b/ReflectorNet/src/Converter/Reflection/Base/BaseReflectionConverter.Serialize.cs @@ -23,7 +23,7 @@ public abstract partial class BaseReflectionConverter : IReflectionConverter public virtual SerializedMember Serialize( Reflector reflector, object? obj, - Type? type = null, + Type? fallbackType = null, string? name = null, bool recursive = true, BindingFlags flags = BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.Instance, @@ -32,10 +32,12 @@ public virtual SerializedMember Serialize( ILogger? logger = null, SerializationContext? context = null) { + var actualType = fallbackType ?? obj?.GetType() ?? typeof(T); + return InternalSerialize( reflector: reflector, obj: obj, - type: type ?? obj?.GetType() ?? typeof(T), + type: actualType, name: name, recursive: recursive, flags: flags, @@ -63,13 +65,6 @@ public virtual SerializedMember Serialize( foreach (var field in fields) { - if (GetIgnoredFields().Contains(field.Name)) - { - if (logger?.IsEnabled(LogLevel.Trace) == true) - logger.LogTrace("{padding}Skipping serialization of field '{fieldName}' in '{objType}' because it is ignored.\nPath: {path}", - StringUtils.GetPadding(depth + 1), field.Name, objType.GetTypeId(), context?.GetPath(obj)); - continue; - } if (reflector.Converters.IsTypeBlacklisted(field.FieldType)) { if (logger?.IsEnabled(LogLevel.Trace) == true) @@ -128,24 +123,17 @@ public virtual SerializedMember Serialize( foreach (var prop in properties) { - if (GetIgnoredProperties().Contains(prop.Name)) - { - if (logger?.IsEnabled(LogLevel.Trace) == true) - logger.LogTrace("{padding} Skipping serialization of property '{propertyName}' in '{objType}' because it is ignored.\nPath: {path}", - StringUtils.GetPadding(depth + 1), prop.Name, objType.GetTypeId(), context?.GetPath(obj)); - continue; - } if (reflector.Converters.IsTypeBlacklisted(prop.PropertyType)) { if (logger?.IsEnabled(LogLevel.Trace) == true) - logger.LogTrace("{padding} Skipping serialization of property '{propertyName}' of type '{type}' in '{objType}' because its type is blacklisted.\nPath: {path}", + logger.LogTrace("{padding}Skipping serialization of property '{propertyName}' of type '{type}' in '{objType}' because its type is blacklisted.\nPath: {path}", StringUtils.GetPadding(depth + 1), prop.Name, prop.PropertyType.GetTypeId(), objType.GetTypeId(), context?.GetPath(obj)); continue; } try { if (logger?.IsEnabled(LogLevel.Trace) == true) - logger.LogTrace("{padding} Serializing property '{propertyName}' of type '{type}' in '{objType}'.\nPath: {path}", + logger.LogTrace("{padding}Serializing property '{propertyName}' of type '{type}' in '{objType}'.\nPath: {path}", StringUtils.GetPadding(depth + 1), prop.Name, prop.PropertyType.GetTypeId(), objType.GetTypeId(), context?.GetPath(obj)); var value = prop.GetValue(obj); diff --git a/ReflectorNet/src/Converter/Reflection/Base/BaseReflectionConverter.cs b/ReflectorNet/src/Converter/Reflection/Base/BaseReflectionConverter.cs index 2cdc118..b62149f 100644 --- a/ReflectorNet/src/Converter/Reflection/Base/BaseReflectionConverter.cs +++ b/ReflectorNet/src/Converter/Reflection/Base/BaseReflectionConverter.cs @@ -103,7 +103,22 @@ public virtual int SerializationPriority(Type type, ILogger? logger = null) /// Default implementation returns public fields that are not marked with [Obsolete] or [NonSerialized]. /// Derived classes can override to customize field selection. /// - public virtual IEnumerable? GetSerializableFields( + public IEnumerable? GetSerializableFields( + Reflector reflector, + Type objType, + BindingFlags flags = BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.Instance, + ILogger? logger = null) + { + return GetSerializableFieldsInternal(reflector, objType, flags, logger) + ?.Where(field => !GetIgnoredFields().Contains(field.Name)); + } + + /// + /// Gets the serializable fields for the specified type. + /// Default implementation returns public fields that are not marked with [Obsolete] or [NonSerialized]. + /// Derived classes can override to customize field selection. + /// + protected virtual IEnumerable? GetSerializableFieldsInternal( Reflector reflector, Type objType, BindingFlags flags = BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.Instance, @@ -120,7 +135,22 @@ public virtual int SerializationPriority(Type type, ILogger? logger = null) /// Default implementation returns readable properties that are not marked with [Obsolete]. /// Derived classes can override to customize property selection. /// - public virtual IEnumerable? GetSerializableProperties( + public IEnumerable? GetSerializableProperties( + Reflector reflector, + Type objType, + BindingFlags flags = BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.Instance, + ILogger? logger = null) + { + return GetSerializablePropertiesInternal(reflector, objType, flags, logger) + ?.Where(prop => !GetIgnoredProperties().Contains(prop.Name)); + } + + /// + /// Gets the serializable properties for the specified type. + /// Default implementation returns readable properties that are not marked with [Obsolete]. + /// Derived classes can override to customize property selection. + /// + protected virtual IEnumerable? GetSerializablePropertiesInternal( Reflector reflector, Type objType, BindingFlags flags = BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.Instance, diff --git a/ReflectorNet/src/Converter/Reflection/GenericReflectionConverter.cs b/ReflectorNet/src/Converter/Reflection/GenericReflectionConverter.cs index fb84855..756e0a7 100644 --- a/ReflectorNet/src/Converter/Reflection/GenericReflectionConverter.cs +++ b/ReflectorNet/src/Converter/Reflection/GenericReflectionConverter.cs @@ -60,7 +60,7 @@ protected override SerializedMember InternalSerialize( valueJsonElement = new JsonObject().ToJsonElement() }; } - return SerializedMember.FromJson(type, obj.ToJson(reflector, logger: logger), name: name); + return SerializedMember.FromJson(type, obj.ToJson(reflector, depth: depth, logger: logger), name: name); } throw new ArgumentException($"Unsupported type: '{type.GetTypeId()}' for converter '{GetType().GetTypeShortName()}'."); } diff --git a/ReflectorNet/src/Converter/Reflection/IgnoreFieldsAndPropertiesReflectionConverter.cs b/ReflectorNet/src/Converter/Reflection/IgnoreFieldsAndPropertiesReflectionConverter.cs index ab04c9a..743a45a 100644 --- a/ReflectorNet/src/Converter/Reflection/IgnoreFieldsAndPropertiesReflectionConverter.cs +++ b/ReflectorNet/src/Converter/Reflection/IgnoreFieldsAndPropertiesReflectionConverter.cs @@ -33,7 +33,7 @@ public IgnoreFieldsAndPropertiesReflectionConverter(bool ignoreFields, bool igno /// public override bool AllowCascadeSerialization => false; - public override IEnumerable? GetSerializableFields( + protected override IEnumerable? GetSerializableFieldsInternal( Reflector reflector, Type objType, BindingFlags flags, @@ -41,10 +41,10 @@ public IgnoreFieldsAndPropertiesReflectionConverter(bool ignoreFields, bool igno { return _ignoreFields ? null - : base.GetSerializableFields(reflector, objType, flags, logger); + : base.GetSerializableFieldsInternal(reflector, objType, flags, logger); } - public override IEnumerable? GetSerializableProperties( + protected override IEnumerable? GetSerializablePropertiesInternal( Reflector reflector, Type objType, BindingFlags flags, @@ -52,7 +52,7 @@ public IgnoreFieldsAndPropertiesReflectionConverter(bool ignoreFields, bool igno { return _ignoreProperties ? null - : base.GetSerializableProperties(reflector, objType, flags, logger); + : base.GetSerializablePropertiesInternal(reflector, objType, flags, logger); } } } \ No newline at end of file diff --git a/ReflectorNet/src/Converter/Reflection/PrimitiveReflectionConverter.cs b/ReflectorNet/src/Converter/Reflection/PrimitiveReflectionConverter.cs index a0326e9..159c2d4 100644 --- a/ReflectorNet/src/Converter/Reflection/PrimitiveReflectionConverter.cs +++ b/ReflectorNet/src/Converter/Reflection/PrimitiveReflectionConverter.cs @@ -44,7 +44,7 @@ protected override SerializedMember InternalSerialize( return SerializedMember.FromValue(reflector, type, obj, name: name); } - public override IEnumerable? GetSerializableFields( + protected override IEnumerable? GetSerializableFieldsInternal( Reflector reflector, Type objType, BindingFlags flags, @@ -53,7 +53,7 @@ protected override SerializedMember InternalSerialize( return null; } - public override IEnumerable? GetSerializableProperties( + protected override IEnumerable? GetSerializablePropertiesInternal( Reflector reflector, Type objType, BindingFlags flags, diff --git a/ReflectorNet/src/Extension/ExtensionsJson.cs b/ReflectorNet/src/Extension/ExtensionsJson.cs index 031b17c..8f68288 100644 --- a/ReflectorNet/src/Extension/ExtensionsJson.cs +++ b/ReflectorNet/src/Extension/ExtensionsJson.cs @@ -8,6 +8,7 @@ using System; using System.Text.Json; using System.Text.Json.Nodes; +using com.IvanMurzak.ReflectorNet.Utils; using Microsoft.Extensions.Logging; namespace com.IvanMurzak.ReflectorNet @@ -20,7 +21,7 @@ public static JsonElement ToJsonElement(this object data, Reflector? reflector, logger.LogTrace("Converting object of type {Type} to JsonElement.", data?.GetType().GetTypeId().ValueOrNull()); - return JsonSerializer.SerializeToElement(data, options ?? reflector?.JsonSerializerOptions); + return System.Text.Json.JsonSerializer.SerializeToElement(data, options ?? reflector?.JsonSerializerOptions); } public static JsonElement? ToJsonElement(this JsonNode? node) @@ -36,17 +37,18 @@ public static JsonElement ToJsonElement(this object data, Reflector? reflector, return document.RootElement.Clone(); } - public static string ToJson(this object? value, Reflector? reflector, JsonSerializerOptions? options = null, ILogger? logger = null) + public static string? ToJson(this object? value, Reflector? reflector, JsonSerializerOptions? options = null, int depth = 0, ILogger? logger = null) { return ToJson( value: value, - defaultValue: Utils.JsonSerializer.EmptyJsonObject, // Use empty JSON object as default value + defaultValue: null, // Use null as default value reflector: reflector, options: options, + depth: depth, logger: logger); } - public static string ToJson(this object? value, string defaultValue, Reflector? reflector, JsonSerializerOptions? options = null, ILogger? logger = null) + public static string? ToJson(this object? value, string? defaultValue, Reflector? reflector, JsonSerializerOptions? options = null, int depth = 0, ILogger? logger = null) { if (value == null) return defaultValue; @@ -55,10 +57,10 @@ public static string ToJson(this object? value, string defaultValue, Reflector? throw new ArgumentException("Cannot serialize JsonSerializer instance.", nameof(value)); if (logger?.IsEnabled(LogLevel.Trace) == true) - logger.LogTrace("Serializing object of type {Type} to JSON string.", - value.GetType().GetTypeId().ValueOrNull()); + logger.LogTrace("{padding}Serializing object of type {type} to JSON string.", + StringUtils.GetPadding(depth), value.GetType().GetTypeId().ValueOrNull()); - return JsonSerializer.Serialize( + return System.Text.Json.JsonSerializer.Serialize( value: value, options: options ?? reflector?.JsonSerializerOptions); } diff --git a/ReflectorNet/src/Model/SerializedMember.cs b/ReflectorNet/src/Model/SerializedMember.cs index 91f3f40..46b5dbe 100644 --- a/ReflectorNet/src/Model/SerializedMember.cs +++ b/ReflectorNet/src/Model/SerializedMember.cs @@ -101,6 +101,11 @@ public SerializedMember AddProperty(SerializedMember property) return this; } + public bool IsNull() + { + return !valueJsonElement.HasValue || valueJsonElement.Value.ValueKind == JsonValueKind.Null; + } + public T? GetValue(Reflector reflector) => valueJsonElement.Deserialize(reflector); public SerializedMember SetValue(Reflector reflector, object? value) diff --git a/ReflectorNet/src/Reflector/Reflector.CallMethod.cs b/ReflectorNet/src/Reflector/Reflector.CallMethod.cs index d00a89f..9e65f5f 100644 --- a/ReflectorNet/src/Reflector/Reflector.CallMethod.cs +++ b/ReflectorNet/src/Reflector/Reflector.CallMethod.cs @@ -147,7 +147,7 @@ out var error : methodWrapper.Invoke(); var result = task.Result; - return $"[Success] Execution result:\n```json\n{result.ToJson(reflector, logger: logger)}\n```"; + return $"[Success] Execution result:\n```json\n{result.ToJson(reflector, depth: 0, logger: logger)}\n```"; }; if (executeInMainThread) diff --git a/ReflectorNet/src/Reflector/Reflector.Error.cs b/ReflectorNet/src/Reflector/Reflector.Error.cs index 65bf8e6..d51146c 100644 --- a/ReflectorNet/src/Reflector/Reflector.Error.cs +++ b/ReflectorNet/src/Reflector/Reflector.Error.cs @@ -46,7 +46,7 @@ public static string NotSupportedInRuntime(Type type) public static string MoreThanOneMethodFound(Reflector reflector, List methods, ILogger? logger = null) { var methodDataList = methods.Select(method => new MethodData(reflector, method)); - var methodsString = methodDataList.ToJson(reflector, logger: logger); + var methodsString = methodDataList.ToJson(reflector, depth: 0, logger: logger); return @$"[Error] Found more than one method. Only single method should be targeted. Please specify the method name more precisely. Found {methods.Count} method(s): diff --git a/ReflectorNet/src/Reflector/Reflector.Json.cs b/ReflectorNet/src/Reflector/Reflector.Json.cs index fd3c4b1..1de989c 100644 --- a/ReflectorNet/src/Reflector/Reflector.Json.cs +++ b/ReflectorNet/src/Reflector/Reflector.Json.cs @@ -2,7 +2,6 @@ using System.Reflection; using System.Text.Json; using System.Text.Json.Nodes; -using System.Threading.Tasks; using com.IvanMurzak.ReflectorNet.Utils; namespace com.IvanMurzak.ReflectorNet @@ -35,7 +34,9 @@ public partial class Reflector /// The type for which to generate the JSON Schema. /// A JsonNode containing the JSON Schema representation of the specified type. public JsonNode GetSchema() - => jsonSchema.GetSchema(this); + { + return GetSchema(typeof(T)); + } /// /// Generates a JSON Schema representation for the specified generic type parameter. @@ -54,7 +55,9 @@ public JsonNode GetSchema() /// The type for which to generate the JSON Schema. /// A JsonNode containing the JSON Schema representation of the specified type. public JsonNode GetSchemaRef() - => jsonSchema.GetSchemaRef(this); + { + return jsonSchema.GetSchemaRef(this); + } /// /// Generates a JSON Schema representation for the specified type. @@ -73,7 +76,9 @@ public JsonNode GetSchemaRef() /// The Type for which to generate the JSON Schema. /// A JsonNode containing the JSON Schema representation of the specified type. public JsonNode GetSchema(Type type) - => jsonSchema.GetSchema(this, type); + { + return jsonSchema.GetSchema(this, type); + } /// /// Generates a JSON Schema representation for the specified type. diff --git a/ReflectorNet/src/Reflector/Reflector.Populate.cs b/ReflectorNet/src/Reflector/Reflector.Populate.cs index 17ea543..68acec9 100644 --- a/ReflectorNet/src/Reflector/Reflector.Populate.cs +++ b/ReflectorNet/src/Reflector/Reflector.Populate.cs @@ -45,6 +45,14 @@ public bool TryPopulate( { var padding = StringUtils.GetPadding(depth); + if (obj == null && data.IsNull()) + { + if (logger?.IsEnabled(LogLevel.Trace) == true) + logger.LogTrace($"{padding}Object population skipped: both target object and data are null."); + + return true; + } + var objType = TypeUtils.GetTypeWithNamePriority(data, fallbackObjType, out var typeError) ?? obj?.GetType(); if (objType == null) { @@ -60,21 +68,21 @@ public bool TryPopulate( if (converter == null) { if (logger?.IsEnabled(LogLevel.Error) == true) - logger.LogError($"{padding}No suitable converter found for type '{objType.GetTypeId()}'"); + logger.LogError($"{padding}No suitable converter found for type '{objType.GetTypeId().ValueOrNull()}'"); - logs?.Error($"No suitable converter found for type '{objType.GetTypeId()}'", depth); + logs?.Error($"No suitable converter found for type '{objType.GetTypeId().ValueOrNull()}'", depth); return false; } if (logger?.IsEnabled(LogLevel.Trace) == true) - logger.LogTrace($"{padding}Populate. {converter.GetType().GetTypeShortName()} used for type='{objType?.GetTypeShortName()}', name='{data.name.ValueOrNull()}'"); + logger.LogTrace($"{padding}Populate. {converter.GetType().GetTypeShortName()} used for type='{objType.GetTypeShortName().ValueOrNull()}', name='{data.name.ValueOrNull()}'"); var success = converter.TryPopulate( this, ref obj, data: data, - fallbackType: objType, + type: objType, depth: depth, logs: logs, flags: flags, diff --git a/ReflectorNet/src/Reflector/Reflector.cs b/ReflectorNet/src/Reflector/Reflector.cs index 455243d..721356e 100644 --- a/ReflectorNet/src/Reflector/Reflector.cs +++ b/ReflectorNet/src/Reflector/Reflector.cs @@ -8,6 +8,7 @@ using System; using System.Collections.Generic; +using System.Linq; using System.Reflection; using com.IvanMurzak.ReflectorNet.Model; using com.IvanMurzak.ReflectorNet.Utils; @@ -110,20 +111,34 @@ public SerializedMember Serialize( if (Converters.IsTypeBlacklisted(type)) { if (logger?.IsEnabled(LogLevel.Trace) == true) - logger.LogTrace("{padding}Serialize. Type '{type}' is blacklisted, skipping.", - StringUtils.GetPadding(depth), type.GetTypeId().ValueOrNull()); + logger.LogTrace("{padding}Serialize skip for '{name}' of type '{type}', it is blacklisted type.", + StringUtils.GetPadding(depth), name.ValueOrNull(), type.GetTypeId().ValueOrNull()); return SerializedMember.Null(type, name); } - var converter = Converters.GetConverter(type); - if (converter == null) - throw new ArgumentException($"Failed to serialize '{name.ValueOrNull()}'. Type '{type.GetTypeId().ValueOrNull()}' not supported for serialization."); + var jsonConverter = JsonSerializer.GetJsonConverter(type); + if (jsonConverter != null) + { + if (logger?.IsEnabled(LogLevel.Trace) == true) + logger.LogTrace("{padding}Serialize '{name}' of type '{type}'. JsonConverter: {converter}", + StringUtils.GetPadding(depth), name.ValueOrNull(), type.GetTypeId().ValueOrNull(), jsonConverter.GetType().GetTypeId().ValueOrNull()); + + return SerializedMember.FromJson( + type: type, + json: obj.ToJson(this, depth: depth, logger: logger), + name: name); + } + + var reflectionConverter = Converters.GetConverter(type); if (logger?.IsEnabled(LogLevel.Trace) == true) - logger.LogTrace("{padding}Serialize '{name}' of type '{type}'. Converter: {converter}", - StringUtils.GetPadding(depth), name.ValueOrNull(), type.GetTypeId().ValueOrNull(), converter.GetType().GetTypeShortName()); + logger.LogTrace("{padding}Serialize '{name}' of type '{type}'. ReflectionConverter: {converter}", + StringUtils.GetPadding(depth), name.ValueOrNull(), type.GetTypeId().ValueOrNull(), reflectionConverter?.GetType().GetTypeShortName()?.ValueOrNull()); + + if (reflectionConverter == null) + throw new ArgumentException($"Failed to serialize '{name.ValueOrNull()}'. Type '{type.GetTypeId().ValueOrNull()}' not supported for serialization."); - return converter.Serialize( + return reflectionConverter.Serialize( this, obj, fallbackType: type, @@ -254,6 +269,15 @@ public SerializedMember Serialize( return GetDefaultValue(type); } + var jsonConverter = JsonSerializer.GetJsonConverter(type); + if (jsonConverter != null) + { + if (logger?.IsEnabled(LogLevel.Trace) == true) + logger.LogTrace($"{padding}{Consts.Emoji.Launch} Deserialize type='{type.GetTypeShortName()}' name='{name.ValueOrNull()}' JsonConverter: {jsonConverter.GetType().GetTypeShortName()}"); + + return data.valueJsonElement.Deserialize(type, this); + } + var converter = Converters.GetConverter(type); if (converter == null) throw new ArgumentException($"[Error] Type '{type?.GetTypeId().ValueOrNull()}' not supported for deserialization."); @@ -350,8 +374,6 @@ public SerializedMember Serialize( /// /// The type to analyze for serializable fields. /// BindingFlags controlling which fields are considered (public, private, static, instance, etc.). Default includes public and non-public instance fields. - /// The current depth level in the object hierarchy, used for error message indentation. Default is 0. - /// Optional StringBuilder to accumulate error messages and status information. A new one is created if not provided. /// Optional logger for tracing field discovery operations. /// An enumerable of FieldInfo objects representing serializable fields, or null if no deserializer supports the type. public IEnumerable? GetSerializableFields( @@ -381,8 +403,6 @@ public SerializedMember Serialize( /// /// The type to analyze for serializable properties. /// BindingFlags controlling which properties are considered (public, private, static, instance, etc.). Default includes public and non-public instance properties. - /// The current depth level in the object hierarchy, used for error message indentation. Default is 0. - /// Optional StringBuilder to accumulate error messages and status information. A new one is created if not provided. /// Optional logger for tracing property discovery operations. /// An enumerable of PropertyInfo objects representing serializable properties, or null if no deserializer supports the type. public IEnumerable? GetSerializableProperties( diff --git a/ReflectorNet/src/Utils/Json/JsonSchema.cs b/ReflectorNet/src/Utils/Json/JsonSchema.cs index 4bd12f4..c541354 100644 --- a/ReflectorNet/src/Utils/Json/JsonSchema.cs +++ b/ReflectorNet/src/Utils/Json/JsonSchema.cs @@ -10,6 +10,7 @@ using System.Linq; using System.Reflection; using System.Text.Json.Nodes; +using System.Text.Json.Schema; using System.Threading.Tasks; using com.IvanMurzak.ReflectorNet.Json; @@ -44,6 +45,11 @@ namespace com.IvanMurzak.ReflectorNet.Utils /// public partial class JsonSchema { + readonly JsonSchemaExporterOptions jsonSchemaExporterOptions = new JsonSchemaExporterOptions + { + TreatNullObliviousAsNonNullable = false + }; + /// /// Generates a comprehensive JSON Schema representation for the specified generic type. /// This method provides flexible schema generation supporting both full schema definitions diff --git a/ReflectorNet/src/Utils/Json/JsonSerializer.cs b/ReflectorNet/src/Utils/Json/JsonSerializer.cs index 1191009..ab4ab28 100644 --- a/ReflectorNet/src/Utils/Json/JsonSerializer.cs +++ b/ReflectorNet/src/Utils/Json/JsonSerializer.cs @@ -169,6 +169,19 @@ public void AddConverter(JsonConverter converter) jsonSerializerOptions.Converters.Add(converter); } + /// + /// Retrieves the first registered JsonConverter that can handle the specified type. + /// + public JsonConverter? GetJsonConverter(Type type) + { + foreach (var converter in jsonSerializerOptions.Converters) + { + if (converter.CanConvert(type)) + return converter; + } + return null; + } + /// /// Removes all custom converters from the serializer, reverting to default .NET JSON serialization behavior. /// This method is useful for scenarios where you need to reset the serializer to a clean state or