From a1a4d98bf577bec91e97ac10f0f5cbfa01beb75c Mon Sep 17 00:00:00 2001 From: Ivan Murzak Date: Wed, 24 Dec 2025 02:07:49 -0800 Subject: [PATCH 01/23] test: Add tests for serialization of non-serialized interface fields --- .../NonSerializedInterfaceTests.cs | 46 +++++++++++++++++++ 1 file changed, 46 insertions(+) create mode 100644 ReflectorNet.Tests/src/ReflectorTests/NonSerializedInterfaceTests.cs diff --git a/ReflectorNet.Tests/src/ReflectorTests/NonSerializedInterfaceTests.cs b/ReflectorNet.Tests/src/ReflectorTests/NonSerializedInterfaceTests.cs new file mode 100644 index 00000000..721e61fe --- /dev/null +++ b/ReflectorNet.Tests/src/ReflectorTests/NonSerializedInterfaceTests.cs @@ -0,0 +1,46 @@ +using System; +using Xunit; +using Xunit.Abstractions; +using com.IvanMurzak.ReflectorNet.Utils; + +namespace com.IvanMurzak.ReflectorNet.Tests.ReflectorTests +{ + public interface ICharacterController + { + void BeforeCharacterUpdate(float deltaTime); + void PostGroundingUpdate(float deltaTime); + void AfterCharacterUpdate(float deltaTime); + } + + public class ClassWithNonSerializedInterface + { + public string Name; + + [System.NonSerialized] + public ICharacterController CharacterController; + } + + public class NonSerializedInterfaceTests : BaseTest + { + public NonSerializedInterfaceTests(ITestOutputHelper output) : base(output) { } + + [Fact] + public void TestNonSerializedInterfaceField() + { + var instance = new ClassWithNonSerializedInterface + { + Name = "TestInstance", + CharacterController = null // Or mock it if needed, but null should be fine for serialization if it's ignored + }; + + var reflector = new Reflector(); + + // This should not throw + var serialized = reflector.Serialize(instance); + + Assert.NotNull(serialized); + // Verify that the field is NOT present in the serialized data if it's NonSerialized + // Or if ReflectorNet handles NonSerialized by ignoring it. + } + } +} From cee33d78bb39d3ff939cc9f1243c78684a0389d2 Mon Sep 17 00:00:00 2001 From: Ivan Murzak Date: Wed, 24 Dec 2025 02:19:27 -0800 Subject: [PATCH 02/23] feat: Enhance serialization handling by excluding NonSerialized attributes --- .../NonSerializedInterfaceTests.cs | 25 +++++++++++++++++++ .../Base/BaseReflectionConverter.cs | 10 ++++++-- 2 files changed, 33 insertions(+), 2 deletions(-) diff --git a/ReflectorNet.Tests/src/ReflectorTests/NonSerializedInterfaceTests.cs b/ReflectorNet.Tests/src/ReflectorTests/NonSerializedInterfaceTests.cs index 721e61fe..56010044 100644 --- a/ReflectorNet.Tests/src/ReflectorTests/NonSerializedInterfaceTests.cs +++ b/ReflectorNet.Tests/src/ReflectorTests/NonSerializedInterfaceTests.cs @@ -20,6 +20,14 @@ public class ClassWithNonSerializedInterface public ICharacterController CharacterController; } + public class ClassWithNonSerializedProperty + { + public string Name; + + [field: System.NonSerialized] + public ICharacterController CharacterController { get; set; } + } + public class NonSerializedInterfaceTests : BaseTest { public NonSerializedInterfaceTests(ITestOutputHelper output) : base(output) { } @@ -42,5 +50,22 @@ public void TestNonSerializedInterfaceField() // Verify that the field is NOT present in the serialized data if it's NonSerialized // Or if ReflectorNet handles NonSerialized by ignoring it. } + + [Fact] + public void TestNonSerializedInterfaceProperty() + { + var instance = new ClassWithNonSerializedProperty + { + Name = "TestInstance", + CharacterController = null + }; + + var reflector = new Reflector(); + + // This should not throw + var serialized = reflector.Serialize(instance); + + Assert.NotNull(serialized); + } } } diff --git a/ReflectorNet/src/Converter/Reflection/Base/BaseReflectionConverter.cs b/ReflectorNet/src/Converter/Reflection/Base/BaseReflectionConverter.cs index fb508c3b..2bf04a82 100644 --- a/ReflectorNet/src/Converter/Reflection/Base/BaseReflectionConverter.cs +++ b/ReflectorNet/src/Converter/Reflection/Base/BaseReflectionConverter.cs @@ -108,9 +108,12 @@ public virtual int SerializationPriority(Type type, ILogger? logger = null) Type objType, BindingFlags flags = BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.Instance, ILogger? logger = null) - => objType.GetFields(flags) + { + return objType.GetFields(flags) .Where(field => field.GetCustomAttribute() == null) + .Where(field => field.GetCustomAttribute() == null) .Where(field => field.IsPublic); + } /// /// Gets the serializable properties for the specified type. @@ -122,9 +125,12 @@ public virtual int SerializationPriority(Type type, ILogger? logger = null) Type objType, BindingFlags flags = BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.Instance, ILogger? logger = null) - => objType.GetProperties(flags) + { + return objType.GetProperties(flags) .Where(prop => prop.GetCustomAttribute() == null) + .Where(prop => prop.GetCustomAttribute() == null) .Where(prop => prop.CanRead); + } public virtual IEnumerable GetAdditionalSerializableFields( Reflector reflector, From 390debdda2c18647a1cb4d06f1b530686819db35 Mon Sep 17 00:00:00 2001 From: Ivan Murzak Date: Wed, 24 Dec 2025 03:36:09 -0800 Subject: [PATCH 03/23] feat: Improve serialization robustness by handling inaccessible fields and updating non-serialized property checks --- .../NonSerializedInterfaceTests.cs | 194 +++++++++++++++++- .../Base/BaseReflectionConverter.Serialize.cs | 49 +++-- .../Base/BaseReflectionConverter.cs | 1 - 3 files changed, 217 insertions(+), 27 deletions(-) diff --git a/ReflectorNet.Tests/src/ReflectorTests/NonSerializedInterfaceTests.cs b/ReflectorNet.Tests/src/ReflectorTests/NonSerializedInterfaceTests.cs index 56010044..c8958b5d 100644 --- a/ReflectorNet.Tests/src/ReflectorTests/NonSerializedInterfaceTests.cs +++ b/ReflectorNet.Tests/src/ReflectorTests/NonSerializedInterfaceTests.cs @@ -1,7 +1,5 @@ -using System; using Xunit; using Xunit.Abstractions; -using com.IvanMurzak.ReflectorNet.Utils; namespace com.IvanMurzak.ReflectorNet.Tests.ReflectorTests { @@ -12,20 +10,53 @@ public interface ICharacterController void AfterCharacterUpdate(float deltaTime); } + public class MockCharacterController : ICharacterController + { + public int Id { get; set; } = 1; + public float Speed { get; set; } = 10.0f; + + public void BeforeCharacterUpdate(float deltaTime) { } + public void PostGroundingUpdate(float deltaTime) { } + public void AfterCharacterUpdate(float deltaTime) { } + } + public class ClassWithNonSerializedInterface { - public string Name; + public string? Name; [System.NonSerialized] - public ICharacterController CharacterController; + public ICharacterController? CharacterController; } public class ClassWithNonSerializedProperty { - public string Name; + public string? Name; + + [field: System.NonSerialized] + public string? Description { get; set; } + } + public class BaseClassWithNonSerializedProperty + { [field: System.NonSerialized] - public ICharacterController CharacterController { get; set; } + public string? BaseDescription { get; set; } + } + + public class DerivedClass : BaseClassWithNonSerializedProperty + { + public string? DerivedName; + } + + public class ClassWithInterfaceField + { + public string? Name; + public ICharacterController? CharacterController; + } + + public class ClassWithInterfaceProperty + { + public string? Name; + public ICharacterController? CharacterController { get; set; } } public class NonSerializedInterfaceTests : BaseTest @@ -47,8 +78,33 @@ public void TestNonSerializedInterfaceField() var serialized = reflector.Serialize(instance); Assert.NotNull(serialized); - // Verify that the field is NOT present in the serialized data if it's NonSerialized - // Or if ReflectorNet handles NonSerialized by ignoring it. + + var deserialized = reflector.Deserialize(serialized); + Assert.NotNull(deserialized); + Assert.Equal("TestInstance", deserialized.Name); + Assert.Null(deserialized.CharacterController); + } + + [Fact] + public void TestNonSerializedInterfaceField_NotNull() + { + var instance = new ClassWithNonSerializedInterface + { + Name = "TestInstance", + CharacterController = new MockCharacterController() + }; + + var reflector = new Reflector(); + + // This should not throw + var serialized = reflector.Serialize(instance); + + Assert.NotNull(serialized); + + var deserialized = reflector.Deserialize(serialized); + Assert.NotNull(deserialized); + Assert.Equal("TestInstance", deserialized.Name); + Assert.Null(deserialized.CharacterController); } [Fact] @@ -57,7 +113,7 @@ public void TestNonSerializedInterfaceProperty() var instance = new ClassWithNonSerializedProperty { Name = "TestInstance", - CharacterController = null + Description = "ShouldBeSerialized" }; var reflector = new Reflector(); @@ -66,6 +122,126 @@ public void TestNonSerializedInterfaceProperty() var serialized = reflector.Serialize(instance); Assert.NotNull(serialized); + + var deserialized = reflector.Deserialize(serialized); + Assert.NotNull(deserialized); + Assert.Equal("TestInstance", deserialized.Name); + Assert.Equal("ShouldBeSerialized", deserialized.Description); + } + + + [Fact] + public void TestNonSerializedInheritedProperty() + { + var instance = new DerivedClass + { + DerivedName = "Derived", + BaseDescription = "BaseShouldBeSerialized" + }; + + var reflector = new Reflector(); + + // This should not throw + var serialized = reflector.Serialize(instance); + + Assert.NotNull(serialized); + + var deserialized = reflector.Deserialize(serialized); + Assert.NotNull(deserialized); + Assert.Equal("Derived", deserialized.DerivedName); + Assert.Equal("BaseShouldBeSerialized", deserialized.BaseDescription); + } + + [Fact] + public void TestInterfaceField_Null_NoAttribute() + { + var instance = new ClassWithInterfaceField + { + Name = "TestInstance", + CharacterController = null + }; + + var reflector = new Reflector(); + + // Fields now swallow exceptions during serialization (like properties), so this should NOT throw + var serialized = reflector.Serialize(instance); + Assert.NotNull(serialized); + + var deserialized = reflector.Deserialize(serialized); + Assert.NotNull(deserialized); + Assert.Equal("TestInstance", deserialized.Name); + Assert.Null(deserialized.CharacterController); + } + + [Fact] + public void TestInterfaceField_NotNull_NoAttribute() + { + var instance = new ClassWithInterfaceField + { + Name = "TestInstance", + CharacterController = new MockCharacterController { Id = 99, Speed = 123.45f } + }; + + var reflector = new Reflector(); + + var serialized = reflector.Serialize(instance); + Assert.NotNull(serialized); + + var deserialized = reflector.Deserialize(serialized); + Assert.NotNull(deserialized); + Assert.Equal("TestInstance", deserialized.Name); + Assert.NotNull(deserialized.CharacterController); + Assert.IsType(deserialized.CharacterController); + + var controller = (MockCharacterController)deserialized.CharacterController; + Assert.Equal(99, controller.Id); + Assert.Equal(123.45f, controller.Speed); + } + + [Fact] + public void TestInterfaceProperty_Null_NoAttribute() + { + var instance = new ClassWithInterfaceProperty + { + Name = "TestInstance", + CharacterController = null + }; + + var reflector = new Reflector(); + + // Properties swallow exceptions during serialization, so this should NOT throw + var serialized = reflector.Serialize(instance); + Assert.NotNull(serialized); + + var deserialized = reflector.Deserialize(serialized); + Assert.NotNull(deserialized); + Assert.Equal("TestInstance", deserialized.Name); + Assert.Null(deserialized.CharacterController); + } + + [Fact] + public void TestInterfaceProperty_NotNull_NoAttribute() + { + var instance = new ClassWithInterfaceProperty + { + Name = "TestInstance", + CharacterController = new MockCharacterController { Id = 88, Speed = 67.89f } + }; + + var reflector = new Reflector(); + + var serialized = reflector.Serialize(instance); + Assert.NotNull(serialized); + + var deserialized = reflector.Deserialize(serialized); + Assert.NotNull(deserialized); + Assert.Equal("TestInstance", deserialized.Name); + Assert.NotNull(deserialized.CharacterController); + Assert.IsType(deserialized.CharacterController); + + var controller = (MockCharacterController)deserialized.CharacterController; + Assert.Equal(88, controller.Id); + Assert.Equal(67.89f, controller.Speed); } } } diff --git a/ReflectorNet/src/Converter/Reflection/Base/BaseReflectionConverter.Serialize.cs b/ReflectorNet/src/Converter/Reflection/Base/BaseReflectionConverter.Serialize.cs index 1682ca5d..4bdbbad3 100644 --- a/ReflectorNet/src/Converter/Reflection/Base/BaseReflectionConverter.Serialize.cs +++ b/ReflectorNet/src/Converter/Reflection/Base/BaseReflectionConverter.Serialize.cs @@ -62,20 +62,29 @@ public virtual SerializedMember Serialize( { if (GetIgnoredFields().Contains(field.Name)) continue; + try + { + var value = field.GetValue(obj); + var fieldType = field.FieldType; - var value = field.GetValue(obj); - var fieldType = field.FieldType; + var serialized = reflector.Serialize(value, fieldType, + name: field.Name, + recursive: AllowCascadeFieldsConversion, + flags: flags, + depth: depth + 1, + logs: logs, + logger: logger, + context: context); - serializedFields ??= new SerializedMemberList(); - serializedFields.Add(reflector.Serialize(value, fieldType, - name: field.Name, - recursive: AllowCascadeFieldsConversion, - flags: flags, - depth: depth + 1, - logs: logs, - logger: logger, - context: context) - ); + serializedFields ??= new SerializedMemberList(); + serializedFields.Add(serialized); + } + catch (Exception ex) + { + /* skip inaccessible fields */ + logger?.LogWarning(ex, "Failed to serialize field '{fieldName}' of type '{type}'. Exception: {exMessage}", + field.Name, objType.GetTypeId(), ex.Message); + } } return serializedFields; } @@ -105,18 +114,24 @@ public virtual SerializedMember Serialize( var value = prop.GetValue(obj); var propType = prop.PropertyType; - serializedProperties ??= new SerializedMemberList(); - serializedProperties.Add(reflector.Serialize(value, propType, + var serialized = reflector.Serialize(value, propType, name: prop.Name, recursive: AllowCascadePropertiesConversion, flags: flags, depth: depth + 1, logs: logs, logger: logger, - context: context) - ); + context: context); + + serializedProperties ??= new SerializedMemberList(); + serializedProperties.Add(serialized); + } + catch (Exception ex) + { + /* skip inaccessible properties */ + logger?.LogWarning(ex, "Failed to serialize property '{propertyName}' of type '{type}'. Exception: {exMessage}", + prop.Name, objType.GetTypeId(), ex.Message); } - catch { /* skip inaccessible properties */ } } return serializedProperties; } diff --git a/ReflectorNet/src/Converter/Reflection/Base/BaseReflectionConverter.cs b/ReflectorNet/src/Converter/Reflection/Base/BaseReflectionConverter.cs index 2bf04a82..780ee292 100644 --- a/ReflectorNet/src/Converter/Reflection/Base/BaseReflectionConverter.cs +++ b/ReflectorNet/src/Converter/Reflection/Base/BaseReflectionConverter.cs @@ -128,7 +128,6 @@ public virtual int SerializationPriority(Type type, ILogger? logger = null) { return objType.GetProperties(flags) .Where(prop => prop.GetCustomAttribute() == null) - .Where(prop => prop.GetCustomAttribute() == null) .Where(prop => prop.CanRead); } From 06ff3c078d374552fdd05f4d906ccf8ad0f5b8bc Mon Sep 17 00:00:00 2001 From: Ivan Murzak Date: Wed, 24 Dec 2025 16:53:40 -0800 Subject: [PATCH 04/23] feat: Refactor serialization error handling and add utility for retrieving deepest inner exceptions --- .../NonSerializedInterfaceTests.cs | 4 +-- .../Base/BaseReflectionConverter.Serialize.cs | 26 +++++++++++++------ .../src/Extension/ExtensionsException.cs | 24 +++++++++++++++++ 3 files changed, 44 insertions(+), 10 deletions(-) create mode 100644 ReflectorNet/src/Extension/ExtensionsException.cs diff --git a/ReflectorNet.Tests/src/ReflectorTests/NonSerializedInterfaceTests.cs b/ReflectorNet.Tests/src/ReflectorTests/NonSerializedInterfaceTests.cs index c8958b5d..f97897e4 100644 --- a/ReflectorNet.Tests/src/ReflectorTests/NonSerializedInterfaceTests.cs +++ b/ReflectorNet.Tests/src/ReflectorTests/NonSerializedInterfaceTests.cs @@ -36,13 +36,13 @@ public class ClassWithNonSerializedProperty public string? Description { get; set; } } - public class BaseClassWithNonSerializedProperty + public class BaseClassWithPropertyHavingNonSerializedBackingField { [field: System.NonSerialized] public string? BaseDescription { get; set; } } - public class DerivedClass : BaseClassWithNonSerializedProperty + public class DerivedClass : BaseClassWithPropertyHavingNonSerializedBackingField { public string? DerivedName; } diff --git a/ReflectorNet/src/Converter/Reflection/Base/BaseReflectionConverter.Serialize.cs b/ReflectorNet/src/Converter/Reflection/Base/BaseReflectionConverter.Serialize.cs index 4bdbbad3..f2264b29 100644 --- a/ReflectorNet/src/Converter/Reflection/Base/BaseReflectionConverter.Serialize.cs +++ b/ReflectorNet/src/Converter/Reflection/Base/BaseReflectionConverter.Serialize.cs @@ -10,7 +10,9 @@ using System.Linq; using System.Reflection; using com.IvanMurzak.ReflectorNet.Model; +using com.IvanMurzak.ReflectorNet; using Microsoft.Extensions.Logging; +using com.IvanMurzak.ReflectorNet.Utils; namespace com.IvanMurzak.ReflectorNet.Converter { @@ -62,9 +64,11 @@ public virtual SerializedMember Serialize( { if (GetIgnoredFields().Contains(field.Name)) continue; + + object? value = null; try { - var value = field.GetValue(obj); + value = field.GetValue(obj); var fieldType = field.FieldType; var serialized = reflector.Serialize(value, fieldType, @@ -81,9 +85,11 @@ public virtual SerializedMember Serialize( } catch (Exception ex) { - /* skip inaccessible fields */ - logger?.LogWarning(ex, "Failed to serialize field '{fieldName}' of type '{type}'. Exception: {exMessage}", - field.Name, objType.GetTypeId(), ex.Message); + // skip inaccessible field + logger?.LogWarning(ex.GetDeepestInnerException(), "Failed to serialize field '{fieldName}' of type '{type}' in '{objType}'. Path: {path}", + field.Name, field.FieldType.GetTypeId(), objType.GetTypeId(), value == null + ? StringUtils.Null + : context?.GetPath(value) ?? StringUtils.Null); } } return serializedFields; @@ -109,9 +115,11 @@ public virtual SerializedMember Serialize( { if (GetIgnoredProperties().Contains(prop.Name)) continue; + + object? value = null; try { - var value = prop.GetValue(obj); + value = prop.GetValue(obj); var propType = prop.PropertyType; var serialized = reflector.Serialize(value, propType, @@ -128,9 +136,11 @@ public virtual SerializedMember Serialize( } catch (Exception ex) { - /* skip inaccessible properties */ - logger?.LogWarning(ex, "Failed to serialize property '{propertyName}' of type '{type}'. Exception: {exMessage}", - prop.Name, objType.GetTypeId(), ex.Message); + // skip inaccessible property + logger?.LogWarning(ex.GetDeepestInnerException(), "Failed to serialize property '{propertyName}' of type '{type}' in '{objType}'. Path: {path}", + prop.Name, prop.PropertyType.GetTypeId(), objType.GetTypeId(), value == null + ? StringUtils.Null + : context?.GetPath(value) ?? StringUtils.Null); } } return serializedProperties; diff --git a/ReflectorNet/src/Extension/ExtensionsException.cs b/ReflectorNet/src/Extension/ExtensionsException.cs new file mode 100644 index 00000000..4e6b8633 --- /dev/null +++ b/ReflectorNet/src/Extension/ExtensionsException.cs @@ -0,0 +1,24 @@ +/* + * 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; + +namespace com.IvanMurzak.ReflectorNet +{ + public static class ExtensionsException + { + /// + /// Recursively retrieves the deepest inner exception. + /// + /// The exception to inspect. + /// The deepest inner exception, or the exception itself if no inner exception exists. + public static Exception GetDeepestInnerException(this Exception ex) + { + return ex.InnerException == null ? ex : ex.InnerException.GetDeepestInnerException(); + } + } +} From 4d1ed5fc6a2fe8afdfb256004c176de89a2cb4d1 Mon Sep 17 00:00:00 2001 From: Ivan Murzak Date: Wed, 24 Dec 2025 16:57:35 -0800 Subject: [PATCH 05/23] refactor: Clean up using directives and improve readability of GetDeepestInnerException method --- .../Reflection/Base/BaseReflectionConverter.Serialize.cs | 1 - ReflectorNet/src/Extension/ExtensionsException.cs | 4 +++- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/ReflectorNet/src/Converter/Reflection/Base/BaseReflectionConverter.Serialize.cs b/ReflectorNet/src/Converter/Reflection/Base/BaseReflectionConverter.Serialize.cs index f2264b29..527205c1 100644 --- a/ReflectorNet/src/Converter/Reflection/Base/BaseReflectionConverter.Serialize.cs +++ b/ReflectorNet/src/Converter/Reflection/Base/BaseReflectionConverter.Serialize.cs @@ -10,7 +10,6 @@ using System.Linq; using System.Reflection; using com.IvanMurzak.ReflectorNet.Model; -using com.IvanMurzak.ReflectorNet; using Microsoft.Extensions.Logging; using com.IvanMurzak.ReflectorNet.Utils; diff --git a/ReflectorNet/src/Extension/ExtensionsException.cs b/ReflectorNet/src/Extension/ExtensionsException.cs index 4e6b8633..f5e8fb61 100644 --- a/ReflectorNet/src/Extension/ExtensionsException.cs +++ b/ReflectorNet/src/Extension/ExtensionsException.cs @@ -18,7 +18,9 @@ public static class ExtensionsException /// The deepest inner exception, or the exception itself if no inner exception exists. public static Exception GetDeepestInnerException(this Exception ex) { - return ex.InnerException == null ? ex : ex.InnerException.GetDeepestInnerException(); + return ex.InnerException == null + ? ex + : ex.InnerException.GetDeepestInnerException(); } } } From 75553e6179bec62cfad2315e5e0828c0d1f420b9 Mon Sep 17 00:00:00 2001 From: Ivan Murzak Date: Wed, 24 Dec 2025 16:58:33 -0800 Subject: [PATCH 06/23] fix: Update serialization error logging to use GetBaseException and remove ExtensionsException class --- .../Base/BaseReflectionConverter.Serialize.cs | 4 +-- .../src/Extension/ExtensionsException.cs | 26 ------------------- 2 files changed, 2 insertions(+), 28 deletions(-) delete mode 100644 ReflectorNet/src/Extension/ExtensionsException.cs diff --git a/ReflectorNet/src/Converter/Reflection/Base/BaseReflectionConverter.Serialize.cs b/ReflectorNet/src/Converter/Reflection/Base/BaseReflectionConverter.Serialize.cs index 527205c1..89f4dc66 100644 --- a/ReflectorNet/src/Converter/Reflection/Base/BaseReflectionConverter.Serialize.cs +++ b/ReflectorNet/src/Converter/Reflection/Base/BaseReflectionConverter.Serialize.cs @@ -85,7 +85,7 @@ public virtual SerializedMember Serialize( catch (Exception ex) { // skip inaccessible field - logger?.LogWarning(ex.GetDeepestInnerException(), "Failed to serialize field '{fieldName}' of type '{type}' in '{objType}'. Path: {path}", + logger?.LogWarning(ex.GetBaseException(), "Failed to serialize field '{fieldName}' of type '{type}' in '{objType}'. Path: {path}", field.Name, field.FieldType.GetTypeId(), objType.GetTypeId(), value == null ? StringUtils.Null : context?.GetPath(value) ?? StringUtils.Null); @@ -136,7 +136,7 @@ public virtual SerializedMember Serialize( catch (Exception ex) { // skip inaccessible property - logger?.LogWarning(ex.GetDeepestInnerException(), "Failed to serialize property '{propertyName}' of type '{type}' in '{objType}'. Path: {path}", + logger?.LogWarning(ex.GetBaseException(), "Failed to serialize property '{propertyName}' of type '{type}' in '{objType}'. Path: {path}", prop.Name, prop.PropertyType.GetTypeId(), objType.GetTypeId(), value == null ? StringUtils.Null : context?.GetPath(value) ?? StringUtils.Null); diff --git a/ReflectorNet/src/Extension/ExtensionsException.cs b/ReflectorNet/src/Extension/ExtensionsException.cs deleted file mode 100644 index f5e8fb61..00000000 --- a/ReflectorNet/src/Extension/ExtensionsException.cs +++ /dev/null @@ -1,26 +0,0 @@ -/* - * 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; - -namespace com.IvanMurzak.ReflectorNet -{ - public static class ExtensionsException - { - /// - /// Recursively retrieves the deepest inner exception. - /// - /// The exception to inspect. - /// The deepest inner exception, or the exception itself if no inner exception exists. - public static Exception GetDeepestInnerException(this Exception ex) - { - return ex.InnerException == null - ? ex - : ex.InnerException.GetDeepestInnerException(); - } - } -} From 956cdbce125a2f761b4abff296a0f2f7ee9d0d19 Mon Sep 17 00:00:00 2001 From: Ivan Murzak Date: Wed, 24 Dec 2025 17:13:51 -0800 Subject: [PATCH 07/23] fix: Simplify variable declaration and improve logging for inaccessible fields in serialization --- .../Base/BaseReflectionConverter.Serialize.cs | 16 ++++------------ 1 file changed, 4 insertions(+), 12 deletions(-) diff --git a/ReflectorNet/src/Converter/Reflection/Base/BaseReflectionConverter.Serialize.cs b/ReflectorNet/src/Converter/Reflection/Base/BaseReflectionConverter.Serialize.cs index 89f4dc66..0f692d19 100644 --- a/ReflectorNet/src/Converter/Reflection/Base/BaseReflectionConverter.Serialize.cs +++ b/ReflectorNet/src/Converter/Reflection/Base/BaseReflectionConverter.Serialize.cs @@ -63,11 +63,9 @@ public virtual SerializedMember Serialize( { if (GetIgnoredFields().Contains(field.Name)) continue; - - object? value = null; try { - value = field.GetValue(obj); + var value = field.GetValue(obj); var fieldType = field.FieldType; var serialized = reflector.Serialize(value, fieldType, @@ -86,9 +84,7 @@ public virtual SerializedMember Serialize( { // skip inaccessible field logger?.LogWarning(ex.GetBaseException(), "Failed to serialize field '{fieldName}' of type '{type}' in '{objType}'. Path: {path}", - field.Name, field.FieldType.GetTypeId(), objType.GetTypeId(), value == null - ? StringUtils.Null - : context?.GetPath(value) ?? StringUtils.Null); + field.Name, field.FieldType.GetTypeId(), objType.GetTypeId(), context?.GetPath(obj)); } } return serializedFields; @@ -114,11 +110,9 @@ public virtual SerializedMember Serialize( { if (GetIgnoredProperties().Contains(prop.Name)) continue; - - object? value = null; try { - value = prop.GetValue(obj); + var value = prop.GetValue(obj); var propType = prop.PropertyType; var serialized = reflector.Serialize(value, propType, @@ -137,9 +131,7 @@ public virtual SerializedMember Serialize( { // skip inaccessible property logger?.LogWarning(ex.GetBaseException(), "Failed to serialize property '{propertyName}' of type '{type}' in '{objType}'. Path: {path}", - prop.Name, prop.PropertyType.GetTypeId(), objType.GetTypeId(), value == null - ? StringUtils.Null - : context?.GetPath(value) ?? StringUtils.Null); + prop.Name, prop.PropertyType.GetTypeId(), objType.GetTypeId(), context?.GetPath(obj)); } } return serializedProperties; From 1ed35268d7f7a0acf840910b6dad1d51c551824f Mon Sep 17 00:00:00 2001 From: Ivan Murzak Date: Fri, 26 Dec 2025 01:33:40 -0800 Subject: [PATCH 08/23] Add custom JSON converters for various types - Implemented BigIntegerJsonConverter for handling System.Numerics.BigInteger. - Added CharJsonConverter for char types, supporting nullable and single-character strings. - Created ComplexJsonConverter for System.Numerics.Complex type serialization. - Introduced ConstructorInfoConverter for System.Reflection.ConstructorInfo. - Added DateOnlyJsonConverter for System.DateOnly type. - Implemented ExceptionJsonConverter for System.Exception serialization. - Created FieldInfoConverter for System.Reflection.FieldInfo. - Added HalfJsonConverter for System.Half type. - Implemented IPAddressJsonConverter for System.Net.IPAddress. - Created IPEndPointJsonConverter for System.Net.IPEndPoint. - Added IntPtrJsonConverter for System.IntPtr. - Implemented JsonNodeConverter for JsonNode instances. - Created JsonValueJsonConverter for JsonValue instances. - Added ParameterInfoConverter for System.Reflection.ParameterInfo. - Implemented PropertyInfoConverter for System.Reflection.PropertyInfo. - Added TimeOnlyJsonConverter for System.TimeOnly type. - Implemented UIntPtrJsonConverter for System.UIntPtr. - Created UriJsonConverter for System.Uri. - Added VersionJsonConverter for System.Version. - Updated JsonSerializer to include new converters. --- .../src/SchemaTests/ErrorHandlingTests.cs | 4 +- .../Converter/Json/AssemblyJsonConverter.cs | 54 +++++++++ .../Converter/Json/BigIntegerJsonConverter.cs | 63 ++++++++++ .../src/Converter/Json/CharJsonConverter.cs | 67 +++++++++++ .../Converter/Json/ComplexJsonConverter.cs | 94 +++++++++++++++ .../Json/ConstructorInfoConverter.cs | 83 +++++++++++++ .../Converter/Json/DateOnlyJsonConverter.cs | 37 ++++++ .../Converter/Json/ExceptionJsonConverter.cs | 79 +++++++++++++ .../src/Converter/Json/FieldInfoConverter.cs | 63 ++++++++++ .../src/Converter/Json/HalfJsonConverter.cs | 58 ++++++++++ .../Converter/Json/IPAddressJsonConverter.cs | 63 ++++++++++ .../Converter/Json/IPEndPointJsonConverter.cs | 109 ++++++++++++++++++ .../src/Converter/Json/IntPtrJsonConverter.cs | 56 +++++++++ .../src/Converter/Json/JsonNodeConverter.cs | 42 +++++++ .../Converter/Json/JsonValueJsonConverter.cs | 33 ++++++ .../Converter/Json/ParameterInfoConverter.cs | 83 +++++++++++++ .../Converter/Json/PropertyInfoConverter.cs | 63 ++++++++++ .../Converter/Json/TimeOnlyJsonConverter.cs | 37 ++++++ .../Converter/Json/UIntPtrJsonConverter.cs | 56 +++++++++ .../src/Converter/Json/UriJsonConverter.cs | 42 +++++++ .../Converter/Json/VersionJsonConverter.cs | 43 +++++++ ReflectorNet/src/Utils/Json/JsonSerializer.cs | 28 ++++- 22 files changed, 1254 insertions(+), 3 deletions(-) create mode 100644 ReflectorNet/src/Converter/Json/AssemblyJsonConverter.cs create mode 100644 ReflectorNet/src/Converter/Json/BigIntegerJsonConverter.cs create mode 100644 ReflectorNet/src/Converter/Json/CharJsonConverter.cs create mode 100644 ReflectorNet/src/Converter/Json/ComplexJsonConverter.cs create mode 100644 ReflectorNet/src/Converter/Json/ConstructorInfoConverter.cs create mode 100644 ReflectorNet/src/Converter/Json/DateOnlyJsonConverter.cs create mode 100644 ReflectorNet/src/Converter/Json/ExceptionJsonConverter.cs create mode 100644 ReflectorNet/src/Converter/Json/FieldInfoConverter.cs create mode 100644 ReflectorNet/src/Converter/Json/HalfJsonConverter.cs create mode 100644 ReflectorNet/src/Converter/Json/IPAddressJsonConverter.cs create mode 100644 ReflectorNet/src/Converter/Json/IPEndPointJsonConverter.cs create mode 100644 ReflectorNet/src/Converter/Json/IntPtrJsonConverter.cs create mode 100644 ReflectorNet/src/Converter/Json/JsonNodeConverter.cs create mode 100644 ReflectorNet/src/Converter/Json/JsonValueJsonConverter.cs create mode 100644 ReflectorNet/src/Converter/Json/ParameterInfoConverter.cs create mode 100644 ReflectorNet/src/Converter/Json/PropertyInfoConverter.cs create mode 100644 ReflectorNet/src/Converter/Json/TimeOnlyJsonConverter.cs create mode 100644 ReflectorNet/src/Converter/Json/UIntPtrJsonConverter.cs create mode 100644 ReflectorNet/src/Converter/Json/UriJsonConverter.cs create mode 100644 ReflectorNet/src/Converter/Json/VersionJsonConverter.cs diff --git a/ReflectorNet.Tests/src/SchemaTests/ErrorHandlingTests.cs b/ReflectorNet.Tests/src/SchemaTests/ErrorHandlingTests.cs index 664c0611..17e54712 100644 --- a/ReflectorNet.Tests/src/SchemaTests/ErrorHandlingTests.cs +++ b/ReflectorNet.Tests/src/SchemaTests/ErrorHandlingTests.cs @@ -16,11 +16,11 @@ public void Serialize_UnsupportedType_ThrowsException() { // Arrange var reflector = new Reflector(); - var unsupportedObject = new IntPtr(123); // IntPtr is not typically serializable + var unsupportedType = typeof(int).MakeByRefType(); // Act & Assert var exception = Assert.ThrowsAny(() => - reflector.Serialize(unsupportedObject)); + reflector.Serialize(null, fallbackType: unsupportedType)); Assert.Contains("not supported", exception.Message.ToLowerInvariant()); _output.WriteLine($"Expected exception caught: {exception.Message}"); diff --git a/ReflectorNet/src/Converter/Json/AssemblyJsonConverter.cs b/ReflectorNet/src/Converter/Json/AssemblyJsonConverter.cs new file mode 100644 index 00000000..c4007711 --- /dev/null +++ b/ReflectorNet/src/Converter/Json/AssemblyJsonConverter.cs @@ -0,0 +1,54 @@ +/* + * 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.Linq; +using System.Reflection; +using System.Text.Json; +using System.Text.Json.Serialization; + +namespace com.IvanMurzak.ReflectorNet.Json +{ + /// + /// JsonConverter that handles conversion between JSON strings and System.Reflection.Assembly. + /// + public class AssemblyJsonConverter : JsonConverter + { + public override Assembly? Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) + { + if (reader.TokenType == JsonTokenType.Null) + return null; + + if (reader.TokenType != JsonTokenType.String) + throw new JsonException($"Expected string token for Assembly, but got {reader.TokenType}"); + + var assemblyName = reader.GetString(); + if (string.IsNullOrWhiteSpace(assemblyName)) + return null; + + var assembly = AppDomain.CurrentDomain.GetAssemblies() + .FirstOrDefault(a => a.FullName == assemblyName || a.GetName().Name == assemblyName); + + if (assembly != null) + return assembly; + + try + { + return Assembly.Load(assemblyName); + } + catch + { + throw new JsonException($"Unable to find or load assembly: {assemblyName}"); + } + } + + public override void Write(Utf8JsonWriter writer, Assembly value, JsonSerializerOptions options) + { + writer.WriteStringValue(value.FullName); + } + } +} diff --git a/ReflectorNet/src/Converter/Json/BigIntegerJsonConverter.cs b/ReflectorNet/src/Converter/Json/BigIntegerJsonConverter.cs new file mode 100644 index 00000000..4351df6e --- /dev/null +++ b/ReflectorNet/src/Converter/Json/BigIntegerJsonConverter.cs @@ -0,0 +1,63 @@ +/* + * 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.Numerics; +using System.Text.Json; +using System.Text.Json.Nodes; +using com.IvanMurzak.ReflectorNet.Utils; + +namespace com.IvanMurzak.ReflectorNet.Json +{ + /// + /// JsonConverter that handles conversion between JSON values and System.Numerics.BigInteger. + /// Supports reading from both string and number tokens. + /// + public class BigIntegerJsonConverter : JsonSchemaConverter, IJsonSchemaConverter + { + public static JsonNode Schema => new JsonObject + { + [JsonSchema.Type] = JsonSchema.String, + ["description"] = "A large integer represented as a string" + }; + + public static JsonNode SchemaRef => new JsonObject + { + [JsonSchema.Ref] = JsonSchema.RefValue + StaticId + }; + + public override JsonNode GetSchemaRef() => SchemaRef; + public override JsonNode GetSchema() => Schema; + + public override BigInteger Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) + { + if (reader.TokenType == JsonTokenType.String) + { + var stringValue = reader.GetString(); + if (BigInteger.TryParse(stringValue, out var result)) + { + return result; + } + } + else if (reader.TokenType == JsonTokenType.Number) + { + using var doc = JsonDocument.ParseValue(ref reader); + if (BigInteger.TryParse(doc.RootElement.GetRawText(), out var result)) + { + return result; + } + } + + throw new JsonException($"Expected string or number representing BigInteger."); + } + + public override void Write(Utf8JsonWriter writer, BigInteger value, JsonSerializerOptions options) + { + writer.WriteStringValue(value.ToString()); + } + } +} diff --git a/ReflectorNet/src/Converter/Json/CharJsonConverter.cs b/ReflectorNet/src/Converter/Json/CharJsonConverter.cs new file mode 100644 index 00000000..1cabccef --- /dev/null +++ b/ReflectorNet/src/Converter/Json/CharJsonConverter.cs @@ -0,0 +1,67 @@ +/* + * 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.Text.Json; +using System.Text.Json.Serialization; +using com.IvanMurzak.ReflectorNet.Utils; + +namespace com.IvanMurzak.ReflectorNet.Json +{ + /// + /// JsonConverter that handles conversion between JSON values and char types. + /// Supports nullable char types and handles single-character strings. + /// + public class CharJsonConverter : JsonConverter + { + public override bool CanConvert(Type typeToConvert) + { + var underlyingType = Nullable.GetUnderlyingType(typeToConvert) ?? typeToConvert; + return underlyingType == typeof(char); + } + + public override object? Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) + { + if (reader.TokenType == JsonTokenType.Null) + { + if (Nullable.GetUnderlyingType(typeToConvert) != null) + return null; + + throw new JsonException($"Cannot convert null to non-nullable type '{typeToConvert.GetTypeId()}'."); + } + + if (reader.TokenType == JsonTokenType.String) + { + var stringValue = reader.GetString(); + if (string.IsNullOrEmpty(stringValue)) + { + if (Nullable.GetUnderlyingType(typeToConvert) != null) + return null; + + return default(char); + } + + if (stringValue.Length == 1) + return stringValue[0]; + + throw new JsonException($"String must be exactly one character long to convert to char. Got: '{stringValue}'"); + } + + if (reader.TokenType == JsonTokenType.Number) + { + return (char)reader.GetInt32(); + } + + throw new JsonException($"Expected string or number token but got {reader.TokenType} for type '{typeToConvert.GetTypeId()}'."); + } + + public override void Write(Utf8JsonWriter writer, object value, JsonSerializerOptions options) + { + writer.WriteStringValue(value.ToString()); + } + } +} diff --git a/ReflectorNet/src/Converter/Json/ComplexJsonConverter.cs b/ReflectorNet/src/Converter/Json/ComplexJsonConverter.cs new file mode 100644 index 00000000..18ce518e --- /dev/null +++ b/ReflectorNet/src/Converter/Json/ComplexJsonConverter.cs @@ -0,0 +1,94 @@ +/* + * 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.Numerics; +using System.Text.Json; +using System.Text.Json.Nodes; +using com.IvanMurzak.ReflectorNet.Utils; + +namespace com.IvanMurzak.ReflectorNet.Json +{ + /// + /// JsonConverter that handles conversion between JSON objects and System.Numerics.Complex. + /// + public class ComplexJsonConverter : JsonSchemaConverter, IJsonSchemaConverter + { + private const string RealProperty = "real"; + private const string ImaginaryProperty = "imaginary"; + + public static JsonNode Schema => new JsonObject + { + [JsonSchema.Type] = JsonSchema.Object, + [JsonSchema.Properties] = new JsonObject + { + [RealProperty] = new JsonObject { [JsonSchema.Type] = JsonSchema.Number }, + [ImaginaryProperty] = new JsonObject { [JsonSchema.Type] = JsonSchema.Number } + }, + [JsonSchema.Required] = new JsonArray { RealProperty, ImaginaryProperty } + }; + + public static JsonNode SchemaRef => new JsonObject + { + [JsonSchema.Ref] = JsonSchema.RefValue + StaticId + }; + + public override JsonNode GetSchemaRef() => SchemaRef; + public override JsonNode GetSchema() => Schema; + + public override Complex Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) + { + if (reader.TokenType != JsonTokenType.StartObject) + throw new JsonException("Expected StartObject for Complex."); + + double real = 0; + double imaginary = 0; + bool realSet = false; + bool imaginarySet = false; + + while (reader.Read()) + { + if (reader.TokenType == JsonTokenType.EndObject) + break; + + if (reader.TokenType == JsonTokenType.PropertyName) + { + string propertyName = reader.GetString(); + reader.Read(); + + if (string.Equals(propertyName, RealProperty, StringComparison.OrdinalIgnoreCase)) + { + real = reader.GetDouble(); + realSet = true; + } + else if (string.Equals(propertyName, ImaginaryProperty, StringComparison.OrdinalIgnoreCase)) + { + imaginary = reader.GetDouble(); + imaginarySet = true; + } + else + { + reader.Skip(); + } + } + } + + if (!realSet || !imaginarySet) + throw new JsonException("Complex number requires both 'real' and 'imaginary' properties."); + + return new Complex(real, imaginary); + } + + public override void Write(Utf8JsonWriter writer, Complex value, JsonSerializerOptions options) + { + writer.WriteStartObject(); + writer.WriteNumber(RealProperty, value.Real); + writer.WriteNumber(ImaginaryProperty, value.Imaginary); + writer.WriteEndObject(); + } + } +} diff --git a/ReflectorNet/src/Converter/Json/ConstructorInfoConverter.cs b/ReflectorNet/src/Converter/Json/ConstructorInfoConverter.cs new file mode 100644 index 00000000..ca56e954 --- /dev/null +++ b/ReflectorNet/src/Converter/Json/ConstructorInfoConverter.cs @@ -0,0 +1,83 @@ +/* + * 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.Collections.Generic; +using System.Reflection; +using System.Text.Json; +using System.Text.Json.Serialization; +using com.IvanMurzak.ReflectorNet.Utils; + +namespace com.IvanMurzak.ReflectorNet.Json +{ + /// + /// JsonConverter that handles conversion between JSON objects and System.Reflection.ConstructorInfo. + /// + public class ConstructorInfoConverter : JsonConverter + { + static class Json + { + public const string DeclaringType = "declaringType"; + public const string Parameters = "parameters"; + public const string Type = "type"; + } + + public override ConstructorInfo? Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) + { + if (reader.TokenType == JsonTokenType.Null) + return null; + + using var doc = JsonDocument.ParseValue(ref reader); + var root = doc.RootElement; + + if (!root.TryGetProperty(Json.DeclaringType, out var declaringTypeElement)) + throw new JsonException("ConstructorInfo JSON must contain 'declaringType'."); + + var typeName = declaringTypeElement.GetString(); + var parameterTypes = new List(); + + if (root.TryGetProperty(Json.Parameters, out var parametersElement)) + { + foreach (var param in parametersElement.EnumerateArray()) + { + var paramTypeName = param.GetProperty(Json.Type).GetString(); + var paramType = TypeUtils.GetType(paramTypeName); + if (paramType != null) + parameterTypes.Add(paramType); + } + } + + var declaringType = TypeUtils.GetType(typeName); + if (declaringType == null) + throw new JsonException($"Could not find type: {typeName}"); + + var constructor = declaringType.GetConstructor(parameterTypes.ToArray()); + if (constructor == null) + throw new JsonException($"Could not find constructor on type: {typeName} with specified parameters."); + + return constructor; + } + + public override void Write(Utf8JsonWriter writer, ConstructorInfo value, JsonSerializerOptions options) + { + writer.WriteStartObject(); + writer.WriteString(Json.DeclaringType, value.DeclaringType?.GetTypeId()); + + writer.WritePropertyName(Json.Parameters); + writer.WriteStartArray(); + foreach (var param in value.GetParameters()) + { + writer.WriteStartObject(); + writer.WriteString(Json.Type, param.ParameterType.GetTypeId()); + writer.WriteEndObject(); + } + writer.WriteEndArray(); + + writer.WriteEndObject(); + } + } +} diff --git a/ReflectorNet/src/Converter/Json/DateOnlyJsonConverter.cs b/ReflectorNet/src/Converter/Json/DateOnlyJsonConverter.cs new file mode 100644 index 00000000..f51a4f33 --- /dev/null +++ b/ReflectorNet/src/Converter/Json/DateOnlyJsonConverter.cs @@ -0,0 +1,37 @@ +/* + * 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. + */ + +#if NET6_0_OR_GREATER +using System; +using System.Text.Json; +using System.Text.Json.Serialization; + +namespace com.IvanMurzak.ReflectorNet.Json +{ + /// + /// JsonConverter that handles conversion between JSON strings and System.DateOnly. + /// + public class DateOnlyJsonConverter : JsonConverter + { + private const string Format = "yyyy-MM-dd"; + + public override DateOnly Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) + { + if (reader.TokenType != JsonTokenType.String) + throw new JsonException($"Expected string token for DateOnly, but got {reader.TokenType}"); + + var stringValue = reader.GetString(); + return DateOnly.ParseExact(stringValue!, Format); + } + + public override void Write(Utf8JsonWriter writer, DateOnly value, JsonSerializerOptions options) + { + writer.WriteStringValue(value.ToString(Format)); + } + } +} +#endif diff --git a/ReflectorNet/src/Converter/Json/ExceptionJsonConverter.cs b/ReflectorNet/src/Converter/Json/ExceptionJsonConverter.cs new file mode 100644 index 00000000..18a22892 --- /dev/null +++ b/ReflectorNet/src/Converter/Json/ExceptionJsonConverter.cs @@ -0,0 +1,79 @@ +/* + * 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.Text.Json; +using System.Text.Json.Serialization; +using com.IvanMurzak.ReflectorNet.Utils; + +namespace com.IvanMurzak.ReflectorNet.Json +{ + /// + /// JsonConverter that handles conversion between JSON objects and System.Exception. + /// Captures message, type, and stack trace for serialization. + /// + public class ExceptionJsonConverter : JsonConverter + { + static class Json + { + public const string Message = "message"; + public const string Type = "type"; + public const string StackTrace = "stackTrace"; + public const string InnerException = "innerException"; + } + + public override Exception? Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) + { + if (reader.TokenType == JsonTokenType.Null) + return null; + + using var doc = JsonDocument.ParseValue(ref reader); + var root = doc.RootElement; + + var message = root.TryGetProperty(Json.Message, out var msgEl) ? msgEl.GetString() : string.Empty; + var typeName = root.TryGetProperty(Json.Type, out var typeEl) ? typeEl.GetString() : typeof(Exception).FullName; + + Exception? innerException = null; + if (root.TryGetProperty(Json.InnerException, out var innerEl) && innerEl.ValueKind != JsonValueKind.Null) + { + innerException = System.Text.Json.JsonSerializer.Deserialize(innerEl.GetRawText(), options); + } + + var type = TypeUtils.GetType(typeName); + if (type != null && typeof(Exception).IsAssignableFrom(type)) + { + try + { + return (Exception?)Activator.CreateInstance(type, message, innerException) + ?? new Exception(message, innerException); + } + catch + { + return new Exception(message, innerException); + } + } + + return new Exception(message, innerException); + } + + public override void Write(Utf8JsonWriter writer, Exception value, JsonSerializerOptions options) + { + writer.WriteStartObject(); + writer.WriteString(Json.Type, value.GetType().GetTypeId()); + writer.WriteString(Json.Message, value.Message); + writer.WriteString(Json.StackTrace, value.StackTrace); + + if (value.InnerException != null) + { + writer.WritePropertyName(Json.InnerException); + System.Text.Json.JsonSerializer.Serialize(writer, value.InnerException, options); + } + + writer.WriteEndObject(); + } + } +} diff --git a/ReflectorNet/src/Converter/Json/FieldInfoConverter.cs b/ReflectorNet/src/Converter/Json/FieldInfoConverter.cs new file mode 100644 index 00000000..5dab34d3 --- /dev/null +++ b/ReflectorNet/src/Converter/Json/FieldInfoConverter.cs @@ -0,0 +1,63 @@ +/* + * 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 System.Text.Json; +using System.Text.Json.Serialization; +using com.IvanMurzak.ReflectorNet.Utils; + +namespace com.IvanMurzak.ReflectorNet.Json +{ + /// + /// JsonConverter that handles conversion between JSON objects and System.Reflection.FieldInfo. + /// + public class FieldInfoConverter : JsonConverter + { + static class Json + { + public const string Name = "name"; + public const string DeclaringType = "declaringType"; + } + + public override FieldInfo? Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) + { + if (reader.TokenType == JsonTokenType.Null) + return null; + + using var doc = JsonDocument.ParseValue(ref reader); + var root = doc.RootElement; + + if (!root.TryGetProperty(Json.DeclaringType, out var declaringTypeElement) || + !root.TryGetProperty(Json.Name, out var nameElement)) + { + throw new JsonException("FieldInfo JSON must contain 'declaringType' and 'name'."); + } + + var typeName = declaringTypeElement.GetString(); + var fieldName = nameElement.GetString(); + + var declaringType = TypeUtils.GetType(typeName); + if (declaringType == null) + throw new JsonException($"Could not find type: {typeName}"); + + var field = declaringType.GetField(fieldName!); + if (field == null) + throw new JsonException($"Could not find field: {fieldName} on type: {typeName}"); + + return field; + } + + public override void Write(Utf8JsonWriter writer, FieldInfo value, JsonSerializerOptions options) + { + writer.WriteStartObject(); + writer.WriteString(Json.Name, value.Name); + writer.WriteString(Json.DeclaringType, value.DeclaringType?.GetTypeId()); + writer.WriteEndObject(); + } + } +} diff --git a/ReflectorNet/src/Converter/Json/HalfJsonConverter.cs b/ReflectorNet/src/Converter/Json/HalfJsonConverter.cs new file mode 100644 index 00000000..e3f88d45 --- /dev/null +++ b/ReflectorNet/src/Converter/Json/HalfJsonConverter.cs @@ -0,0 +1,58 @@ +/* + * 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. + */ + +#if NET6_0_OR_GREATER +using System; +using System.Text.Json; +using System.Text.Json.Serialization; +using com.IvanMurzak.ReflectorNet.Utils; + +namespace com.IvanMurzak.ReflectorNet.Json +{ + /// + /// JsonConverter that handles conversion between JSON numbers and System.Half. + /// + public class HalfJsonConverter : JsonConverter + { + public override bool CanConvert(Type typeToConvert) + { + var underlyingType = Nullable.GetUnderlyingType(typeToConvert) ?? typeToConvert; + return underlyingType == typeof(Half); + } + + public override object? Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) + { + if (reader.TokenType == JsonTokenType.Null) + { + if (Nullable.GetUnderlyingType(typeToConvert) != null) + return null; + + throw new JsonException($"Cannot convert null to non-nullable type '{typeToConvert.GetTypeId()}'."); + } + + if (reader.TokenType == JsonTokenType.Number) + { + return (Half)reader.GetDouble(); + } + + if (reader.TokenType == JsonTokenType.String) + { + var stringValue = reader.GetString(); + if (Half.TryParse(stringValue, out var result)) + return result; + } + + throw new JsonException($"Expected number or string token for Half, but got {reader.TokenType}"); + } + + public override void Write(Utf8JsonWriter writer, object value, JsonSerializerOptions options) + { + writer.WriteNumberValue((float)(Half)value); + } + } +} +#endif diff --git a/ReflectorNet/src/Converter/Json/IPAddressJsonConverter.cs b/ReflectorNet/src/Converter/Json/IPAddressJsonConverter.cs new file mode 100644 index 00000000..5bc25ec7 --- /dev/null +++ b/ReflectorNet/src/Converter/Json/IPAddressJsonConverter.cs @@ -0,0 +1,63 @@ +/* + * 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.Net; +using System.Text.Json; +using System.Text.Json.Nodes; +using com.IvanMurzak.ReflectorNet.Utils; + +namespace com.IvanMurzak.ReflectorNet.Json +{ + /// + /// JsonConverter that handles conversion between JSON string values and System.Net.IPAddress. + /// + public class IPAddressJsonConverter : JsonSchemaConverter, IJsonSchemaConverter + { + public static JsonNode Schema => new JsonObject + { + [JsonSchema.Type] = JsonSchema.String, + ["format"] = "ipv4-or-ipv6" + }; + + public static JsonNode SchemaRef => new JsonObject + { + [JsonSchema.Ref] = JsonSchema.RefValue + StaticId + }; + + public override JsonNode GetSchemaRef() => SchemaRef; + public override JsonNode GetSchema() => Schema; + + public override IPAddress? Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) + { + if (reader.TokenType == JsonTokenType.Null) + return null; + + if (reader.TokenType != JsonTokenType.String) + throw new JsonException("Expected string for IPAddress."); + + var stringValue = reader.GetString(); + if (IPAddress.TryParse(stringValue, out var address)) + { + return address; + } + + throw new JsonException($"Invalid IPAddress format: {stringValue}"); + } + + public override void Write(Utf8JsonWriter writer, IPAddress value, JsonSerializerOptions options) + { + if (value == null) + { + writer.WriteNullValue(); + return; + } + + writer.WriteStringValue(value.ToString()); + } + } +} diff --git a/ReflectorNet/src/Converter/Json/IPEndPointJsonConverter.cs b/ReflectorNet/src/Converter/Json/IPEndPointJsonConverter.cs new file mode 100644 index 00000000..ed04fd73 --- /dev/null +++ b/ReflectorNet/src/Converter/Json/IPEndPointJsonConverter.cs @@ -0,0 +1,109 @@ +/* + * 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.Net; +using System.Text.Json; +using System.Text.Json.Nodes; +using com.IvanMurzak.ReflectorNet.Utils; + +namespace com.IvanMurzak.ReflectorNet.Json +{ + /// + /// JsonConverter that handles conversion between JSON objects and System.Net.IPEndPoint. + /// + public class IPEndPointJsonConverter : JsonSchemaConverter, IJsonSchemaConverter + { + private const string AddressProperty = "address"; + private const string PortProperty = "port"; + + public static JsonNode Schema => new JsonObject + { + [JsonSchema.Type] = JsonSchema.Object, + [JsonSchema.Properties] = new JsonObject + { + [AddressProperty] = IPAddressJsonConverter.SchemaRef, + [PortProperty] = new JsonObject + { + [JsonSchema.Type] = JsonSchema.Integer, + ["minimum"] = 0, + ["maximum"] = 65535 + } + }, + [JsonSchema.Required] = new JsonArray { AddressProperty, PortProperty } + }; + + public static JsonNode SchemaRef => new JsonObject + { + [JsonSchema.Ref] = JsonSchema.RefValue + StaticId + }; + + public override JsonNode GetSchemaRef() => SchemaRef; + public override JsonNode GetSchema() => Schema; + + public override IPEndPoint? Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) + { + if (reader.TokenType == JsonTokenType.Null) + return null; + + if (reader.TokenType != JsonTokenType.StartObject) + throw new JsonException("Expected StartObject for IPEndPoint."); + + IPAddress address = null; + int port = 0; + bool addressSet = false; + bool portSet = false; + + while (reader.Read()) + { + if (reader.TokenType == JsonTokenType.EndObject) + break; + + if (reader.TokenType == JsonTokenType.PropertyName) + { + string propertyName = reader.GetString(); + reader.Read(); + + if (string.Equals(propertyName, AddressProperty, StringComparison.OrdinalIgnoreCase)) + { + address = System.Text.Json.JsonSerializer.Deserialize(ref reader, options); + addressSet = true; + } + else if (string.Equals(propertyName, PortProperty, StringComparison.OrdinalIgnoreCase)) + { + port = reader.GetInt32(); + portSet = true; + } + else + { + reader.Skip(); + } + } + } + + if (!addressSet || !portSet) + throw new JsonException("IPEndPoint requires both 'address' and 'port' properties."); + + return new IPEndPoint(address, port); + } + + public override void Write(Utf8JsonWriter writer, IPEndPoint value, JsonSerializerOptions options) + { + if (value == null) + { + writer.WriteNullValue(); + return; + } + + writer.WriteStartObject(); + writer.WritePropertyName(AddressProperty); + System.Text.Json.JsonSerializer.Serialize(writer, value.Address, options); + writer.WriteNumber(PortProperty, value.Port); + writer.WriteEndObject(); + } + } +} diff --git a/ReflectorNet/src/Converter/Json/IntPtrJsonConverter.cs b/ReflectorNet/src/Converter/Json/IntPtrJsonConverter.cs new file mode 100644 index 00000000..ada15652 --- /dev/null +++ b/ReflectorNet/src/Converter/Json/IntPtrJsonConverter.cs @@ -0,0 +1,56 @@ +/* + * 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.Text.Json; +using System.Text.Json.Serialization; +using com.IvanMurzak.ReflectorNet.Utils; + +namespace com.IvanMurzak.ReflectorNet.Json +{ + /// + /// JsonConverter that handles conversion between JSON numbers/strings and System.IntPtr. + /// + public class IntPtrJsonConverter : JsonConverter + { + public override bool CanConvert(Type typeToConvert) + { + var underlyingType = Nullable.GetUnderlyingType(typeToConvert) ?? typeToConvert; + return underlyingType == typeof(IntPtr); + } + + public override object? Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) + { + if (reader.TokenType == JsonTokenType.Null) + { + if (Nullable.GetUnderlyingType(typeToConvert) != null) + return null; + + return IntPtr.Zero; + } + + if (reader.TokenType == JsonTokenType.Number) + { + return new IntPtr(reader.GetInt64()); + } + + if (reader.TokenType == JsonTokenType.String) + { + var stringValue = reader.GetString(); + if (long.TryParse(stringValue, out var result)) + return new IntPtr(result); + } + + throw new JsonException($"Expected number or string token for IntPtr, but got {reader.TokenType}"); + } + + public override void Write(Utf8JsonWriter writer, object value, JsonSerializerOptions options) + { + writer.WriteNumberValue(((IntPtr)value).ToInt64()); + } + } +} diff --git a/ReflectorNet/src/Converter/Json/JsonNodeConverter.cs b/ReflectorNet/src/Converter/Json/JsonNodeConverter.cs new file mode 100644 index 00000000..777edf31 --- /dev/null +++ b/ReflectorNet/src/Converter/Json/JsonNodeConverter.cs @@ -0,0 +1,42 @@ +/* + * 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.Text.Json; +using System.Text.Json.Nodes; +using com.IvanMurzak.ReflectorNet.Utils; + +namespace com.IvanMurzak.ReflectorNet.Json +{ + /// + /// JsonConverter that handles serialization and deserialization of JsonNode instances. + /// + public class JsonNodeConverter : JsonNodeJsonConverter, IJsonSchemaConverter + { + public static JsonNode Schema => JsonArrayJsonConverter.JsonAnySchema; + public static JsonNode SchemaRef => new JsonObject + { + [JsonSchema.Ref] = JsonSchema.RefValue + StaticId + }; + + public override JsonNode GetSchema() => Schema; + public override JsonNode GetSchemaRef() => SchemaRef; + + protected override JsonNode? CreateJsonNode(JsonElement element) + { + switch (element.ValueKind) + { + case JsonValueKind.Object: + return JsonObject.Create(element); + case JsonValueKind.Array: + return JsonArray.Create(element); + default: + return JsonValue.Create(element); + } + } + } +} diff --git a/ReflectorNet/src/Converter/Json/JsonValueJsonConverter.cs b/ReflectorNet/src/Converter/Json/JsonValueJsonConverter.cs new file mode 100644 index 00000000..b526d223 --- /dev/null +++ b/ReflectorNet/src/Converter/Json/JsonValueJsonConverter.cs @@ -0,0 +1,33 @@ +/* + * 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.Text.Json; +using System.Text.Json.Nodes; +using com.IvanMurzak.ReflectorNet.Utils; + +namespace com.IvanMurzak.ReflectorNet.Json +{ + /// + /// JsonConverter that handles serialization and deserialization of JsonValue instances. + /// + public class JsonValueJsonConverter : JsonNodeJsonConverter, IJsonSchemaConverter + { + public static JsonNode Schema => JsonArrayJsonConverter.JsonAnySchema; + public static JsonNode SchemaRef => new JsonObject + { + [JsonSchema.Ref] = JsonSchema.RefValue + StaticId + }; + + public override JsonNode GetSchema() => Schema; + public override JsonNode GetSchemaRef() => SchemaRef; + + protected override JsonValue? CreateJsonNode(JsonElement element) + { + return JsonValue.Create(element); + } + } +} diff --git a/ReflectorNet/src/Converter/Json/ParameterInfoConverter.cs b/ReflectorNet/src/Converter/Json/ParameterInfoConverter.cs new file mode 100644 index 00000000..690c0dc7 --- /dev/null +++ b/ReflectorNet/src/Converter/Json/ParameterInfoConverter.cs @@ -0,0 +1,83 @@ +/* + * 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.Linq; +using System.Reflection; +using System.Text.Json; +using System.Text.Json.Serialization; +using com.IvanMurzak.ReflectorNet.Utils; + +namespace com.IvanMurzak.ReflectorNet.Json +{ + /// + /// JsonConverter that handles conversion between JSON objects and System.Reflection.ParameterInfo. + /// + public class ParameterInfoConverter : JsonConverter + { + static class Json + { + public const string Name = "name"; + public const string Member = "member"; + public const string MemberType = "memberType"; + } + + public override ParameterInfo? Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) + { + if (reader.TokenType == JsonTokenType.Null) + return null; + + using var doc = JsonDocument.ParseValue(ref reader); + var root = doc.RootElement; + + if (!root.TryGetProperty(Json.Name, out var nameElement) || + !root.TryGetProperty(Json.Member, out var memberElement)) + { + throw new JsonException("ParameterInfo JSON must contain 'name' and 'member'."); + } + + var paramName = nameElement.GetString(); + var memberJson = memberElement.GetRawText(); + + MethodBase? methodBase = null; + if (root.TryGetProperty(Json.MemberType, out var memberTypeElement)) + { + var memberType = memberTypeElement.GetString(); + if (memberType == nameof(MethodInfo)) + methodBase = System.Text.Json.JsonSerializer.Deserialize(memberJson, options); + else if (memberType == nameof(ConstructorInfo)) + methodBase = System.Text.Json.JsonSerializer.Deserialize(memberJson, options); + } + + if (methodBase == null) + throw new JsonException("Could not resolve member for ParameterInfo."); + + var parameter = methodBase.GetParameters().FirstOrDefault(p => p.Name == paramName); + if (parameter == null) + throw new JsonException($"Could not find parameter: {paramName} on member: {methodBase.Name}"); + + return parameter; + } + + public override void Write(Utf8JsonWriter writer, ParameterInfo value, JsonSerializerOptions options) + { + writer.WriteStartObject(); + writer.WriteString(Json.Name, value.Name); + writer.WriteString(Json.MemberType, value.Member.GetType().Name); + writer.WritePropertyName(Json.Member); + + if (value.Member is MethodInfo methodInfo) + System.Text.Json.JsonSerializer.Serialize(writer, methodInfo, options); + else if (value.Member is ConstructorInfo constructorInfo) + System.Text.Json.JsonSerializer.Serialize(writer, constructorInfo, options); + else + writer.WriteNullValue(); + + writer.WriteEndObject(); + } + } +} diff --git a/ReflectorNet/src/Converter/Json/PropertyInfoConverter.cs b/ReflectorNet/src/Converter/Json/PropertyInfoConverter.cs new file mode 100644 index 00000000..b85664cc --- /dev/null +++ b/ReflectorNet/src/Converter/Json/PropertyInfoConverter.cs @@ -0,0 +1,63 @@ +/* + * 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 System.Text.Json; +using System.Text.Json.Serialization; +using com.IvanMurzak.ReflectorNet.Utils; + +namespace com.IvanMurzak.ReflectorNet.Json +{ + /// + /// JsonConverter that handles conversion between JSON objects and System.Reflection.PropertyInfo. + /// + public class PropertyInfoConverter : JsonConverter + { + static class Json + { + public const string Name = "name"; + public const string DeclaringType = "declaringType"; + } + + public override PropertyInfo? Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) + { + if (reader.TokenType == JsonTokenType.Null) + return null; + + using var doc = JsonDocument.ParseValue(ref reader); + var root = doc.RootElement; + + if (!root.TryGetProperty(Json.DeclaringType, out var declaringTypeElement) || + !root.TryGetProperty(Json.Name, out var nameElement)) + { + throw new JsonException("PropertyInfo JSON must contain 'declaringType' and 'name'."); + } + + var typeName = declaringTypeElement.GetString(); + var propertyName = nameElement.GetString(); + + var declaringType = TypeUtils.GetType(typeName); + if (declaringType == null) + throw new JsonException($"Could not find type: {typeName}"); + + var property = declaringType.GetProperty(propertyName!); + if (property == null) + throw new JsonException($"Could not find property: {propertyName} on type: {typeName}"); + + return property; + } + + public override void Write(Utf8JsonWriter writer, PropertyInfo value, JsonSerializerOptions options) + { + writer.WriteStartObject(); + writer.WriteString(Json.Name, value.Name); + writer.WriteString(Json.DeclaringType, value.DeclaringType?.GetTypeId()); + writer.WriteEndObject(); + } + } +} diff --git a/ReflectorNet/src/Converter/Json/TimeOnlyJsonConverter.cs b/ReflectorNet/src/Converter/Json/TimeOnlyJsonConverter.cs new file mode 100644 index 00000000..ae8e678f --- /dev/null +++ b/ReflectorNet/src/Converter/Json/TimeOnlyJsonConverter.cs @@ -0,0 +1,37 @@ +/* + * 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. + */ + +#if NET6_0_OR_GREATER +using System; +using System.Text.Json; +using System.Text.Json.Serialization; + +namespace com.IvanMurzak.ReflectorNet.Json +{ + /// + /// JsonConverter that handles conversion between JSON strings and System.TimeOnly. + /// + public class TimeOnlyJsonConverter : JsonConverter + { + private const string Format = "HH:mm:ss.fffffff"; + + public override TimeOnly Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) + { + if (reader.TokenType != JsonTokenType.String) + throw new JsonException($"Expected string token for TimeOnly, but got {reader.TokenType}"); + + var stringValue = reader.GetString(); + return TimeOnly.Parse(stringValue!); + } + + public override void Write(Utf8JsonWriter writer, TimeOnly value, JsonSerializerOptions options) + { + writer.WriteStringValue(value.ToString(Format)); + } + } +} +#endif diff --git a/ReflectorNet/src/Converter/Json/UIntPtrJsonConverter.cs b/ReflectorNet/src/Converter/Json/UIntPtrJsonConverter.cs new file mode 100644 index 00000000..8f49df9f --- /dev/null +++ b/ReflectorNet/src/Converter/Json/UIntPtrJsonConverter.cs @@ -0,0 +1,56 @@ +/* + * 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.Text.Json; +using System.Text.Json.Serialization; +using com.IvanMurzak.ReflectorNet.Utils; + +namespace com.IvanMurzak.ReflectorNet.Json +{ + /// + /// JsonConverter that handles conversion between JSON numbers/strings and System.UIntPtr. + /// + public class UIntPtrJsonConverter : JsonConverter + { + public override bool CanConvert(Type typeToConvert) + { + var underlyingType = Nullable.GetUnderlyingType(typeToConvert) ?? typeToConvert; + return underlyingType == typeof(UIntPtr); + } + + public override object? Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) + { + if (reader.TokenType == JsonTokenType.Null) + { + if (Nullable.GetUnderlyingType(typeToConvert) != null) + return null; + + return UIntPtr.Zero; + } + + if (reader.TokenType == JsonTokenType.Number) + { + return new UIntPtr(reader.GetUInt64()); + } + + if (reader.TokenType == JsonTokenType.String) + { + var stringValue = reader.GetString(); + if (ulong.TryParse(stringValue, out var result)) + return new UIntPtr(result); + } + + throw new JsonException($"Expected number or string token for UIntPtr, but got {reader.TokenType}"); + } + + public override void Write(Utf8JsonWriter writer, object value, JsonSerializerOptions options) + { + writer.WriteNumberValue(((UIntPtr)value).ToUInt64()); + } + } +} diff --git a/ReflectorNet/src/Converter/Json/UriJsonConverter.cs b/ReflectorNet/src/Converter/Json/UriJsonConverter.cs new file mode 100644 index 00000000..f10d7a69 --- /dev/null +++ b/ReflectorNet/src/Converter/Json/UriJsonConverter.cs @@ -0,0 +1,42 @@ +/* + * 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.Text.Json; +using System.Text.Json.Serialization; + +namespace com.IvanMurzak.ReflectorNet.Json +{ + /// + /// JsonConverter that handles conversion between JSON strings and System.Uri. + /// + public class UriJsonConverter : JsonConverter + { + public override Uri? Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) + { + if (reader.TokenType == JsonTokenType.Null) + return null; + + if (reader.TokenType != JsonTokenType.String) + throw new JsonException($"Expected string token for Uri, but got {reader.TokenType}"); + + var stringValue = reader.GetString(); + if (string.IsNullOrWhiteSpace(stringValue)) + return null; + + if (Uri.TryCreate(stringValue, UriKind.RelativeOrAbsolute, out var uri)) + return uri; + + throw new JsonException($"Unable to parse '{stringValue}' as a Uri."); + } + + public override void Write(Utf8JsonWriter writer, Uri value, JsonSerializerOptions options) + { + writer.WriteStringValue(value.OriginalString); + } + } +} diff --git a/ReflectorNet/src/Converter/Json/VersionJsonConverter.cs b/ReflectorNet/src/Converter/Json/VersionJsonConverter.cs new file mode 100644 index 00000000..3461ac0e --- /dev/null +++ b/ReflectorNet/src/Converter/Json/VersionJsonConverter.cs @@ -0,0 +1,43 @@ +/* + * 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.Text.Json; +using System.Text.Json.Serialization; +using com.IvanMurzak.ReflectorNet.Utils; + +namespace com.IvanMurzak.ReflectorNet.Json +{ + /// + /// JsonConverter that handles conversion between JSON strings and System.Version. + /// + public class VersionJsonConverter : JsonConverter + { + public override Version? Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) + { + if (reader.TokenType == JsonTokenType.Null) + return null; + + if (reader.TokenType != JsonTokenType.String) + throw new JsonException($"Expected string token for Version, but got {reader.TokenType}"); + + var stringValue = reader.GetString(); + if (string.IsNullOrWhiteSpace(stringValue)) + return null; + + if (Version.TryParse(stringValue, out var version)) + return version; + + throw new JsonException($"Unable to parse '{stringValue}' as a Version."); + } + + public override void Write(Utf8JsonWriter writer, Version value, JsonSerializerOptions options) + { + writer.WriteStringValue(value.ToString()); + } + } +} diff --git a/ReflectorNet/src/Utils/Json/JsonSerializer.cs b/ReflectorNet/src/Utils/Json/JsonSerializer.cs index f6455d73..025a7885 100644 --- a/ReflectorNet/src/Utils/Json/JsonSerializer.cs +++ b/ReflectorNet/src/Utils/Json/JsonSerializer.cs @@ -99,22 +99,48 @@ public JsonSerializer(Reflector reflector) new SingleJsonConverter(), new DoubleJsonConverter(), new BoolJsonConverter(), + new CharJsonConverter(), new EnumJsonConverter(), +#if NET6_0_OR_GREATER + new HalfJsonConverter(), +#endif + new IntPtrJsonConverter(), + new UIntPtrJsonConverter(), new DateTimeJsonConverter(), new DateTimeOffsetJsonConverter(), +#if NET6_0_OR_GREATER + new DateOnlyJsonConverter(), + new TimeOnlyJsonConverter(), +#endif new DecimalJsonConverter(), new GuidJsonConverter(), new TimeSpanJsonConverter(), new TypeJsonConverter(), + new UriJsonConverter(), + new VersionJsonConverter(), + new BigIntegerJsonConverter(), + new ComplexJsonConverter(), + new IPAddressJsonConverter(), + new IPEndPointJsonConverter(), // Json converters new JsonElementJsonConverter(), + new JsonNodeConverter(), new JsonObjectJsonConverter(), new JsonArrayJsonConverter(), + new JsonValueJsonConverter(), + + // Reflection converters + new AssemblyJsonConverter(), + new ConstructorInfoConverter(), + new FieldInfoConverter(), + new MethodInfoConverter(), + new PropertyInfoConverter(), + new ParameterInfoConverter(), // Other converters + new ExceptionJsonConverter(), new MethodDataConverter(), - new MethodInfoConverter(), new SerializedMemberConverter(reflector), new SerializedMemberListConverter(reflector) } From 31414d6a81130fd91f43bc194fa4b27d1f6b1b99 Mon Sep 17 00:00:00 2001 From: Ivan Murzak Date: Fri, 26 Dec 2025 01:33:49 -0800 Subject: [PATCH 09/23] fix: Improve readability by simplifying parameter passing in serialization methods --- .../Base/BaseReflectionConverter.Serialize.cs | 13 +++++++++---- 1 file changed, 9 insertions(+), 4 deletions(-) diff --git a/ReflectorNet/src/Converter/Reflection/Base/BaseReflectionConverter.Serialize.cs b/ReflectorNet/src/Converter/Reflection/Base/BaseReflectionConverter.Serialize.cs index 0f692d19..59346b92 100644 --- a/ReflectorNet/src/Converter/Reflection/Base/BaseReflectionConverter.Serialize.cs +++ b/ReflectorNet/src/Converter/Reflection/Base/BaseReflectionConverter.Serialize.cs @@ -11,7 +11,6 @@ using System.Reflection; using com.IvanMurzak.ReflectorNet.Model; using Microsoft.Extensions.Logging; -using com.IvanMurzak.ReflectorNet.Utils; namespace com.IvanMurzak.ReflectorNet.Converter { @@ -32,7 +31,9 @@ public virtual SerializedMember Serialize( ILogger? logger = null, SerializationContext? context = null) { - return InternalSerialize(reflector, obj, + return InternalSerialize( + reflector: reflector, + obj: obj, type: type ?? obj?.GetType() ?? typeof(T), name: name, recursive: recursive, @@ -68,7 +69,9 @@ public virtual SerializedMember Serialize( var value = field.GetValue(obj); var fieldType = field.FieldType; - var serialized = reflector.Serialize(value, fieldType, + var serialized = reflector.Serialize( + obj: value, + fallbackType: fieldType, name: field.Name, recursive: AllowCascadeFieldsConversion, flags: flags, @@ -115,7 +118,9 @@ public virtual SerializedMember Serialize( var value = prop.GetValue(obj); var propType = prop.PropertyType; - var serialized = reflector.Serialize(value, propType, + var serialized = reflector.Serialize( + obj: value, + fallbackType: propType, name: prop.Name, recursive: AllowCascadePropertiesConversion, flags: flags, From e2ae538078d046fd40b134e2a7afbdce2c91a6e4 Mon Sep 17 00:00:00 2001 From: Ivan Murzak Date: Fri, 26 Dec 2025 01:36:34 -0800 Subject: [PATCH 10/23] fix: Rename class for clarity and update related test references --- .../src/ReflectorTests/NonSerializedInterfaceTests.cs | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/ReflectorNet.Tests/src/ReflectorTests/NonSerializedInterfaceTests.cs b/ReflectorNet.Tests/src/ReflectorTests/NonSerializedInterfaceTests.cs index f97897e4..5b79a481 100644 --- a/ReflectorNet.Tests/src/ReflectorTests/NonSerializedInterfaceTests.cs +++ b/ReflectorNet.Tests/src/ReflectorTests/NonSerializedInterfaceTests.cs @@ -28,7 +28,7 @@ public class ClassWithNonSerializedInterface public ICharacterController? CharacterController; } - public class ClassWithNonSerializedProperty + public class ClassWithPropertyHavingNonSerializedBackingField { public string? Name; @@ -110,7 +110,7 @@ public void TestNonSerializedInterfaceField_NotNull() [Fact] public void TestNonSerializedInterfaceProperty() { - var instance = new ClassWithNonSerializedProperty + var instance = new ClassWithPropertyHavingNonSerializedBackingField { Name = "TestInstance", Description = "ShouldBeSerialized" @@ -123,7 +123,7 @@ public void TestNonSerializedInterfaceProperty() Assert.NotNull(serialized); - var deserialized = reflector.Deserialize(serialized); + var deserialized = reflector.Deserialize(serialized); Assert.NotNull(deserialized); Assert.Equal("TestInstance", deserialized.Name); Assert.Equal("ShouldBeSerialized", deserialized.Description); From a524022049a02dc880948e355fecdbc5d3b8933a Mon Sep 17 00:00:00 2001 From: Ivan Murzak Date: Fri, 26 Dec 2025 01:40:18 -0800 Subject: [PATCH 11/23] fix: Enhance null handling and improve type safety in JSON converters --- ConsoleApp/Program.cs | 20 +++++++++---------- .../Converter/Json/ComplexJsonConverter.cs | 8 +++++++- .../Converter/Json/IPEndPointJsonConverter.cs | 12 ++++++++--- 3 files changed, 26 insertions(+), 14 deletions(-) diff --git a/ConsoleApp/Program.cs b/ConsoleApp/Program.cs index aa3cdad0..6ee1bf1e 100644 --- a/ConsoleApp/Program.cs +++ b/ConsoleApp/Program.cs @@ -17,13 +17,13 @@ var reflector = new Reflector(); var methodInfo = typeof(Sample).GetMethod(nameof(Sample.Command)); -var argumentsSchema = reflector.GetArgumentsSchema(methodInfo); -var outputSchema = reflector.GetReturnSchema(methodInfo); +var argumentsSchema = reflector.GetArgumentsSchema(methodInfo!); +var outputSchema = reflector.GetReturnSchema(methodInfo!); Console.WriteLine("Arguments Schema:"); Console.WriteLine(argumentsSchema.ToJsonString(new System.Text.Json.JsonSerializerOptions { WriteIndented = true })); Console.WriteLine("Output Schema:"); -Console.WriteLine(outputSchema.ToJsonString(new System.Text.Json.JsonSerializerOptions { WriteIndented = true })); +Console.WriteLine(outputSchema?.ToJsonString(new System.Text.Json.JsonSerializerOptions { WriteIndented = true })); var schemaJson = reflector.GetSchema>(); @@ -44,8 +44,8 @@ public class Data { public int? optionalInt; [Description("THIS IS THE DEMO DESCRIPTION.")] - public string address; - public List list; // recursive + public string address = null!; + public List list = null!; // recursive } public struct Result @@ -74,15 +74,15 @@ public struct GeoPoint public class PlayerProfile { public Guid Id { get; set; } - public string Username { get; set; } - public string[] Badges { get; set; } // Array - public Dictionary Stats { get; set; } // Dictionary + public string Username { get; set; } = null!; + public string[] Badges { get; set; } = null!; // Array + public Dictionary Stats { get; set; } = null!; // Dictionary } // A generic wrapper (common in Game State or API responses) public class GameState { public long Timestamp { get; set; } - public T Player { get; set; } - public List Checkpoints { get; set; } // List of Structs + public T Player { get; set; } = default!; + public List Checkpoints { get; set; } = null!; // List of Structs } \ No newline at end of file diff --git a/ReflectorNet/src/Converter/Json/ComplexJsonConverter.cs b/ReflectorNet/src/Converter/Json/ComplexJsonConverter.cs index 18ce518e..f39cd81c 100644 --- a/ReflectorNet/src/Converter/Json/ComplexJsonConverter.cs +++ b/ReflectorNet/src/Converter/Json/ComplexJsonConverter.cs @@ -57,9 +57,15 @@ public override Complex Read(ref Utf8JsonReader reader, Type typeToConvert, Json if (reader.TokenType == JsonTokenType.PropertyName) { - string propertyName = reader.GetString(); + var propertyName = reader.GetString(); reader.Read(); + if (propertyName == null) + { + reader.Skip(); + continue; + } + if (string.Equals(propertyName, RealProperty, StringComparison.OrdinalIgnoreCase)) { real = reader.GetDouble(); diff --git a/ReflectorNet/src/Converter/Json/IPEndPointJsonConverter.cs b/ReflectorNet/src/Converter/Json/IPEndPointJsonConverter.cs index ed04fd73..ae289d08 100644 --- a/ReflectorNet/src/Converter/Json/IPEndPointJsonConverter.cs +++ b/ReflectorNet/src/Converter/Json/IPEndPointJsonConverter.cs @@ -53,7 +53,7 @@ public class IPEndPointJsonConverter : JsonSchemaConverter, IJsonSch if (reader.TokenType != JsonTokenType.StartObject) throw new JsonException("Expected StartObject for IPEndPoint."); - IPAddress address = null; + IPAddress? address = null; int port = 0; bool addressSet = false; bool portSet = false; @@ -65,9 +65,15 @@ public class IPEndPointJsonConverter : JsonSchemaConverter, IJsonSch if (reader.TokenType == JsonTokenType.PropertyName) { - string propertyName = reader.GetString(); + var propertyName = reader.GetString(); reader.Read(); + if (propertyName == null) + { + reader.Skip(); + continue; + } + if (string.Equals(propertyName, AddressProperty, StringComparison.OrdinalIgnoreCase)) { address = System.Text.Json.JsonSerializer.Deserialize(ref reader, options); @@ -85,7 +91,7 @@ public class IPEndPointJsonConverter : JsonSchemaConverter, IJsonSch } } - if (!addressSet || !portSet) + if (!addressSet || !portSet || address == null) throw new JsonException("IPEndPoint requires both 'address' and 'port' properties."); return new IPEndPoint(address, port); From d1b2a1c22f885e07d20d2454db889eb841303240 Mon Sep 17 00:00:00 2001 From: Ivan Murzak Date: Fri, 26 Dec 2025 01:42:18 -0800 Subject: [PATCH 12/23] fix: Simplify variable declarations in ComplexJsonConverter and IPEndPointJsonConverter --- ReflectorNet/src/Converter/Json/ComplexJsonConverter.cs | 8 ++++---- .../src/Converter/Json/IPEndPointJsonConverter.cs | 6 +++--- 2 files changed, 7 insertions(+), 7 deletions(-) diff --git a/ReflectorNet/src/Converter/Json/ComplexJsonConverter.cs b/ReflectorNet/src/Converter/Json/ComplexJsonConverter.cs index f39cd81c..5604c4c0 100644 --- a/ReflectorNet/src/Converter/Json/ComplexJsonConverter.cs +++ b/ReflectorNet/src/Converter/Json/ComplexJsonConverter.cs @@ -45,10 +45,10 @@ public override Complex Read(ref Utf8JsonReader reader, Type typeToConvert, Json if (reader.TokenType != JsonTokenType.StartObject) throw new JsonException("Expected StartObject for Complex."); - double real = 0; - double imaginary = 0; - bool realSet = false; - bool imaginarySet = false; + var real = 0d; + var imaginary = 0d; + var realSet = false; + var imaginarySet = false; while (reader.Read()) { diff --git a/ReflectorNet/src/Converter/Json/IPEndPointJsonConverter.cs b/ReflectorNet/src/Converter/Json/IPEndPointJsonConverter.cs index ae289d08..44e6fd02 100644 --- a/ReflectorNet/src/Converter/Json/IPEndPointJsonConverter.cs +++ b/ReflectorNet/src/Converter/Json/IPEndPointJsonConverter.cs @@ -54,9 +54,9 @@ public class IPEndPointJsonConverter : JsonSchemaConverter, IJsonSch throw new JsonException("Expected StartObject for IPEndPoint."); IPAddress? address = null; - int port = 0; - bool addressSet = false; - bool portSet = false; + var port = 0; + var addressSet = false; + var portSet = false; while (reader.Read()) { From e780dbee3c2f12be6d9844f4f1dae357fcbd6b43 Mon Sep 17 00:00:00 2001 From: Ivan Murzak Date: Fri, 26 Dec 2025 02:23:35 -0800 Subject: [PATCH 13/23] fix: Enhance null handling in JSON converters and improve field/property retrieval --- .../src/Converter/Json/AssemblyJsonConverter.cs | 5 +++++ ReflectorNet/src/Converter/Json/CharJsonConverter.cs | 5 +++++ .../src/Converter/Json/ConstructorInfoConverter.cs | 12 +++++++++++- .../src/Converter/Json/ExceptionJsonConverter.cs | 5 +++++ .../src/Converter/Json/FieldInfoConverter.cs | 10 +++++++++- ReflectorNet/src/Converter/Json/HalfJsonConverter.cs | 5 +++++ .../src/Converter/Json/IntPtrJsonConverter.cs | 5 +++++ .../src/Converter/Json/ParameterInfoConverter.cs | 5 +++++ .../src/Converter/Json/PropertyInfoConverter.cs | 10 +++++++++- .../src/Converter/Json/TimeOnlyJsonConverter.cs | 8 +++++++- .../src/Converter/Json/UIntPtrJsonConverter.cs | 5 +++++ ReflectorNet/src/Converter/Json/UriJsonConverter.cs | 5 +++++ .../src/Converter/Json/VersionJsonConverter.cs | 7 ++++++- .../Reflection/Base/BaseReflectionConverter.cs | 2 +- 14 files changed, 83 insertions(+), 6 deletions(-) diff --git a/ReflectorNet/src/Converter/Json/AssemblyJsonConverter.cs b/ReflectorNet/src/Converter/Json/AssemblyJsonConverter.cs index c4007711..c2ebb0df 100644 --- a/ReflectorNet/src/Converter/Json/AssemblyJsonConverter.cs +++ b/ReflectorNet/src/Converter/Json/AssemblyJsonConverter.cs @@ -48,6 +48,11 @@ public class AssemblyJsonConverter : JsonConverter public override void Write(Utf8JsonWriter writer, Assembly value, JsonSerializerOptions options) { + if (value == null) + { + writer.WriteNullValue(); + return; + } writer.WriteStringValue(value.FullName); } } diff --git a/ReflectorNet/src/Converter/Json/CharJsonConverter.cs b/ReflectorNet/src/Converter/Json/CharJsonConverter.cs index 1cabccef..b99bcda5 100644 --- a/ReflectorNet/src/Converter/Json/CharJsonConverter.cs +++ b/ReflectorNet/src/Converter/Json/CharJsonConverter.cs @@ -61,6 +61,11 @@ public override bool CanConvert(Type typeToConvert) public override void Write(Utf8JsonWriter writer, object value, JsonSerializerOptions options) { + if (value == null) + { + writer.WriteNullValue(); + return; + } writer.WriteStringValue(value.ToString()); } } diff --git a/ReflectorNet/src/Converter/Json/ConstructorInfoConverter.cs b/ReflectorNet/src/Converter/Json/ConstructorInfoConverter.cs index ca56e954..3c212b1d 100644 --- a/ReflectorNet/src/Converter/Json/ConstructorInfoConverter.cs +++ b/ReflectorNet/src/Converter/Json/ConstructorInfoConverter.cs @@ -55,7 +55,12 @@ static class Json if (declaringType == null) throw new JsonException($"Could not find type: {typeName}"); - var constructor = declaringType.GetConstructor(parameterTypes.ToArray()); + var constructor = declaringType.GetConstructor( + BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic, + binder: null, + types: parameterTypes.ToArray(), + modifiers: null + ); if (constructor == null) throw new JsonException($"Could not find constructor on type: {typeName} with specified parameters."); @@ -64,6 +69,11 @@ static class Json public override void Write(Utf8JsonWriter writer, ConstructorInfo value, JsonSerializerOptions options) { + if (value == null) + { + writer.WriteNullValue(); + return; + } writer.WriteStartObject(); writer.WriteString(Json.DeclaringType, value.DeclaringType?.GetTypeId()); diff --git a/ReflectorNet/src/Converter/Json/ExceptionJsonConverter.cs b/ReflectorNet/src/Converter/Json/ExceptionJsonConverter.cs index 18a22892..61fab385 100644 --- a/ReflectorNet/src/Converter/Json/ExceptionJsonConverter.cs +++ b/ReflectorNet/src/Converter/Json/ExceptionJsonConverter.cs @@ -62,6 +62,11 @@ static class Json public override void Write(Utf8JsonWriter writer, Exception value, JsonSerializerOptions options) { + if (value == null) + { + writer.WriteNullValue(); + return; + } writer.WriteStartObject(); writer.WriteString(Json.Type, value.GetType().GetTypeId()); writer.WriteString(Json.Message, value.Message); diff --git a/ReflectorNet/src/Converter/Json/FieldInfoConverter.cs b/ReflectorNet/src/Converter/Json/FieldInfoConverter.cs index 5dab34d3..33243064 100644 --- a/ReflectorNet/src/Converter/Json/FieldInfoConverter.cs +++ b/ReflectorNet/src/Converter/Json/FieldInfoConverter.cs @@ -45,7 +45,10 @@ static class Json if (declaringType == null) throw new JsonException($"Could not find type: {typeName}"); - var field = declaringType.GetField(fieldName!); + var field = declaringType.GetField( + name: fieldName!, + bindingAttr: BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.Instance | BindingFlags.Static + ); if (field == null) throw new JsonException($"Could not find field: {fieldName} on type: {typeName}"); @@ -54,6 +57,11 @@ static class Json public override void Write(Utf8JsonWriter writer, FieldInfo value, JsonSerializerOptions options) { + if (value == null) + { + writer.WriteNullValue(); + return; + } writer.WriteStartObject(); writer.WriteString(Json.Name, value.Name); writer.WriteString(Json.DeclaringType, value.DeclaringType?.GetTypeId()); diff --git a/ReflectorNet/src/Converter/Json/HalfJsonConverter.cs b/ReflectorNet/src/Converter/Json/HalfJsonConverter.cs index e3f88d45..cee933e9 100644 --- a/ReflectorNet/src/Converter/Json/HalfJsonConverter.cs +++ b/ReflectorNet/src/Converter/Json/HalfJsonConverter.cs @@ -51,6 +51,11 @@ public override bool CanConvert(Type typeToConvert) public override void Write(Utf8JsonWriter writer, object value, JsonSerializerOptions options) { + if (value == null) + { + writer.WriteNullValue(); + return; + } writer.WriteNumberValue((float)(Half)value); } } diff --git a/ReflectorNet/src/Converter/Json/IntPtrJsonConverter.cs b/ReflectorNet/src/Converter/Json/IntPtrJsonConverter.cs index ada15652..61d42f06 100644 --- a/ReflectorNet/src/Converter/Json/IntPtrJsonConverter.cs +++ b/ReflectorNet/src/Converter/Json/IntPtrJsonConverter.cs @@ -50,6 +50,11 @@ public override bool CanConvert(Type typeToConvert) public override void Write(Utf8JsonWriter writer, object value, JsonSerializerOptions options) { + if (value is null) + { + writer.WriteNullValue(); + return; + } writer.WriteNumberValue(((IntPtr)value).ToInt64()); } } diff --git a/ReflectorNet/src/Converter/Json/ParameterInfoConverter.cs b/ReflectorNet/src/Converter/Json/ParameterInfoConverter.cs index 690c0dc7..5c4c2aff 100644 --- a/ReflectorNet/src/Converter/Json/ParameterInfoConverter.cs +++ b/ReflectorNet/src/Converter/Json/ParameterInfoConverter.cs @@ -65,6 +65,11 @@ static class Json public override void Write(Utf8JsonWriter writer, ParameterInfo value, JsonSerializerOptions options) { + if (value == null) + { + writer.WriteNullValue(); + return; + } writer.WriteStartObject(); writer.WriteString(Json.Name, value.Name); writer.WriteString(Json.MemberType, value.Member.GetType().Name); diff --git a/ReflectorNet/src/Converter/Json/PropertyInfoConverter.cs b/ReflectorNet/src/Converter/Json/PropertyInfoConverter.cs index b85664cc..4b0819c4 100644 --- a/ReflectorNet/src/Converter/Json/PropertyInfoConverter.cs +++ b/ReflectorNet/src/Converter/Json/PropertyInfoConverter.cs @@ -45,7 +45,10 @@ static class Json if (declaringType == null) throw new JsonException($"Could not find type: {typeName}"); - var property = declaringType.GetProperty(propertyName!); + var property = declaringType.GetProperty( + name: propertyName!, + bindingAttr: BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.Instance | BindingFlags.Static + ); if (property == null) throw new JsonException($"Could not find property: {propertyName} on type: {typeName}"); @@ -54,6 +57,11 @@ static class Json public override void Write(Utf8JsonWriter writer, PropertyInfo value, JsonSerializerOptions options) { + if (value == null) + { + writer.WriteNullValue(); + return; + } writer.WriteStartObject(); writer.WriteString(Json.Name, value.Name); writer.WriteString(Json.DeclaringType, value.DeclaringType?.GetTypeId()); diff --git a/ReflectorNet/src/Converter/Json/TimeOnlyJsonConverter.cs b/ReflectorNet/src/Converter/Json/TimeOnlyJsonConverter.cs index ae8e678f..43aca990 100644 --- a/ReflectorNet/src/Converter/Json/TimeOnlyJsonConverter.cs +++ b/ReflectorNet/src/Converter/Json/TimeOnlyJsonConverter.cs @@ -25,7 +25,13 @@ public override TimeOnly Read(ref Utf8JsonReader reader, Type typeToConvert, Jso throw new JsonException($"Expected string token for TimeOnly, but got {reader.TokenType}"); var stringValue = reader.GetString(); - return TimeOnly.Parse(stringValue!); + if (stringValue is null) + throw new JsonException("Expected non-null string value for TimeOnly."); + + if (TimeOnly.TryParseExact(stringValue, Format, null, System.Globalization.DateTimeStyles.None, out var result)) + return result; + + throw new JsonException($"Invalid TimeOnly value '{stringValue}'. Expected format: '{Format}'."); } public override void Write(Utf8JsonWriter writer, TimeOnly value, JsonSerializerOptions options) diff --git a/ReflectorNet/src/Converter/Json/UIntPtrJsonConverter.cs b/ReflectorNet/src/Converter/Json/UIntPtrJsonConverter.cs index 8f49df9f..db6f4bb7 100644 --- a/ReflectorNet/src/Converter/Json/UIntPtrJsonConverter.cs +++ b/ReflectorNet/src/Converter/Json/UIntPtrJsonConverter.cs @@ -50,6 +50,11 @@ public override bool CanConvert(Type typeToConvert) public override void Write(Utf8JsonWriter writer, object value, JsonSerializerOptions options) { + if (value is null) + { + writer.WriteNullValue(); + return; + } writer.WriteNumberValue(((UIntPtr)value).ToUInt64()); } } diff --git a/ReflectorNet/src/Converter/Json/UriJsonConverter.cs b/ReflectorNet/src/Converter/Json/UriJsonConverter.cs index f10d7a69..65b5d039 100644 --- a/ReflectorNet/src/Converter/Json/UriJsonConverter.cs +++ b/ReflectorNet/src/Converter/Json/UriJsonConverter.cs @@ -36,6 +36,11 @@ public class UriJsonConverter : JsonConverter public override void Write(Utf8JsonWriter writer, Uri value, JsonSerializerOptions options) { + if (value is null) + { + writer.WriteNullValue(); + return; + } writer.WriteStringValue(value.OriginalString); } } diff --git a/ReflectorNet/src/Converter/Json/VersionJsonConverter.cs b/ReflectorNet/src/Converter/Json/VersionJsonConverter.cs index 3461ac0e..d85b00ec 100644 --- a/ReflectorNet/src/Converter/Json/VersionJsonConverter.cs +++ b/ReflectorNet/src/Converter/Json/VersionJsonConverter.cs @@ -35,8 +35,13 @@ public class VersionJsonConverter : JsonConverter throw new JsonException($"Unable to parse '{stringValue}' as a Version."); } - public override void Write(Utf8JsonWriter writer, Version value, JsonSerializerOptions options) + public override void Write(Utf8JsonWriter writer, Version? value, JsonSerializerOptions options) { + if (value is null) + { + writer.WriteNullValue(); + return; + } writer.WriteStringValue(value.ToString()); } } diff --git a/ReflectorNet/src/Converter/Reflection/Base/BaseReflectionConverter.cs b/ReflectorNet/src/Converter/Reflection/Base/BaseReflectionConverter.cs index 780ee292..2cdc1184 100644 --- a/ReflectorNet/src/Converter/Reflection/Base/BaseReflectionConverter.cs +++ b/ReflectorNet/src/Converter/Reflection/Base/BaseReflectionConverter.cs @@ -100,7 +100,7 @@ public virtual int SerializationPriority(Type type, ILogger? logger = null) /// /// Gets the serializable fields for the specified type. - /// Default implementation returns public fields that are not marked with [Obsolete]. + /// 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( From c327a693de4b0280db54623cd637da782eb6e17a Mon Sep 17 00:00:00 2001 From: Ivan Murzak Date: Fri, 26 Dec 2025 02:47:14 -0800 Subject: [PATCH 14/23] fix: Remove unused using directives and update type handling in test utilities --- ReflectorNet.Tests/src/SchemaTests/ErrorHandlingTests.cs | 1 - ReflectorNet.Tests/src/Utils/TestUtils.Types.cs | 9 +++++---- ReflectorNet/src/Converter/Json/IntPtrJsonConverter.cs | 1 - 3 files changed, 5 insertions(+), 6 deletions(-) diff --git a/ReflectorNet.Tests/src/SchemaTests/ErrorHandlingTests.cs b/ReflectorNet.Tests/src/SchemaTests/ErrorHandlingTests.cs index 17e54712..e877b7cf 100644 --- a/ReflectorNet.Tests/src/SchemaTests/ErrorHandlingTests.cs +++ b/ReflectorNet.Tests/src/SchemaTests/ErrorHandlingTests.cs @@ -1,5 +1,4 @@ using com.IvanMurzak.ReflectorNet.Model; -using com.IvanMurzak.ReflectorNet; using com.IvanMurzak.ReflectorNet.Tests.Model; using Xunit.Abstractions; using System; diff --git a/ReflectorNet.Tests/src/Utils/TestUtils.Types.cs b/ReflectorNet.Tests/src/Utils/TestUtils.Types.cs index 0bc93acc..3c3bd69f 100644 --- a/ReflectorNet.Tests/src/Utils/TestUtils.Types.cs +++ b/ReflectorNet.Tests/src/Utils/TestUtils.Types.cs @@ -39,10 +39,11 @@ public static partial class Types typeof(TimeSpan), typeof(Guid), - // typeof(CultureInfo), - // typeof(AssemblyName), - // typeof(Version), - // typeof(IntPtr), + typeof(IntPtr), + typeof(UIntPtr), + typeof(Version), + // typeof(CultureInfo), // No converter yet + // typeof(AssemblyName), // No converter yet typeof(ParentClass), typeof(ParentClass.NestedClass), diff --git a/ReflectorNet/src/Converter/Json/IntPtrJsonConverter.cs b/ReflectorNet/src/Converter/Json/IntPtrJsonConverter.cs index 61d42f06..9308b37b 100644 --- a/ReflectorNet/src/Converter/Json/IntPtrJsonConverter.cs +++ b/ReflectorNet/src/Converter/Json/IntPtrJsonConverter.cs @@ -8,7 +8,6 @@ using System; using System.Text.Json; using System.Text.Json.Serialization; -using com.IvanMurzak.ReflectorNet.Utils; namespace com.IvanMurzak.ReflectorNet.Json { From db796420c846eee7ef79513679680acb37b3bc10 Mon Sep 17 00:00:00 2001 From: Ivan Murzak Date: Fri, 26 Dec 2025 02:49:11 -0800 Subject: [PATCH 15/23] fix: Enhance null handling in BigIntegerJsonConverter to return default value --- ReflectorNet/src/Converter/Json/BigIntegerJsonConverter.cs | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/ReflectorNet/src/Converter/Json/BigIntegerJsonConverter.cs b/ReflectorNet/src/Converter/Json/BigIntegerJsonConverter.cs index 4351df6e..7130c6aa 100644 --- a/ReflectorNet/src/Converter/Json/BigIntegerJsonConverter.cs +++ b/ReflectorNet/src/Converter/Json/BigIntegerJsonConverter.cs @@ -51,6 +51,10 @@ public override BigInteger Read(ref Utf8JsonReader reader, Type typeToConvert, J return result; } } + else if (reader.TokenType == JsonTokenType.Null) + { + return default; + } throw new JsonException($"Expected string or number representing BigInteger."); } From 63349e99ac2a7370272a7189c08ae95fc73b7f4f Mon Sep 17 00:00:00 2001 From: Ivan Murzak Date: Fri, 26 Dec 2025 03:08:50 -0800 Subject: [PATCH 16/23] fix: Enhance null handling and improve type safety in JSON converters --- ReflectorNet/src/Converter/Json/AssemblyJsonConverter.cs | 2 +- ReflectorNet/src/Converter/Json/CharJsonConverter.cs | 1 - ReflectorNet/src/Converter/Json/ComplexJsonConverter.cs | 2 ++ .../src/Converter/Json/ConstructorInfoConverter.cs | 2 +- ReflectorNet/src/Converter/Json/DateOnlyJsonConverter.cs | 2 ++ ReflectorNet/src/Converter/Json/DateTimeJsonConverter.cs | 2 +- ReflectorNet/src/Converter/Json/EnumJsonConverter.cs | 2 +- ReflectorNet/src/Converter/Json/ExceptionJsonConverter.cs | 2 +- ReflectorNet/src/Converter/Json/FieldInfoConverter.cs | 2 +- ReflectorNet/src/Converter/Json/GuidJsonConverter.cs | 7 ++++++- ReflectorNet/src/Converter/Json/IPAddressJsonConverter.cs | 2 +- ReflectorNet/src/Converter/Json/IPEndPointJsonConverter.cs | 2 +- ReflectorNet/src/Converter/Json/IntPtrJsonConverter.cs | 2 +- ReflectorNet/src/Converter/Json/MethodInfoConverter.cs | 7 ++++++- ReflectorNet/src/Converter/Json/ParameterInfoConverter.cs | 2 +- ReflectorNet/src/Converter/Json/PropertyInfoConverter.cs | 2 +- .../src/Converter/Json/SerializedMemberConverter.cs | 2 +- .../src/Converter/Json/SerializedMemberListConverter.cs | 2 +- ReflectorNet/src/Converter/Json/TimeOnlyJsonConverter.cs | 2 ++ ReflectorNet/src/Converter/Json/TypeJsonConverter.cs | 2 +- ReflectorNet/src/Converter/Json/UInt16JsonConverter.cs | 5 +++++ ReflectorNet/src/Converter/Json/UInt32JsonConverter.cs | 5 +++++ ReflectorNet/src/Converter/Json/UInt64JsonConverter.cs | 5 +++++ ReflectorNet/src/Converter/Json/UriJsonConverter.cs | 2 +- 24 files changed, 48 insertions(+), 18 deletions(-) diff --git a/ReflectorNet/src/Converter/Json/AssemblyJsonConverter.cs b/ReflectorNet/src/Converter/Json/AssemblyJsonConverter.cs index c2ebb0df..3e17530a 100644 --- a/ReflectorNet/src/Converter/Json/AssemblyJsonConverter.cs +++ b/ReflectorNet/src/Converter/Json/AssemblyJsonConverter.cs @@ -46,7 +46,7 @@ public class AssemblyJsonConverter : JsonConverter } } - public override void Write(Utf8JsonWriter writer, Assembly value, JsonSerializerOptions options) + public override void Write(Utf8JsonWriter writer, Assembly? value, JsonSerializerOptions options) { if (value == null) { diff --git a/ReflectorNet/src/Converter/Json/CharJsonConverter.cs b/ReflectorNet/src/Converter/Json/CharJsonConverter.cs index b99bcda5..1f1d6486 100644 --- a/ReflectorNet/src/Converter/Json/CharJsonConverter.cs +++ b/ReflectorNet/src/Converter/Json/CharJsonConverter.cs @@ -8,7 +8,6 @@ using System; using System.Text.Json; using System.Text.Json.Serialization; -using com.IvanMurzak.ReflectorNet.Utils; namespace com.IvanMurzak.ReflectorNet.Json { diff --git a/ReflectorNet/src/Converter/Json/ComplexJsonConverter.cs b/ReflectorNet/src/Converter/Json/ComplexJsonConverter.cs index 5604c4c0..ced05566 100644 --- a/ReflectorNet/src/Converter/Json/ComplexJsonConverter.cs +++ b/ReflectorNet/src/Converter/Json/ComplexJsonConverter.cs @@ -42,6 +42,8 @@ public class ComplexJsonConverter : JsonSchemaConverter, IJsonSchemaCon public override Complex Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) { + if (reader.TokenType == JsonTokenType.Null) + throw new JsonException("Cannot convert null to Complex."); if (reader.TokenType != JsonTokenType.StartObject) throw new JsonException("Expected StartObject for Complex."); diff --git a/ReflectorNet/src/Converter/Json/ConstructorInfoConverter.cs b/ReflectorNet/src/Converter/Json/ConstructorInfoConverter.cs index 3c212b1d..36bdd451 100644 --- a/ReflectorNet/src/Converter/Json/ConstructorInfoConverter.cs +++ b/ReflectorNet/src/Converter/Json/ConstructorInfoConverter.cs @@ -67,7 +67,7 @@ static class Json return constructor; } - public override void Write(Utf8JsonWriter writer, ConstructorInfo value, JsonSerializerOptions options) + public override void Write(Utf8JsonWriter writer, ConstructorInfo? value, JsonSerializerOptions options) { if (value == null) { diff --git a/ReflectorNet/src/Converter/Json/DateOnlyJsonConverter.cs b/ReflectorNet/src/Converter/Json/DateOnlyJsonConverter.cs index f51a4f33..aaae2a91 100644 --- a/ReflectorNet/src/Converter/Json/DateOnlyJsonConverter.cs +++ b/ReflectorNet/src/Converter/Json/DateOnlyJsonConverter.cs @@ -21,6 +21,8 @@ public class DateOnlyJsonConverter : JsonConverter public override DateOnly Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) { + if (reader.TokenType == JsonTokenType.Null) + throw new JsonException("Cannot convert null to DateOnly."); if (reader.TokenType != JsonTokenType.String) throw new JsonException($"Expected string token for DateOnly, but got {reader.TokenType}"); diff --git a/ReflectorNet/src/Converter/Json/DateTimeJsonConverter.cs b/ReflectorNet/src/Converter/Json/DateTimeJsonConverter.cs index 64c13866..2744e3f7 100644 --- a/ReflectorNet/src/Converter/Json/DateTimeJsonConverter.cs +++ b/ReflectorNet/src/Converter/Json/DateTimeJsonConverter.cs @@ -64,7 +64,7 @@ public override bool CanConvert(Type typeToConvert) throw new JsonException($"Expected string or number token but got {reader.TokenType} for type '{typeToConvert.GetTypeId()}'"); } - public override void Write(Utf8JsonWriter writer, object value, JsonSerializerOptions options) + public override void Write(Utf8JsonWriter writer, object? value, JsonSerializerOptions options) { if (value == null) { diff --git a/ReflectorNet/src/Converter/Json/EnumJsonConverter.cs b/ReflectorNet/src/Converter/Json/EnumJsonConverter.cs index 2e095478..2b4f3eb5 100644 --- a/ReflectorNet/src/Converter/Json/EnumJsonConverter.cs +++ b/ReflectorNet/src/Converter/Json/EnumJsonConverter.cs @@ -72,7 +72,7 @@ public override bool CanConvert(Type typeToConvert) throw new JsonException($"Expected string or number token but got {reader.TokenType} for enum type '{typeToConvert.GetTypeId()}'"); } - public override void Write(Utf8JsonWriter writer, object value, JsonSerializerOptions options) + public override void Write(Utf8JsonWriter writer, object? value, JsonSerializerOptions options) { if (value == null) { diff --git a/ReflectorNet/src/Converter/Json/ExceptionJsonConverter.cs b/ReflectorNet/src/Converter/Json/ExceptionJsonConverter.cs index 61fab385..6f034f55 100644 --- a/ReflectorNet/src/Converter/Json/ExceptionJsonConverter.cs +++ b/ReflectorNet/src/Converter/Json/ExceptionJsonConverter.cs @@ -60,7 +60,7 @@ static class Json return new Exception(message, innerException); } - public override void Write(Utf8JsonWriter writer, Exception value, JsonSerializerOptions options) + public override void Write(Utf8JsonWriter writer, Exception? value, JsonSerializerOptions options) { if (value == null) { diff --git a/ReflectorNet/src/Converter/Json/FieldInfoConverter.cs b/ReflectorNet/src/Converter/Json/FieldInfoConverter.cs index 33243064..9224b699 100644 --- a/ReflectorNet/src/Converter/Json/FieldInfoConverter.cs +++ b/ReflectorNet/src/Converter/Json/FieldInfoConverter.cs @@ -55,7 +55,7 @@ static class Json return field; } - public override void Write(Utf8JsonWriter writer, FieldInfo value, JsonSerializerOptions options) + public override void Write(Utf8JsonWriter writer, FieldInfo? value, JsonSerializerOptions options) { if (value == null) { diff --git a/ReflectorNet/src/Converter/Json/GuidJsonConverter.cs b/ReflectorNet/src/Converter/Json/GuidJsonConverter.cs index 8d103678..f8a97859 100644 --- a/ReflectorNet/src/Converter/Json/GuidJsonConverter.cs +++ b/ReflectorNet/src/Converter/Json/GuidJsonConverter.cs @@ -55,8 +55,13 @@ public override bool CanConvert(Type typeToConvert) throw new JsonException($"Expected string token but got {reader.TokenType} for type '{typeToConvert.GetTypeId()}'"); } - public override void Write(Utf8JsonWriter writer, object value, JsonSerializerOptions options) + public override void Write(Utf8JsonWriter writer, object? value, JsonSerializerOptions options) { + if (value is null) + { + writer.WriteNullValue(); + return; + } writer.WriteStringValue(((Guid)value).ToString()); } } diff --git a/ReflectorNet/src/Converter/Json/IPAddressJsonConverter.cs b/ReflectorNet/src/Converter/Json/IPAddressJsonConverter.cs index 5bc25ec7..1b71b99b 100644 --- a/ReflectorNet/src/Converter/Json/IPAddressJsonConverter.cs +++ b/ReflectorNet/src/Converter/Json/IPAddressJsonConverter.cs @@ -49,7 +49,7 @@ public class IPAddressJsonConverter : JsonSchemaConverter, IJsonSchem throw new JsonException($"Invalid IPAddress format: {stringValue}"); } - public override void Write(Utf8JsonWriter writer, IPAddress value, JsonSerializerOptions options) + public override void Write(Utf8JsonWriter writer, IPAddress? value, JsonSerializerOptions options) { if (value == null) { diff --git a/ReflectorNet/src/Converter/Json/IPEndPointJsonConverter.cs b/ReflectorNet/src/Converter/Json/IPEndPointJsonConverter.cs index 44e6fd02..56faea65 100644 --- a/ReflectorNet/src/Converter/Json/IPEndPointJsonConverter.cs +++ b/ReflectorNet/src/Converter/Json/IPEndPointJsonConverter.cs @@ -97,7 +97,7 @@ public class IPEndPointJsonConverter : JsonSchemaConverter, IJsonSch return new IPEndPoint(address, port); } - public override void Write(Utf8JsonWriter writer, IPEndPoint value, JsonSerializerOptions options) + public override void Write(Utf8JsonWriter writer, IPEndPoint? value, JsonSerializerOptions options) { if (value == null) { diff --git a/ReflectorNet/src/Converter/Json/IntPtrJsonConverter.cs b/ReflectorNet/src/Converter/Json/IntPtrJsonConverter.cs index 9308b37b..3dddf357 100644 --- a/ReflectorNet/src/Converter/Json/IntPtrJsonConverter.cs +++ b/ReflectorNet/src/Converter/Json/IntPtrJsonConverter.cs @@ -47,7 +47,7 @@ public override bool CanConvert(Type typeToConvert) throw new JsonException($"Expected number or string token for IntPtr, but got {reader.TokenType}"); } - public override void Write(Utf8JsonWriter writer, object value, JsonSerializerOptions options) + public override void Write(Utf8JsonWriter writer, object? value, JsonSerializerOptions options) { if (value is null) { diff --git a/ReflectorNet/src/Converter/Json/MethodInfoConverter.cs b/ReflectorNet/src/Converter/Json/MethodInfoConverter.cs index 80e11a3b..f814019c 100644 --- a/ReflectorNet/src/Converter/Json/MethodInfoConverter.cs +++ b/ReflectorNet/src/Converter/Json/MethodInfoConverter.cs @@ -56,8 +56,13 @@ public override MethodInfo Read(ref Utf8JsonReader reader, Type typeToConvert, J return method; } - public override void Write(Utf8JsonWriter writer, MethodInfo value, JsonSerializerOptions options) + public override void Write(Utf8JsonWriter writer, MethodInfo? value, JsonSerializerOptions options) { + if (value == null) + { + writer.WriteNullValue(); + return; + } writer.WriteStartObject(); writer.WriteString(Json.Name, value.Name); writer.WriteString(Json.DeclaringType, value.DeclaringType?.GetTypeId()); diff --git a/ReflectorNet/src/Converter/Json/ParameterInfoConverter.cs b/ReflectorNet/src/Converter/Json/ParameterInfoConverter.cs index 5c4c2aff..346945a5 100644 --- a/ReflectorNet/src/Converter/Json/ParameterInfoConverter.cs +++ b/ReflectorNet/src/Converter/Json/ParameterInfoConverter.cs @@ -63,7 +63,7 @@ static class Json return parameter; } - public override void Write(Utf8JsonWriter writer, ParameterInfo value, JsonSerializerOptions options) + public override void Write(Utf8JsonWriter writer, ParameterInfo? value, JsonSerializerOptions options) { if (value == null) { diff --git a/ReflectorNet/src/Converter/Json/PropertyInfoConverter.cs b/ReflectorNet/src/Converter/Json/PropertyInfoConverter.cs index 4b0819c4..95940907 100644 --- a/ReflectorNet/src/Converter/Json/PropertyInfoConverter.cs +++ b/ReflectorNet/src/Converter/Json/PropertyInfoConverter.cs @@ -55,7 +55,7 @@ static class Json return property; } - public override void Write(Utf8JsonWriter writer, PropertyInfo value, JsonSerializerOptions options) + public override void Write(Utf8JsonWriter writer, PropertyInfo? value, JsonSerializerOptions options) { if (value == null) { diff --git a/ReflectorNet/src/Converter/Json/SerializedMemberConverter.cs b/ReflectorNet/src/Converter/Json/SerializedMemberConverter.cs index 1365c046..bd1559d3 100644 --- a/ReflectorNet/src/Converter/Json/SerializedMemberConverter.cs +++ b/ReflectorNet/src/Converter/Json/SerializedMemberConverter.cs @@ -143,7 +143,7 @@ public override IEnumerable GetDefinedTypes() throw new JsonException("Unexpected end of JSON while reading SerializedMember."); } - public override void Write(Utf8JsonWriter writer, SerializedMember value, JsonSerializerOptions options) + public override void Write(Utf8JsonWriter writer, SerializedMember? value, JsonSerializerOptions options) { if (value == null) { diff --git a/ReflectorNet/src/Converter/Json/SerializedMemberListConverter.cs b/ReflectorNet/src/Converter/Json/SerializedMemberListConverter.cs index 937f4935..b94728d5 100644 --- a/ReflectorNet/src/Converter/Json/SerializedMemberListConverter.cs +++ b/ReflectorNet/src/Converter/Json/SerializedMemberListConverter.cs @@ -65,7 +65,7 @@ public override IEnumerable GetDefinedTypes() throw new JsonException("Unexpected end of array."); } - public override void Write(Utf8JsonWriter writer, SerializedMemberList value, JsonSerializerOptions options) + public override void Write(Utf8JsonWriter writer, SerializedMemberList? value, JsonSerializerOptions options) { if (value == null) { diff --git a/ReflectorNet/src/Converter/Json/TimeOnlyJsonConverter.cs b/ReflectorNet/src/Converter/Json/TimeOnlyJsonConverter.cs index 43aca990..93ac471f 100644 --- a/ReflectorNet/src/Converter/Json/TimeOnlyJsonConverter.cs +++ b/ReflectorNet/src/Converter/Json/TimeOnlyJsonConverter.cs @@ -21,6 +21,8 @@ public class TimeOnlyJsonConverter : JsonConverter public override TimeOnly Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) { + if (reader.TokenType == JsonTokenType.Null) + throw new JsonException("Cannot convert null value to TimeOnly."); if (reader.TokenType != JsonTokenType.String) throw new JsonException($"Expected string token for TimeOnly, but got {reader.TokenType}"); diff --git a/ReflectorNet/src/Converter/Json/TypeJsonConverter.cs b/ReflectorNet/src/Converter/Json/TypeJsonConverter.cs index 572d0b2c..0d0b3b36 100644 --- a/ReflectorNet/src/Converter/Json/TypeJsonConverter.cs +++ b/ReflectorNet/src/Converter/Json/TypeJsonConverter.cs @@ -52,7 +52,7 @@ public override bool CanConvert(Type typeToConvert) throw new JsonException($"Expected string which represents System.Type in the format `System.Int32`"); } - public override void Write(Utf8JsonWriter writer, Type value, JsonSerializerOptions options) + public override void Write(Utf8JsonWriter writer, Type? value, JsonSerializerOptions options) { if (value == null) { diff --git a/ReflectorNet/src/Converter/Json/UInt16JsonConverter.cs b/ReflectorNet/src/Converter/Json/UInt16JsonConverter.cs index d2d969ce..d12fa259 100644 --- a/ReflectorNet/src/Converter/Json/UInt16JsonConverter.cs +++ b/ReflectorNet/src/Converter/Json/UInt16JsonConverter.cs @@ -62,6 +62,11 @@ public override bool CanConvert(Type typeToConvert) public override void Write(Utf8JsonWriter writer, object value, JsonSerializerOptions options) { + if (value == null) + { + writer.WriteNullValue(); + return; + } writer.WriteNumberValue((ushort)value); } diff --git a/ReflectorNet/src/Converter/Json/UInt32JsonConverter.cs b/ReflectorNet/src/Converter/Json/UInt32JsonConverter.cs index ff8d3ef1..a6c811ab 100644 --- a/ReflectorNet/src/Converter/Json/UInt32JsonConverter.cs +++ b/ReflectorNet/src/Converter/Json/UInt32JsonConverter.cs @@ -62,6 +62,11 @@ public override bool CanConvert(Type typeToConvert) public override void Write(Utf8JsonWriter writer, object value, JsonSerializerOptions options) { + if (value == null) + { + writer.WriteNullValue(); + return; + } writer.WriteNumberValue((uint)value); } diff --git a/ReflectorNet/src/Converter/Json/UInt64JsonConverter.cs b/ReflectorNet/src/Converter/Json/UInt64JsonConverter.cs index 9fdc0dec..31a2bc7c 100644 --- a/ReflectorNet/src/Converter/Json/UInt64JsonConverter.cs +++ b/ReflectorNet/src/Converter/Json/UInt64JsonConverter.cs @@ -62,6 +62,11 @@ public override bool CanConvert(Type typeToConvert) public override void Write(Utf8JsonWriter writer, object value, JsonSerializerOptions options) { + if (value == null) + { + writer.WriteNullValue(); + return; + } writer.WriteNumberValue((ulong)value); } diff --git a/ReflectorNet/src/Converter/Json/UriJsonConverter.cs b/ReflectorNet/src/Converter/Json/UriJsonConverter.cs index 65b5d039..e448fa37 100644 --- a/ReflectorNet/src/Converter/Json/UriJsonConverter.cs +++ b/ReflectorNet/src/Converter/Json/UriJsonConverter.cs @@ -34,7 +34,7 @@ public class UriJsonConverter : JsonConverter throw new JsonException($"Unable to parse '{stringValue}' as a Uri."); } - public override void Write(Utf8JsonWriter writer, Uri value, JsonSerializerOptions options) + public override void Write(Utf8JsonWriter writer, Uri? value, JsonSerializerOptions options) { if (value is null) { From 490e0b640cf840389a629c28384a4fb123fbd8a4 Mon Sep 17 00:00:00 2001 From: Ivan Murzak Date: Fri, 26 Dec 2025 03:09:57 -0800 Subject: [PATCH 17/23] Add comprehensive JSON converter tests for various types - Implement tests for JsonNode serialization and deserialization, covering objects, arrays, and null values. - Introduce tests for network types, including IPAddress and IPEndPoint serialization and deserialization. - Add tests for primitive types such as IntPtr, UIntPtr, Char, BigInteger, Complex, and Half. - Create reflection type tests for Assembly, PropertyInfo, ConstructorInfo, and ParameterInfo converters, ensuring proper handling of serialization and deserialization. --- .../CommonTypesConverterTests.cs | 328 ++++++++++++ .../DateTimeConverterTests.cs | 295 +++++++++++ .../JsonNodeConverterTests.cs | 232 ++++++++ .../NetworkTypesConverterTests.cs | 293 ++++++++++ .../PrimitiveConverterTests.cs | 500 ++++++++++++++++++ .../ReflectionConverterTests.cs | 280 ++++++++++ 6 files changed, 1928 insertions(+) create mode 100644 ReflectorNet.Tests/src/JsonConverterTests/CommonTypesConverterTests.cs create mode 100644 ReflectorNet.Tests/src/JsonConverterTests/DateTimeConverterTests.cs create mode 100644 ReflectorNet.Tests/src/JsonConverterTests/NetworkTypesConverterTests.cs create mode 100644 ReflectorNet.Tests/src/JsonConverterTests/PrimitiveConverterTests.cs create mode 100644 ReflectorNet.Tests/src/JsonConverterTests/ReflectionConverterTests.cs diff --git a/ReflectorNet.Tests/src/JsonConverterTests/CommonTypesConverterTests.cs b/ReflectorNet.Tests/src/JsonConverterTests/CommonTypesConverterTests.cs new file mode 100644 index 00000000..c31b4a4b --- /dev/null +++ b/ReflectorNet.Tests/src/JsonConverterTests/CommonTypesConverterTests.cs @@ -0,0 +1,328 @@ +using System; +using System.Text.Json; +using Xunit; +using Xunit.Abstractions; + +namespace com.IvanMurzak.ReflectorNet.Tests.JsonConverterTests +{ + /// + /// Tests for common type JSON converters: Version, Uri + /// + public class CommonTypesConverterTests : BaseTest + { + private readonly Reflector _reflector; + + public CommonTypesConverterTests(ITestOutputHelper output) : base(output) + { + _reflector = new Reflector(); + } + + #region VersionJsonConverter Tests + + [Fact] + public void Version_Write_TwoComponents() + { + var value = new Version(1, 0); + var json = _reflector.JsonSerializer.Serialize(value); + _output.WriteLine($"Version 2 components: {json}"); + Assert.Equal("\"1.0\"", json); + } + + [Fact] + public void Version_Write_ThreeComponents() + { + var value = new Version(1, 2, 3); + var json = _reflector.JsonSerializer.Serialize(value); + _output.WriteLine($"Version 3 components: {json}"); + Assert.Equal("\"1.2.3\"", json); + } + + [Fact] + public void Version_Write_FourComponents() + { + var value = new Version(1, 2, 3, 4); + var json = _reflector.JsonSerializer.Serialize(value); + _output.WriteLine($"Version 4 components: {json}"); + Assert.Equal("\"1.2.3.4\"", json); + } + + [Fact] + public void Version_Write_LargeNumbers() + { + var value = new Version(999, 888, 777, 666); + var json = _reflector.JsonSerializer.Serialize(value); + _output.WriteLine($"Version large numbers: {json}"); + Assert.Equal("\"999.888.777.666\"", json); + } + + [Fact] + public void Version_Write_Null() + { + Version? value = null; + var json = _reflector.JsonSerializer.Serialize(value); + _output.WriteLine($"Version null: {json}"); + Assert.Equal("null", json); + } + + [Fact] + public void Version_Read_TwoComponents() + { + var json = "\"2.0\""; + var result = _reflector.JsonSerializer.Deserialize(json); + _output.WriteLine($"Version from 2 components: {result}"); + Assert.NotNull(result); + Assert.Equal(2, result.Major); + Assert.Equal(0, result.Minor); + } + + [Fact] + public void Version_Read_ThreeComponents() + { + var json = "\"3.2.1\""; + var result = _reflector.JsonSerializer.Deserialize(json); + _output.WriteLine($"Version from 3 components: {result}"); + Assert.NotNull(result); + Assert.Equal(3, result.Major); + Assert.Equal(2, result.Minor); + Assert.Equal(1, result.Build); + } + + [Fact] + public void Version_Read_FourComponents() + { + var json = "\"4.3.2.1\""; + var result = _reflector.JsonSerializer.Deserialize(json); + _output.WriteLine($"Version from 4 components: {result}"); + Assert.NotNull(result); + Assert.Equal(4, result.Major); + Assert.Equal(3, result.Minor); + Assert.Equal(2, result.Build); + Assert.Equal(1, result.Revision); + } + + [Fact] + public void Version_Read_Null() + { + var json = "null"; + var result = _reflector.JsonSerializer.Deserialize(json); + _output.WriteLine($"Version from null: {result}"); + Assert.Null(result); + } + + [Fact] + public void Version_Read_EmptyString_ReturnsNull() + { + var json = "\"\""; + var result = _reflector.JsonSerializer.Deserialize(json); + _output.WriteLine($"Version from empty: {result}"); + Assert.Null(result); + } + + [Fact] + public void Version_Read_WhitespaceString_ReturnsNull() + { + var json = "\" \""; + var result = _reflector.JsonSerializer.Deserialize(json); + _output.WriteLine($"Version from whitespace: {result}"); + Assert.Null(result); + } + + [Fact] + public void Version_Read_InvalidFormat_Throws() + { + var json = "\"not.a.version\""; + Assert.ThrowsAny(() => _reflector.JsonSerializer.Deserialize(json)); + } + + [Fact] + public void Version_Read_WrongTokenType_Throws() + { + var json = "123"; + Assert.ThrowsAny(() => _reflector.JsonSerializer.Deserialize(json)); + } + + [Fact] + public void Version_RoundTrip_TwoComponents() + { + var original = new Version(5, 6); + var json = _reflector.JsonSerializer.Serialize(original); + var deserialized = _reflector.JsonSerializer.Deserialize(json); + _output.WriteLine($"Version roundtrip: {original} -> {json} -> {deserialized}"); + Assert.Equal(original, deserialized); + } + + [Fact] + public void Version_RoundTrip_FourComponents() + { + var original = new Version(1, 2, 3, 4); + var json = _reflector.JsonSerializer.Serialize(original); + var deserialized = _reflector.JsonSerializer.Deserialize(json); + _output.WriteLine($"Version roundtrip: {original} -> {json} -> {deserialized}"); + Assert.Equal(original, deserialized); + } + + #endregion + + #region UriJsonConverter Tests + + [Fact] + public void Uri_Write_HttpsUrl() + { + var value = new Uri("https://example.com/path"); + var json = _reflector.JsonSerializer.Serialize(value); + _output.WriteLine($"Uri https: {json}"); + Assert.Equal("\"https://example.com/path\"", json); + } + + [Fact] + public void Uri_Write_HttpUrl() + { + var value = new Uri("http://localhost:8080/api"); + var json = _reflector.JsonSerializer.Serialize(value); + _output.WriteLine($"Uri http: {json}"); + Assert.Equal("\"http://localhost:8080/api\"", json); + } + + [Fact] + public void Uri_Write_FileUrl() + { + var value = new Uri("file:///C:/path/to/file.txt"); + var json = _reflector.JsonSerializer.Serialize(value); + _output.WriteLine($"Uri file: {json}"); + Assert.Contains("file:///", json); + } + + [Fact] + public void Uri_Write_RelativeUrl() + { + var value = new Uri("/api/endpoint", UriKind.Relative); + var json = _reflector.JsonSerializer.Serialize(value); + _output.WriteLine($"Uri relative: {json}"); + Assert.Equal("\"/api/endpoint\"", json); + } + + [Fact] + public void Uri_Write_UrlWithQueryString() + { + var value = new Uri("https://example.com/search?q=test&page=1"); + var json = _reflector.JsonSerializer.Serialize(value); + _output.WriteLine($"Uri with query: {json}"); + Assert.Contains("q=test", json); + Assert.Contains("page=1", json); + } + + [Fact] + public void Uri_Write_UrlWithFragment() + { + var value = new Uri("https://example.com/page#section"); + var json = _reflector.JsonSerializer.Serialize(value); + _output.WriteLine($"Uri with fragment: {json}"); + Assert.Contains("#section", json); + } + + [Fact] + public void Uri_Write_Null() + { + Uri? value = null; + var json = _reflector.JsonSerializer.Serialize(value); + _output.WriteLine($"Uri null: {json}"); + Assert.Equal("null", json); + } + + [Fact] + public void Uri_Read_HttpsUrl() + { + var json = "\"https://example.com/test\""; + var result = _reflector.JsonSerializer.Deserialize(json); + _output.WriteLine($"Uri from https: {result}"); + Assert.NotNull(result); + Assert.Equal("https", result.Scheme); + Assert.Equal("example.com", result.Host); + } + + [Fact] + public void Uri_Read_RelativeUrl() + { + var json = "\"/api/users\""; + var result = _reflector.JsonSerializer.Deserialize(json); + _output.WriteLine($"Uri from relative: {result}"); + Assert.NotNull(result); + Assert.False(result.IsAbsoluteUri); + } + + [Fact] + public void Uri_Read_Null() + { + var json = "null"; + var result = _reflector.JsonSerializer.Deserialize(json); + _output.WriteLine($"Uri from null: {result}"); + Assert.Null(result); + } + + [Fact] + public void Uri_Read_EmptyString_ReturnsNull() + { + var json = "\"\""; + var result = _reflector.JsonSerializer.Deserialize(json); + _output.WriteLine($"Uri from empty: {result}"); + Assert.Null(result); + } + + [Fact] + public void Uri_Read_WhitespaceString_ReturnsNull() + { + var json = "\" \""; + var result = _reflector.JsonSerializer.Deserialize(json); + _output.WriteLine($"Uri from whitespace: {result}"); + Assert.Null(result); + } + + [Fact] + public void Uri_Read_WrongTokenType_Throws() + { + var json = "12345"; + Assert.ThrowsAny(() => _reflector.JsonSerializer.Deserialize(json)); + } + + [Fact] + public void Uri_RoundTrip_AbsoluteUrl() + { + var original = new Uri("https://api.example.com:8443/v1/users?active=true"); + var json = _reflector.JsonSerializer.Serialize(original); + var deserialized = _reflector.JsonSerializer.Deserialize(json); + _output.WriteLine($"Uri roundtrip: {original} -> {json} -> {deserialized}"); + Assert.Equal(original, deserialized); + } + + [Fact] + public void Uri_RoundTrip_RelativeUrl() + { + var original = new Uri("/path/to/resource", UriKind.Relative); + var json = _reflector.JsonSerializer.Serialize(original); + var deserialized = _reflector.JsonSerializer.Deserialize(json); + _output.WriteLine($"Uri roundtrip relative: {original} -> {json} -> {deserialized}"); + Assert.Equal(original.OriginalString, deserialized?.OriginalString); + } + + [Fact] + public void Uri_Read_EncodedCharacters() + { + var json = "\"https://example.com/path%20with%20spaces\""; + var result = _reflector.JsonSerializer.Deserialize(json); + _output.WriteLine($"Uri with encoded chars: {result}"); + Assert.NotNull(result); + Assert.Contains("path", result.AbsolutePath); + } + + [Fact] + public void Uri_Read_UnicodeCharacters() + { + var json = "\"https://example.com/\u4E2D\u6587\""; + var result = _reflector.JsonSerializer.Deserialize(json); + _output.WriteLine($"Uri with unicode: {result}"); + Assert.NotNull(result); + } + + #endregion + } +} diff --git a/ReflectorNet.Tests/src/JsonConverterTests/DateTimeConverterTests.cs b/ReflectorNet.Tests/src/JsonConverterTests/DateTimeConverterTests.cs new file mode 100644 index 00000000..e787624c --- /dev/null +++ b/ReflectorNet.Tests/src/JsonConverterTests/DateTimeConverterTests.cs @@ -0,0 +1,295 @@ +using System; +using System.Text.Json; +using Xunit; +using Xunit.Abstractions; + +namespace com.IvanMurzak.ReflectorNet.Tests.JsonConverterTests +{ + /// + /// Tests for date/time JSON converters: DateOnly, TimeOnly (NET6+) + /// + public class DateTimeConverterTests : BaseTest + { + private readonly Reflector _reflector; + + public DateTimeConverterTests(ITestOutputHelper output) : base(output) + { + _reflector = new Reflector(); + } + +#if NET6_0_OR_GREATER + + #region DateOnlyJsonConverter Tests + + [Fact] + public void DateOnly_Write_StandardDate() + { + var value = new DateOnly(2024, 12, 25); + var json = _reflector.JsonSerializer.Serialize(value); + _output.WriteLine($"DateOnly standard: {json}"); + Assert.Equal("\"2024-12-25\"", json); + } + + [Fact] + public void DateOnly_Write_FirstDayOfYear() + { + var value = new DateOnly(2024, 1, 1); + var json = _reflector.JsonSerializer.Serialize(value); + _output.WriteLine($"DateOnly first day: {json}"); + Assert.Equal("\"2024-01-01\"", json); + } + + [Fact] + public void DateOnly_Write_LastDayOfYear() + { + var value = new DateOnly(2024, 12, 31); + var json = _reflector.JsonSerializer.Serialize(value); + _output.WriteLine($"DateOnly last day: {json}"); + Assert.Equal("\"2024-12-31\"", json); + } + + [Fact] + public void DateOnly_Write_LeapYearDate() + { + var value = new DateOnly(2024, 2, 29); + var json = _reflector.JsonSerializer.Serialize(value); + _output.WriteLine($"DateOnly leap year: {json}"); + Assert.Equal("\"2024-02-29\"", json); + } + + [Fact] + public void DateOnly_Write_MinValue() + { + var value = DateOnly.MinValue; + var json = _reflector.JsonSerializer.Serialize(value); + _output.WriteLine($"DateOnly min: {json}"); + Assert.Equal("\"0001-01-01\"", json); + } + + [Fact] + public void DateOnly_Write_MaxValue() + { + var value = DateOnly.MaxValue; + var json = _reflector.JsonSerializer.Serialize(value); + _output.WriteLine($"DateOnly max: {json}"); + Assert.Equal("\"9999-12-31\"", json); + } + + [Fact] + public void DateOnly_Read_StandardDate() + { + var json = "\"2024-06-15\""; + var result = _reflector.JsonSerializer.Deserialize(json); + _output.WriteLine($"DateOnly from standard: {result}"); + Assert.Equal(new DateOnly(2024, 6, 15), result); + } + + [Fact] + public void DateOnly_Read_SingleDigitMonthDay() + { + var json = "\"2024-01-05\""; + var result = _reflector.JsonSerializer.Deserialize(json); + _output.WriteLine($"DateOnly single digit: {result}"); + Assert.Equal(new DateOnly(2024, 1, 5), result); + } + + [Fact] + public void DateOnly_Read_InvalidFormat_Throws() + { + var json = "\"25-12-2024\""; // wrong format + Assert.ThrowsAny(() => _reflector.JsonSerializer.Deserialize(json)); + } + + [Fact] + public void DateOnly_Read_WrongTokenType_Throws() + { + var json = "12345"; + Assert.ThrowsAny(() => _reflector.JsonSerializer.Deserialize(json)); + } + + [Fact] + public void DateOnly_Read_Null_Throws() + { + var json = "null"; + Assert.ThrowsAny(() => _reflector.JsonSerializer.Deserialize(json)); + } + + [Fact] + public void DateOnly_RoundTrip() + { + var original = new DateOnly(2025, 7, 4); + var json = _reflector.JsonSerializer.Serialize(original); + var deserialized = _reflector.JsonSerializer.Deserialize(json); + _output.WriteLine($"DateOnly roundtrip: {original} -> {json} -> {deserialized}"); + Assert.Equal(original, deserialized); + } + + [Fact] + public void DateOnly_RoundTrip_Various() + { + var dates = new[] + { + new DateOnly(2000, 1, 1), + new DateOnly(2024, 2, 29), + new DateOnly(1999, 12, 31), + new DateOnly(2050, 6, 15) + }; + + foreach (var original in dates) + { + var json = _reflector.JsonSerializer.Serialize(original); + var deserialized = _reflector.JsonSerializer.Deserialize(json); + _output.WriteLine($"DateOnly roundtrip: {original} -> {json} -> {deserialized}"); + Assert.Equal(original, deserialized); + } + } + + #endregion + + #region TimeOnlyJsonConverter Tests + + [Fact] + public void TimeOnly_Write_Morning() + { + var value = new TimeOnly(9, 30, 0); + var json = _reflector.JsonSerializer.Serialize(value); + _output.WriteLine($"TimeOnly morning: {json}"); + Assert.Equal("\"09:30:00.0000000\"", json); + } + + [Fact] + public void TimeOnly_Write_Afternoon() + { + var value = new TimeOnly(14, 45, 30); + var json = _reflector.JsonSerializer.Serialize(value); + _output.WriteLine($"TimeOnly afternoon: {json}"); + Assert.Equal("\"14:45:30.0000000\"", json); + } + + [Fact] + public void TimeOnly_Write_Midnight() + { + var value = new TimeOnly(0, 0, 0); + var json = _reflector.JsonSerializer.Serialize(value); + _output.WriteLine($"TimeOnly midnight: {json}"); + Assert.Equal("\"00:00:00.0000000\"", json); + } + + [Fact] + public void TimeOnly_Write_LastSecond() + { + var value = new TimeOnly(23, 59, 59); + var json = _reflector.JsonSerializer.Serialize(value); + _output.WriteLine($"TimeOnly last second: {json}"); + Assert.Equal("\"23:59:59.0000000\"", json); + } + + [Fact] + public void TimeOnly_Write_WithMilliseconds() + { + var value = new TimeOnly(12, 30, 45, 123); + var json = _reflector.JsonSerializer.Serialize(value); + _output.WriteLine($"TimeOnly with ms: {json}"); + Assert.Contains("12:30:45", json); + } + + [Fact] + public void TimeOnly_Write_WithTicks() + { + var value = new TimeOnly(10, 20, 30).Add(TimeSpan.FromTicks(5000)); + var json = _reflector.JsonSerializer.Serialize(value); + _output.WriteLine($"TimeOnly with ticks: {json}"); + Assert.Contains("10:20:30", json); + } + + [Fact] + public void TimeOnly_Write_MinValue() + { + var value = TimeOnly.MinValue; + var json = _reflector.JsonSerializer.Serialize(value); + _output.WriteLine($"TimeOnly min: {json}"); + Assert.Equal("\"00:00:00.0000000\"", json); + } + + [Fact] + public void TimeOnly_Write_MaxValue() + { + var value = TimeOnly.MaxValue; + var json = _reflector.JsonSerializer.Serialize(value); + _output.WriteLine($"TimeOnly max: {json}"); + Assert.Contains("23:59:59.9999999", json); + } + + [Fact] + public void TimeOnly_Read_Standard() + { + var json = "\"15:30:45.0000000\""; + var result = _reflector.JsonSerializer.Deserialize(json); + _output.WriteLine($"TimeOnly from standard: {result}"); + Assert.Equal(15, result.Hour); + Assert.Equal(30, result.Minute); + Assert.Equal(45, result.Second); + } + + [Fact] + public void TimeOnly_Read_Midnight() + { + var json = "\"00:00:00.0000000\""; + var result = _reflector.JsonSerializer.Deserialize(json); + _output.WriteLine($"TimeOnly from midnight: {result}"); + Assert.Equal(TimeOnly.MinValue, result); + } + + [Fact] + public void TimeOnly_Read_WrongTokenType_Throws() + { + var json = "12345"; + Assert.ThrowsAny(() => _reflector.JsonSerializer.Deserialize(json)); + } + + [Fact] + public void TimeOnly_Read_InvalidFormat_Throws() + { + var json = "\"3:30 PM\""; // wrong format + Assert.ThrowsAny(() => _reflector.JsonSerializer.Deserialize(json)); + } + + [Fact] + public void TimeOnly_RoundTrip() + { + var original = new TimeOnly(16, 45, 30, 250); + var json = _reflector.JsonSerializer.Serialize(original); + var deserialized = _reflector.JsonSerializer.Deserialize(json); + _output.WriteLine($"TimeOnly roundtrip: {original} -> {json} -> {deserialized}"); + Assert.Equal(original.Hour, deserialized.Hour); + Assert.Equal(original.Minute, deserialized.Minute); + Assert.Equal(original.Second, deserialized.Second); + } + + [Fact] + public void TimeOnly_RoundTrip_Various() + { + var times = new[] + { + new TimeOnly(0, 0, 0), + new TimeOnly(12, 0, 0), + new TimeOnly(23, 59, 59), + new TimeOnly(6, 30, 15) + }; + + foreach (var original in times) + { + var json = _reflector.JsonSerializer.Serialize(original); + var deserialized = _reflector.JsonSerializer.Deserialize(json); + _output.WriteLine($"TimeOnly roundtrip: {original} -> {json} -> {deserialized}"); + Assert.Equal(original.Hour, deserialized.Hour); + Assert.Equal(original.Minute, deserialized.Minute); + Assert.Equal(original.Second, deserialized.Second); + } + } + + #endregion + +#endif + } +} diff --git a/ReflectorNet.Tests/src/JsonConverterTests/JsonNodeConverterTests.cs b/ReflectorNet.Tests/src/JsonConverterTests/JsonNodeConverterTests.cs index 129ea192..804ea1f1 100644 --- a/ReflectorNet.Tests/src/JsonConverterTests/JsonNodeConverterTests.cs +++ b/ReflectorNet.Tests/src/JsonConverterTests/JsonNodeConverterTests.cs @@ -743,5 +743,237 @@ public void AllConverters_WorkTogether_InComplexStructure() } #endregion + + #region JsonNode Base Type Tests + + [Fact] + public void JsonNode_Serialize_AsObject_ShouldSucceed() + { + // Arrange + JsonNode node = new JsonObject { ["key"] = "value" }; + + // Act + var json = _reflector.JsonSerializer.Serialize(node); + _output.WriteLine($"JsonNode as object: {json}"); + + // Assert + Assert.Contains("\"key\"", json); + Assert.Contains("value", json); + } + + [Fact] + public void JsonNode_Serialize_AsArray_ShouldSucceed() + { + // Arrange + JsonNode node = new JsonArray { 1, 2, 3 }; + + // Act + var json = _reflector.JsonSerializer.Serialize(node); + _output.WriteLine($"JsonNode as array: {json}"); + + // Assert + Assert.Contains("1", json); + Assert.Contains("2", json); + Assert.Contains("3", json); + } + + [Fact] + public void JsonNode_Serialize_Null_ShouldSucceed() + { + // Arrange + JsonNode? node = null; + + // Act + var json = _reflector.JsonSerializer.Serialize(node); + _output.WriteLine($"JsonNode null: {json}"); + + // Assert + Assert.Equal("null", json); + } + + [Fact] + public void JsonNode_Deserialize_Object_ShouldReturnJsonObject() + { + // Arrange + var json = "{\"name\":\"test\"}"; + + // Act + var node = _reflector.JsonSerializer.Deserialize(json); + _output.WriteLine($"JsonNode from object: {node?.GetType().Name}"); + + // Assert + Assert.NotNull(node); + Assert.IsType(node); + } + + [Fact] + public void JsonNode_Deserialize_Array_ShouldReturnJsonArray() + { + // Arrange + var json = "[1,2,3]"; + + // Act + var node = _reflector.JsonSerializer.Deserialize(json); + _output.WriteLine($"JsonNode from array: {node?.GetType().Name}"); + + // Assert + Assert.NotNull(node); + Assert.IsType(node); + } + + [Fact] + public void JsonNode_RoundTrip_Object_ShouldPreserveData() + { + // Arrange + JsonNode original = new JsonObject + { + ["id"] = 123, + ["items"] = new JsonArray { "a", "b", "c" } + }; + + // Act + var json = _reflector.JsonSerializer.Serialize(original); + var deserialized = _reflector.JsonSerializer.Deserialize(json); + _output.WriteLine($"JsonNode roundtrip: {json}"); + + // Assert + Assert.NotNull(deserialized); + Assert.Equal(123, deserialized["id"]?.GetValue()); + } + + [Fact] + public void JsonNode_RoundTrip_Array_ShouldPreserveData() + { + // Arrange + JsonNode original = new JsonArray { 10, 20, 30 }; + + // Act + var json = _reflector.JsonSerializer.Serialize(original); + var deserialized = _reflector.JsonSerializer.Deserialize(json); + _output.WriteLine($"JsonNode array roundtrip: {json}"); + + // Assert + Assert.NotNull(deserialized); + Assert.IsType(deserialized); + Assert.Equal(3, ((JsonArray)deserialized).Count); + } + + #endregion + + #region JsonValue Tests + + [Fact] + public void JsonValue_Serialize_String_ShouldSucceed() + { + // Arrange + var value = JsonValue.Create("hello world")!; + + // Act + var json = _reflector.JsonSerializer.Serialize(value); + _output.WriteLine($"JsonValue string: {json}"); + + // Assert + Assert.Equal("\"hello world\"", json); + } + + [Fact] + public void JsonValue_Serialize_Number_ShouldSucceed() + { + // Arrange + var value = JsonValue.Create(42)!; + + // Act + var json = _reflector.JsonSerializer.Serialize(value); + _output.WriteLine($"JsonValue number: {json}"); + + // Assert + Assert.Equal("42", json); + } + + [Fact] + public void JsonValue_Serialize_Double_ShouldSucceed() + { + // Arrange + var value = JsonValue.Create(3.14159)!; + + // Act + var json = _reflector.JsonSerializer.Serialize(value); + _output.WriteLine($"JsonValue double: {json}"); + + // Assert + Assert.Contains("3.14159", json); + } + + [Fact] + public void JsonValue_Serialize_Boolean_True_ShouldSucceed() + { + // Arrange + var value = JsonValue.Create(true)!; + + // Act + var json = _reflector.JsonSerializer.Serialize(value); + _output.WriteLine($"JsonValue bool true: {json}"); + + // Assert + Assert.Equal("true", json); + } + + [Fact] + public void JsonValue_Serialize_Boolean_False_ShouldSucceed() + { + // Arrange + var value = JsonValue.Create(false)!; + + // Act + var json = _reflector.JsonSerializer.Serialize(value); + _output.WriteLine($"JsonValue bool false: {json}"); + + // Assert + Assert.Equal("false", json); + } + + [Fact] + public void JsonValue_Serialize_Null_ShouldSucceed() + { + // Arrange + JsonValue? value = null; + + // Act + var json = _reflector.JsonSerializer.Serialize(value); + _output.WriteLine($"JsonValue null: {json}"); + + // Assert + Assert.Equal("null", json); + } + + [Fact] + public void JsonValue_Serialize_LargeNumber_ShouldSucceed() + { + // Arrange + var value = JsonValue.Create(9999999999999L)!; + + // Act + var json = _reflector.JsonSerializer.Serialize(value); + _output.WriteLine($"JsonValue large number: {json}"); + + // Assert + Assert.Equal("9999999999999", json); + } + + [Fact] + public void JsonValue_Serialize_NegativeNumber_ShouldSucceed() + { + // Arrange + var value = JsonValue.Create(-42)!; + + // Act + var json = _reflector.JsonSerializer.Serialize(value); + _output.WriteLine($"JsonValue negative: {json}"); + + // Assert + Assert.Equal("-42", json); + } + + #endregion } } diff --git a/ReflectorNet.Tests/src/JsonConverterTests/NetworkTypesConverterTests.cs b/ReflectorNet.Tests/src/JsonConverterTests/NetworkTypesConverterTests.cs new file mode 100644 index 00000000..9f907c62 --- /dev/null +++ b/ReflectorNet.Tests/src/JsonConverterTests/NetworkTypesConverterTests.cs @@ -0,0 +1,293 @@ +using System; +using System.Net; +using System.Text.Json; +using Xunit; +using Xunit.Abstractions; + +namespace com.IvanMurzak.ReflectorNet.Tests.JsonConverterTests +{ + /// + /// Tests for network type JSON converters: IPAddress, IPEndPoint + /// + public class NetworkTypesConverterTests : BaseTest + { + private readonly Reflector _reflector; + + public NetworkTypesConverterTests(ITestOutputHelper output) : base(output) + { + _reflector = new Reflector(); + } + + #region IPAddressJsonConverter Tests + + [Fact] + public void IPAddress_Serialize_IPv4() + { + var value = IPAddress.Parse("192.168.1.1"); + var json = _reflector.JsonSerializer.Serialize(value, typeof(IPAddress)); + _output.WriteLine($"IPAddress IPv4: {json}"); + Assert.Contains("192.168.1.1", json); + } + + [Fact] + public void IPAddress_Serialize_Loopback() + { + var value = IPAddress.Parse("127.0.0.1"); + var json = _reflector.JsonSerializer.Serialize(value, typeof(IPAddress)); + _output.WriteLine($"IPAddress loopback: {json}"); + Assert.Contains("127.0.0.1", json); + } + + [Fact] + public void IPAddress_Serialize_Any() + { + var value = IPAddress.Parse("0.0.0.0"); + var json = _reflector.JsonSerializer.Serialize(value, typeof(IPAddress)); + _output.WriteLine($"IPAddress any: {json}"); + Assert.Contains("0.0.0.0", json); + } + + [Fact] + public void IPAddress_Serialize_IPv6() + { + var value = IPAddress.Parse("::1"); + var json = _reflector.JsonSerializer.Serialize(value, typeof(IPAddress)); + _output.WriteLine($"IPAddress IPv6 loopback: {json}"); + Assert.Contains("::1", json); + } + + [Fact] + public void IPAddress_Serialize_IPv6_Full() + { + var value = IPAddress.Parse("2001:0db8:85a3:0000:0000:8a2e:0370:7334"); + var json = _reflector.JsonSerializer.Serialize(value, typeof(IPAddress)); + _output.WriteLine($"IPAddress IPv6 full: {json}"); + Assert.Contains("2001:", json); + } + + [Fact] + public void IPAddress_Serialize_Null() + { + IPAddress? value = null; + var json = _reflector.JsonSerializer.Serialize(value, typeof(IPAddress)); + _output.WriteLine($"IPAddress null: {json}"); + Assert.Equal("null", json); + } + + [Fact] + public void IPAddress_Read_IPv4() + { + var json = "\"10.0.0.1\""; + var result = _reflector.JsonSerializer.Deserialize(json); + _output.WriteLine($"IPAddress from IPv4: {result}"); + Assert.NotNull(result); + Assert.Equal(IPAddress.Parse("10.0.0.1"), result); + } + + [Fact] + public void IPAddress_Read_IPv6() + { + var json = "\"::1\""; + var result = _reflector.JsonSerializer.Deserialize(json); + _output.WriteLine($"IPAddress from IPv6: {result}"); + Assert.NotNull(result); + Assert.Equal(IPAddress.IPv6Loopback, result); + } + + [Fact] + public void IPAddress_Read_Null() + { + var json = "null"; + var result = _reflector.JsonSerializer.Deserialize(json); + _output.WriteLine($"IPAddress from null: {result}"); + Assert.Null(result); + } + + [Fact] + public void IPAddress_Read_InvalidFormat_Throws() + { + var json = "\"not.an.ip\""; + Assert.ThrowsAny(() => _reflector.JsonSerializer.Deserialize(json)); + } + + [Fact] + public void IPAddress_Read_WrongTokenType_Throws() + { + var json = "12345"; + Assert.ThrowsAny(() => _reflector.JsonSerializer.Deserialize(json)); + } + + [Fact] + public void IPAddress_RoundTrip_IPv4() + { + var original = IPAddress.Parse("172.16.0.1"); + var json = _reflector.JsonSerializer.Serialize(original, typeof(IPAddress)); + var deserialized = _reflector.JsonSerializer.Deserialize(json); + _output.WriteLine($"IPAddress roundtrip IPv4: {original} -> {json} -> {deserialized}"); + Assert.Equal(original, deserialized); + } + + [Fact] + public void IPAddress_RoundTrip_IPv6() + { + var original = IPAddress.Parse("fe80::1"); + var json = _reflector.JsonSerializer.Serialize(original, typeof(IPAddress)); + var deserialized = _reflector.JsonSerializer.Deserialize(json); + _output.WriteLine($"IPAddress roundtrip IPv6: {original} -> {json} -> {deserialized}"); + Assert.Equal(original, deserialized); + } + + #endregion + + #region IPEndPointJsonConverter Tests + + [Fact] + public void IPEndPoint_Serialize_IPv4() + { + var value = new IPEndPoint(IPAddress.Parse("192.168.1.1"), 8080); + var json = _reflector.JsonSerializer.Serialize(value, typeof(IPEndPoint)); + _output.WriteLine($"IPEndPoint IPv4: {json}"); + Assert.Contains("\"address\"", json); + Assert.Contains("192.168.1.1", json); + Assert.Contains("\"port\"", json); + Assert.Contains("8080", json); + } + + [Fact] + public void IPEndPoint_Serialize_IPv6() + { + var value = new IPEndPoint(IPAddress.Parse("::1"), 443); + var json = _reflector.JsonSerializer.Serialize(value, typeof(IPEndPoint)); + _output.WriteLine($"IPEndPoint IPv6: {json}"); + Assert.Contains("\"address\"", json); + Assert.Contains("::1", json); + Assert.Contains("\"port\"", json); + Assert.Contains("443", json); + } + + [Fact] + public void IPEndPoint_Serialize_Port0() + { + var value = new IPEndPoint(IPAddress.Parse("127.0.0.1"), 0); + var json = _reflector.JsonSerializer.Serialize(value, typeof(IPEndPoint)); + _output.WriteLine($"IPEndPoint port 0: {json}"); + Assert.Contains("\"port\"", json); + } + + [Fact] + public void IPEndPoint_Serialize_MaxPort() + { + var value = new IPEndPoint(IPAddress.Parse("127.0.0.1"), 65535); + var json = _reflector.JsonSerializer.Serialize(value, typeof(IPEndPoint)); + _output.WriteLine($"IPEndPoint max port: {json}"); + Assert.Contains("\"port\"", json); + Assert.Contains("65535", json); + } + + [Fact] + public void IPEndPoint_Serialize_Null() + { + IPEndPoint? value = null; + var json = _reflector.JsonSerializer.Serialize(value, typeof(IPEndPoint)); + _output.WriteLine($"IPEndPoint null: {json}"); + Assert.Equal("null", json); + } + + [Fact] + public void IPEndPoint_Read_IPv4() + { + var json = "{\"address\":\"10.0.0.1\",\"port\":3000}"; + var result = _reflector.JsonSerializer.Deserialize(json); + _output.WriteLine($"IPEndPoint from IPv4: {result}"); + Assert.NotNull(result); + Assert.Equal(IPAddress.Parse("10.0.0.1"), result.Address); + Assert.Equal(3000, result.Port); + } + + [Fact] + public void IPEndPoint_Read_IPv6() + { + var json = "{\"address\":\"::1\",\"port\":8443}"; + var result = _reflector.JsonSerializer.Deserialize(json); + _output.WriteLine($"IPEndPoint from IPv6: {result}"); + Assert.NotNull(result); + Assert.Equal(IPAddress.IPv6Loopback, result.Address); + Assert.Equal(8443, result.Port); + } + + [Fact] + public void IPEndPoint_Read_CaseInsensitive() + { + var json = "{\"ADDRESS\":\"127.0.0.1\",\"PORT\":80}"; + var result = _reflector.JsonSerializer.Deserialize(json); + _output.WriteLine($"IPEndPoint case insensitive: {result}"); + Assert.NotNull(result); + Assert.Equal(IPAddress.Loopback, result.Address); + Assert.Equal(80, result.Port); + } + + [Fact] + public void IPEndPoint_Read_Null() + { + var json = "null"; + var result = _reflector.JsonSerializer.Deserialize(json); + _output.WriteLine($"IPEndPoint from null: {result}"); + Assert.Null(result); + } + + [Fact] + public void IPEndPoint_Read_MissingAddress_Throws() + { + var json = "{\"port\":8080}"; + Assert.ThrowsAny(() => _reflector.JsonSerializer.Deserialize(json)); + } + + [Fact] + public void IPEndPoint_Read_MissingPort_Throws() + { + var json = "{\"address\":\"127.0.0.1\"}"; + Assert.ThrowsAny(() => _reflector.JsonSerializer.Deserialize(json)); + } + + [Fact] + public void IPEndPoint_Read_WrongTokenType_Throws() + { + var json = "\"127.0.0.1:8080\""; + Assert.ThrowsAny(() => _reflector.JsonSerializer.Deserialize(json)); + } + + [Fact] + public void IPEndPoint_RoundTrip_IPv4() + { + var original = new IPEndPoint(IPAddress.Parse("192.168.0.1"), 5000); + var json = _reflector.JsonSerializer.Serialize(original, typeof(IPEndPoint)); + var deserialized = _reflector.JsonSerializer.Deserialize(json); + _output.WriteLine($"IPEndPoint roundtrip: {original} -> {json} -> {deserialized}"); + Assert.Equal(original.Address, deserialized?.Address); + Assert.Equal(original.Port, deserialized?.Port); + } + + [Fact] + public void IPEndPoint_RoundTrip_IPv6() + { + var original = new IPEndPoint(IPAddress.Parse("fe80::1"), 9000); + var json = _reflector.JsonSerializer.Serialize(original, typeof(IPEndPoint)); + var deserialized = _reflector.JsonSerializer.Deserialize(json); + _output.WriteLine($"IPEndPoint roundtrip IPv6: {original} -> {json} -> {deserialized}"); + Assert.Equal(original.Address, deserialized?.Address); + Assert.Equal(original.Port, deserialized?.Port); + } + + [Fact] + public void IPEndPoint_Read_ExtraProperties_Ignored() + { + var json = "{\"address\":\"127.0.0.1\",\"port\":80,\"extra\":\"ignored\"}"; + var result = _reflector.JsonSerializer.Deserialize(json); + _output.WriteLine($"IPEndPoint with extra props: {result}"); + Assert.NotNull(result); + Assert.Equal(80, result.Port); + } + + #endregion + } +} diff --git a/ReflectorNet.Tests/src/JsonConverterTests/PrimitiveConverterTests.cs b/ReflectorNet.Tests/src/JsonConverterTests/PrimitiveConverterTests.cs new file mode 100644 index 00000000..10ab6fbf --- /dev/null +++ b/ReflectorNet.Tests/src/JsonConverterTests/PrimitiveConverterTests.cs @@ -0,0 +1,500 @@ +using System; +using System.Numerics; +using System.Text.Json; +using com.IvanMurzak.ReflectorNet.Json; +using Xunit; +using Xunit.Abstractions; + +namespace com.IvanMurzak.ReflectorNet.Tests.JsonConverterTests +{ + /// + /// Tests for primitive type JSON converters: IntPtr, UIntPtr, Char, BigInteger, Complex, Half + /// + public class PrimitiveConverterTests : BaseTest + { + private readonly Reflector _reflector; + + public PrimitiveConverterTests(ITestOutputHelper output) : base(output) + { + _reflector = new Reflector(); + } + + #region IntPtrJsonConverter Tests + + [Fact] + public void IntPtr_Write_PositiveValue() + { + var value = new IntPtr(12345); + var json = _reflector.JsonSerializer.Serialize(value); + _output.WriteLine($"IntPtr positive: {json}"); + Assert.Equal("12345", json); + } + + [Fact] + public void IntPtr_Write_NegativeValue() + { + var value = new IntPtr(-98765); + var json = _reflector.JsonSerializer.Serialize(value); + _output.WriteLine($"IntPtr negative: {json}"); + Assert.Equal("-98765", json); + } + + [Fact] + public void IntPtr_Write_Zero() + { + var value = IntPtr.Zero; + var json = _reflector.JsonSerializer.Serialize(value); + _output.WriteLine($"IntPtr zero: {json}"); + Assert.Equal("0", json); + } + + [Fact] + public void IntPtr_Write_MaxValue() + { + var value = new IntPtr(long.MaxValue); + var json = _reflector.JsonSerializer.Serialize(value); + _output.WriteLine($"IntPtr max: {json}"); + Assert.Equal(long.MaxValue.ToString(), json); + } + + [Fact] + public void IntPtr_Write_MinValue() + { + var value = new IntPtr(long.MinValue); + var json = _reflector.JsonSerializer.Serialize(value); + _output.WriteLine($"IntPtr min: {json}"); + Assert.Equal(long.MinValue.ToString(), json); + } + + [Fact] + public void IntPtr_Read_FromNumber() + { + var json = "42"; + var result = _reflector.JsonSerializer.Deserialize(json); + _output.WriteLine($"IntPtr from number: {result}"); + Assert.Equal(new IntPtr(42), result); + } + + [Fact] + public void IntPtr_Read_FromString() + { + var json = "\"99999\""; + var result = _reflector.JsonSerializer.Deserialize(json); + _output.WriteLine($"IntPtr from string: {result}"); + Assert.Equal(new IntPtr(99999), result); + } + + [Fact] + public void IntPtr_Read_Null_AsNullable_ReturnsNull() + { + var json = "null"; + var result = _reflector.JsonSerializer.Deserialize(json); + _output.WriteLine($"IntPtr? from null: {result}"); + Assert.Null(result); + } + + [Fact] + public void IntPtr_RoundTrip() + { + var original = new IntPtr(123456789); + var json = _reflector.JsonSerializer.Serialize(original); + var deserialized = _reflector.JsonSerializer.Deserialize(json); + _output.WriteLine($"IntPtr roundtrip: {original} -> {json} -> {deserialized}"); + Assert.Equal(original, deserialized); + } + + #endregion + + #region UIntPtrJsonConverter Tests + + [Fact] + public void UIntPtr_Write_PositiveValue() + { + var value = new UIntPtr(12345); + var json = _reflector.JsonSerializer.Serialize(value); + _output.WriteLine($"UIntPtr positive: {json}"); + Assert.Equal("12345", json); + } + + [Fact] + public void UIntPtr_Write_Zero() + { + var value = UIntPtr.Zero; + var json = _reflector.JsonSerializer.Serialize(value); + _output.WriteLine($"UIntPtr zero: {json}"); + Assert.Equal("0", json); + } + + [Fact] + public void UIntPtr_Write_MaxValue() + { + var value = new UIntPtr(ulong.MaxValue); + var json = _reflector.JsonSerializer.Serialize(value); + _output.WriteLine($"UIntPtr max: {json}"); + Assert.Equal(ulong.MaxValue.ToString(), json); + } + + [Fact] + public void UIntPtr_Read_FromNumber() + { + var json = "42"; + var result = _reflector.JsonSerializer.Deserialize(json); + _output.WriteLine($"UIntPtr from number: {result}"); + Assert.Equal(new UIntPtr(42), result); + } + + [Fact] + public void UIntPtr_Read_FromString() + { + var json = "\"99999\""; + var result = _reflector.JsonSerializer.Deserialize(json); + _output.WriteLine($"UIntPtr from string: {result}"); + Assert.Equal(new UIntPtr(99999), result); + } + + [Fact] + public void UIntPtr_Read_Null_AsNullable_ReturnsNull() + { + var json = "null"; + var result = _reflector.JsonSerializer.Deserialize(json); + _output.WriteLine($"UIntPtr? from null: {result}"); + Assert.Null(result); + } + + [Fact] + public void UIntPtr_RoundTrip() + { + var original = new UIntPtr(987654321); + var json = _reflector.JsonSerializer.Serialize(original); + var deserialized = _reflector.JsonSerializer.Deserialize(json); + _output.WriteLine($"UIntPtr roundtrip: {original} -> {json} -> {deserialized}"); + Assert.Equal(original, deserialized); + } + + #endregion + + #region CharJsonConverter Tests + + [Fact] + public void Char_Write_Letter() + { + var value = 'A'; + var json = _reflector.JsonSerializer.Serialize(value); + _output.WriteLine($"Char letter: {json}"); + Assert.Equal("\"A\"", json); + } + + [Fact] + public void Char_Write_Digit() + { + var value = '7'; + var json = _reflector.JsonSerializer.Serialize(value); + _output.WriteLine($"Char digit: {json}"); + Assert.Equal("\"7\"", json); + } + + [Fact] + public void Char_Write_SpecialChar() + { + var value = '@'; + var json = _reflector.JsonSerializer.Serialize(value); + _output.WriteLine($"Char special: {json}"); + Assert.Equal("\"@\"", json); + } + + [Fact] + public void Char_Write_UnicodeChar() + { + var value = '\u00E9'; // é + var json = _reflector.JsonSerializer.Serialize(value); + _output.WriteLine($"Char unicode: {json}"); + Assert.Contains("\\u00E9", json, StringComparison.OrdinalIgnoreCase); + } + + [Fact] + public void Char_Read_FromString() + { + var json = "\"X\""; + var result = _reflector.JsonSerializer.Deserialize(json); + _output.WriteLine($"Char from string: {result}"); + Assert.Equal('X', result); + } + + [Fact] + public void Char_Read_FromNumber() + { + var json = "65"; // ASCII for 'A' + var result = _reflector.JsonSerializer.Deserialize(json); + _output.WriteLine($"Char from number: {result}"); + Assert.Equal('A', result); + } + + [Fact] + public void Char_Read_EmptyString_ReturnsDefault() + { + var json = "\"\""; + var result = _reflector.JsonSerializer.Deserialize(json); + _output.WriteLine($"Char from empty string: '{result}'"); + Assert.Equal(default(char), result); + } + + [Fact] + public void Char_Read_MultipleChars_Throws() + { + var json = "\"AB\""; + Assert.ThrowsAny(() => _reflector.JsonSerializer.Deserialize(json)); + } + + [Fact] + public void Char_RoundTrip() + { + var original = 'Z'; + var json = _reflector.JsonSerializer.Serialize(original); + var deserialized = _reflector.JsonSerializer.Deserialize(json); + _output.WriteLine($"Char roundtrip: {original} -> {json} -> {deserialized}"); + Assert.Equal(original, deserialized); + } + + #endregion + + #region BigIntegerJsonConverter Tests + + [Fact] + public void BigInteger_Write_PositiveValue() + { + var value = new BigInteger(123456789); + var json = _reflector.JsonSerializer.Serialize(value); + _output.WriteLine($"BigInteger positive: {json}"); + Assert.Equal("\"123456789\"", json); + } + + [Fact] + public void BigInteger_Write_NegativeValue() + { + var value = new BigInteger(-987654321); + var json = _reflector.JsonSerializer.Serialize(value); + _output.WriteLine($"BigInteger negative: {json}"); + Assert.Equal("\"-987654321\"", json); + } + + [Fact] + public void BigInteger_Write_Zero() + { + var value = BigInteger.Zero; + var json = _reflector.JsonSerializer.Serialize(value); + _output.WriteLine($"BigInteger zero: {json}"); + Assert.Equal("\"0\"", json); + } + + [Fact] + public void BigInteger_Write_VeryLargeValue() + { + var value = BigInteger.Parse("123456789012345678901234567890"); + var json = _reflector.JsonSerializer.Serialize(value); + _output.WriteLine($"BigInteger very large: {json}"); + Assert.Equal("\"123456789012345678901234567890\"", json); + } + + [Fact] + public void BigInteger_Read_FromString() + { + var json = "\"999999999999999999999\""; + var result = _reflector.JsonSerializer.Deserialize(json); + _output.WriteLine($"BigInteger from string: {result}"); + Assert.Equal(BigInteger.Parse("999999999999999999999"), result); + } + + [Fact] + public void BigInteger_Read_FromNumber() + { + var json = "12345"; + var result = _reflector.JsonSerializer.Deserialize(json); + _output.WriteLine($"BigInteger from number: {result}"); + Assert.Equal(new BigInteger(12345), result); + } + + [Fact] + public void BigInteger_Read_Null_ReturnsDefault() + { + var json = "null"; + var result = _reflector.JsonSerializer.Deserialize(json); + _output.WriteLine($"BigInteger from null: {result}"); + Assert.Equal(BigInteger.Zero, result); + } + + [Fact] + public void BigInteger_RoundTrip() + { + var original = BigInteger.Parse("12345678901234567890"); + var json = _reflector.JsonSerializer.Serialize(original); + var deserialized = _reflector.JsonSerializer.Deserialize(json); + _output.WriteLine($"BigInteger roundtrip: {original} -> {json} -> {deserialized}"); + Assert.Equal(original, deserialized); + } + + #endregion + + #region ComplexJsonConverter Tests + + [Fact] + public void Complex_Write_PositiveValues() + { + var value = new Complex(3.5, 4.5); + var json = _reflector.JsonSerializer.Serialize(value); + _output.WriteLine($"Complex positive: {json}"); + Assert.Contains("\"real\"", json); + Assert.Contains("3.5", json); + Assert.Contains("\"imaginary\"", json); + Assert.Contains("4.5", json); + } + + [Fact] + public void Complex_Write_NegativeValues() + { + var value = new Complex(-1.5, -2.5); + var json = _reflector.JsonSerializer.Serialize(value); + _output.WriteLine($"Complex negative: {json}"); + Assert.Contains("\"real\"", json); + Assert.Contains("-1.5", json); + Assert.Contains("\"imaginary\"", json); + Assert.Contains("-2.5", json); + } + + [Fact] + public void Complex_Write_Zero() + { + var value = Complex.Zero; + var json = _reflector.JsonSerializer.Serialize(value); + _output.WriteLine($"Complex zero: {json}"); + Assert.Contains("\"real\"", json); + Assert.Contains("\"imaginary\"", json); + } + + [Fact] + public void Complex_Write_ImaginaryOnly() + { + var value = new Complex(0, 5.0); + var json = _reflector.JsonSerializer.Serialize(value); + _output.WriteLine($"Complex imaginary only: {json}"); + Assert.Contains("\"real\"", json); + Assert.Contains("5", json); + } + + [Fact] + public void Complex_Read_ValidJson() + { + var json = "{\"real\":2.5,\"imaginary\":3.5}"; + var result = _reflector.JsonSerializer.Deserialize(json); + _output.WriteLine($"Complex from json: {result}"); + Assert.Equal(2.5, result.Real); + Assert.Equal(3.5, result.Imaginary); + } + + [Fact] + public void Complex_Read_CaseInsensitive() + { + var json = "{\"REAL\":1.0,\"IMAGINARY\":2.0}"; + var result = _reflector.JsonSerializer.Deserialize(json); + _output.WriteLine($"Complex case insensitive: {result}"); + Assert.Equal(1.0, result.Real); + Assert.Equal(2.0, result.Imaginary); + } + + [Fact] + public void Complex_Read_MissingProperty_Throws() + { + var json = "{\"real\":1.0}"; + Assert.ThrowsAny(() => _reflector.JsonSerializer.Deserialize(json)); + } + + [Fact] + public void Complex_Read_Null_Throws() + { + var json = "null"; + Assert.ThrowsAny(() => _reflector.JsonSerializer.Deserialize(json)); + } + + [Fact] + public void Complex_RoundTrip() + { + var original = new Complex(7.25, -3.75); + var json = _reflector.JsonSerializer.Serialize(original); + var deserialized = _reflector.JsonSerializer.Deserialize(json); + _output.WriteLine($"Complex roundtrip: {original} -> {json} -> {deserialized}"); + Assert.Equal(original, deserialized); + } + + [Fact] + public void Complex_Read_ExtraProperties_Ignored() + { + var json = "{\"real\":1.0,\"imaginary\":2.0,\"extra\":\"ignored\"}"; + var result = _reflector.JsonSerializer.Deserialize(json); + _output.WriteLine($"Complex with extra props: {result}"); + Assert.Equal(1.0, result.Real); + Assert.Equal(2.0, result.Imaginary); + } + + #endregion + +#if NET6_0_OR_GREATER + #region HalfJsonConverter Tests + + [Fact] + public void Half_Write_PositiveValue() + { + var value = (Half)3.14; + var json = _reflector.JsonSerializer.Serialize(value); + _output.WriteLine($"Half positive: {json}"); + Assert.Contains("3.14", json); + } + + [Fact] + public void Half_Write_NegativeValue() + { + var value = (Half)(-2.5); + var json = _reflector.JsonSerializer.Serialize(value); + _output.WriteLine($"Half negative: {json}"); + Assert.Contains("-2.5", json); + } + + [Fact] + public void Half_Write_Zero() + { + var value = (Half)0; + var json = _reflector.JsonSerializer.Serialize(value); + _output.WriteLine($"Half zero: {json}"); + Assert.Equal("0", json); + } + + [Fact] + public void Half_Read_FromNumber() + { + var json = "1.5"; + var result = _reflector.JsonSerializer.Deserialize(json); + _output.WriteLine($"Half from number: {result}"); + Assert.Equal((Half)1.5, result); + } + + [Fact] + public void Half_Read_FromString() + { + var json = "\"2.25\""; + var result = _reflector.JsonSerializer.Deserialize(json); + _output.WriteLine($"Half from string: {result}"); + Assert.Equal((Half)2.25, result); + } + + [Fact] + public void Half_RoundTrip() + { + var original = (Half)5.5; + var json = _reflector.JsonSerializer.Serialize(original); + var deserialized = _reflector.JsonSerializer.Deserialize(json); + _output.WriteLine($"Half roundtrip: {original} -> {json} -> {deserialized}"); + Assert.Equal(original, deserialized); + } + + #endregion +#endif + } +} diff --git a/ReflectorNet.Tests/src/JsonConverterTests/ReflectionConverterTests.cs b/ReflectorNet.Tests/src/JsonConverterTests/ReflectionConverterTests.cs new file mode 100644 index 00000000..407f24ad --- /dev/null +++ b/ReflectorNet.Tests/src/JsonConverterTests/ReflectionConverterTests.cs @@ -0,0 +1,280 @@ +using System; +using System.Reflection; +using System.Text.Json; +using com.IvanMurzak.ReflectorNet.Utils; +using Xunit; +using Xunit.Abstractions; + +namespace com.IvanMurzak.ReflectorNet.Tests.JsonConverterTests +{ + /// + /// Tests for reflection type JSON converters: Assembly, PropertyInfo, ConstructorInfo, ParameterInfo. + /// Note: These converters are primarily designed for deserialization from known JSON formats. + /// + public class ReflectionConverterTests : BaseTest + { + private readonly Reflector _reflector; + + public ReflectionConverterTests(ITestOutputHelper output) : base(output) + { + _reflector = new Reflector(); + } + + // Sample class for testing reflection converters + public class SampleClass + { + public string? Name { get; set; } + public int Value { get; set; } +#pragma warning disable CS0169 // Field is never used - kept for reflection tests + private string? _privateField; +#pragma warning restore CS0169 + + public SampleClass() { } + public SampleClass(string name) { Name = name; } + public SampleClass(string name, int value) { Name = name; Value = value; } + + public void DoSomething(string input, int count) { } + public static void StaticMethod() { } + } + + #region AssemblyJsonConverter Tests + + [Fact] + public void Assembly_Read_ByFullName() + { + var original = typeof(ReflectionConverterTests).Assembly; + var json = $"\"{original.FullName}\""; + var result = _reflector.JsonSerializer.Deserialize(json); + _output.WriteLine($"Assembly from full name: {result?.GetName().Name}"); + Assert.NotNull(result); + Assert.Equal(original.FullName, result.FullName); + } + + [Fact] + public void Assembly_Read_ByShortName() + { + var original = typeof(string).Assembly; + var shortName = original.GetName().Name; + var json = $"\"{shortName}\""; + var result = _reflector.JsonSerializer.Deserialize(json); + _output.WriteLine($"Assembly from short name '{shortName}': {result?.FullName}"); + Assert.NotNull(result); + } + + [Fact] + public void Assembly_Read_Null() + { + var json = "null"; + var result = _reflector.JsonSerializer.Deserialize(json); + _output.WriteLine($"Assembly from null: {result}"); + Assert.Null(result); + } + + [Fact] + public void Assembly_Read_EmptyString_ReturnsNull() + { + var json = "\"\""; + var result = _reflector.JsonSerializer.Deserialize(json); + _output.WriteLine($"Assembly from empty: {result}"); + Assert.Null(result); + } + + [Fact] + public void Assembly_Read_NotFound_Throws() + { + var json = "\"NonExistent.Assembly.Name.That.Does.Not.Exist\""; + Assert.ThrowsAny(() => _reflector.JsonSerializer.Deserialize(json)); + } + + [Fact] + public void Assembly_Read_WrongTokenType_Throws() + { + var json = "12345"; + Assert.ThrowsAny(() => _reflector.JsonSerializer.Deserialize(json)); + } + + [Fact] + public void Assembly_Read_TestsAssembly() + { + // Use a known loaded assembly + var assemblyName = "ReflectorNet.Tests"; + var json = $"\"{assemblyName}\""; + var result = _reflector.JsonSerializer.Deserialize(json); + _output.WriteLine($"Assembly {assemblyName}: {result?.GetName().Name}"); + Assert.NotNull(result); + Assert.Contains("ReflectorNet.Tests", result.GetName().Name ?? ""); + } + + #endregion + + #region PropertyInfoConverter Tests + + [Fact] + public void PropertyInfo_Read_ValidProperty() + { + var declaringType = typeof(SampleClass).GetTypeId(); + var json = $"{{\"name\":\"Name\",\"declaringType\":\"{declaringType}\"}}"; + var result = _reflector.JsonSerializer.Deserialize(json); + _output.WriteLine($"PropertyInfo from json: {result?.Name}"); + Assert.NotNull(result); + Assert.Equal("Name", result.Name); + Assert.Equal(typeof(string), result.PropertyType); + } + + [Fact] + public void PropertyInfo_Read_IntProperty() + { + var declaringType = typeof(SampleClass).GetTypeId(); + var json = $"{{\"name\":\"Value\",\"declaringType\":\"{declaringType}\"}}"; + var result = _reflector.JsonSerializer.Deserialize(json); + _output.WriteLine($"PropertyInfo int: {result?.Name}"); + Assert.NotNull(result); + Assert.Equal("Value", result.Name); + Assert.Equal(typeof(int), result.PropertyType); + } + + [Fact] + public void PropertyInfo_Read_Null() + { + var json = "null"; + var result = _reflector.JsonSerializer.Deserialize(json); + _output.WriteLine($"PropertyInfo from null: {result}"); + Assert.Null(result); + } + + [Fact] + public void PropertyInfo_Read_MissingFields_Throws() + { + var json = "{\"name\":\"Name\"}"; // missing declaringType + Assert.ThrowsAny(() => _reflector.JsonSerializer.Deserialize(json)); + } + + [Fact] + public void PropertyInfo_Read_PropertyNotFound_Throws() + { + var declaringType = typeof(SampleClass).GetTypeId(); + var json = $"{{\"name\":\"NonExistentProperty\",\"declaringType\":\"{declaringType}\"}}"; + Assert.ThrowsAny(() => _reflector.JsonSerializer.Deserialize(json)); + } + + #endregion + + #region ConstructorInfoConverter Tests + + [Fact] + public void ConstructorInfo_Read_DefaultConstructor() + { + var declaringType = typeof(SampleClass).GetTypeId(); + var json = $"{{\"declaringType\":\"{declaringType}\",\"parameters\":[]}}"; + var result = _reflector.JsonSerializer.Deserialize(json); + _output.WriteLine($"ConstructorInfo from default: {result}"); + Assert.NotNull(result); + Assert.Empty(result.GetParameters()); + } + + [Fact] + public void ConstructorInfo_Read_SingleParameter() + { + var declaringType = typeof(SampleClass).GetTypeId(); + var stringType = typeof(string).GetTypeId(); + var json = $"{{\"declaringType\":\"{declaringType}\",\"parameters\":[{{\"type\":\"{stringType}\"}}]}}"; + var result = _reflector.JsonSerializer.Deserialize(json); + _output.WriteLine($"ConstructorInfo single param: {result}"); + Assert.NotNull(result); + Assert.Single(result.GetParameters()); + Assert.Equal(typeof(string), result.GetParameters()[0].ParameterType); + } + + [Fact] + public void ConstructorInfo_Read_MultipleParameters() + { + var declaringType = typeof(SampleClass).GetTypeId(); + var stringType = typeof(string).GetTypeId(); + var intType = typeof(int).GetTypeId(); + var json = $"{{\"declaringType\":\"{declaringType}\",\"parameters\":[{{\"type\":\"{stringType}\"}},{{\"type\":\"{intType}\"}}]}}"; + var result = _reflector.JsonSerializer.Deserialize(json); + _output.WriteLine($"ConstructorInfo multi params: {result}"); + Assert.NotNull(result); + Assert.Equal(2, result.GetParameters().Length); + } + + [Fact] + public void ConstructorInfo_Read_Null() + { + var json = "null"; + var result = _reflector.JsonSerializer.Deserialize(json); + _output.WriteLine($"ConstructorInfo from null: {result}"); + Assert.Null(result); + } + + [Fact] + public void ConstructorInfo_Read_MissingDeclaringType_Throws() + { + var json = "{\"parameters\":[]}"; + Assert.ThrowsAny(() => _reflector.JsonSerializer.Deserialize(json)); + } + + [Fact] + public void ConstructorInfo_Read_NotFound_Throws() + { + var declaringType = typeof(SampleClass).GetTypeId(); + var doubleType = typeof(double).GetTypeId(); + // SampleClass doesn't have a constructor that takes double + var json = $"{{\"declaringType\":\"{declaringType}\",\"parameters\":[{{\"type\":\"{doubleType}\"}}]}}"; + Assert.ThrowsAny(() => _reflector.JsonSerializer.Deserialize(json)); + } + + #endregion + + #region ParameterInfoConverter Tests + + [Fact] + public void ParameterInfo_Read_MethodParameter() + { + var declaringType = typeof(SampleClass).GetTypeId(); + var stringType = typeof(string).GetTypeId(); + var intType = typeof(int).GetTypeId(); + // Build the member JSON for DoSomething method + var memberJson = $"{{\"name\":\"DoSomething\",\"declaringType\":\"{declaringType}\",\"parameters\":[{{\"type\":\"{stringType}\"}},{{\"type\":\"{intType}\"}}]}}"; + var json = $"{{\"name\":\"input\",\"memberType\":\"MethodInfo\",\"member\":{memberJson}}}"; + var result = _reflector.JsonSerializer.Deserialize(json); + _output.WriteLine($"ParameterInfo from method: {result?.Name}"); + Assert.NotNull(result); + Assert.Equal("input", result.Name); + Assert.Equal(typeof(string), result.ParameterType); + } + + [Fact] + public void ParameterInfo_Read_ConstructorParameter() + { + var declaringType = typeof(SampleClass).GetTypeId(); + var stringType = typeof(string).GetTypeId(); + // Build the member JSON for the single-param constructor + var memberJson = $"{{\"declaringType\":\"{declaringType}\",\"parameters\":[{{\"type\":\"{stringType}\"}}]}}"; + var json = $"{{\"name\":\"name\",\"memberType\":\"ConstructorInfo\",\"member\":{memberJson}}}"; + var result = _reflector.JsonSerializer.Deserialize(json); + _output.WriteLine($"ParameterInfo from ctor: {result?.Name}"); + Assert.NotNull(result); + Assert.Equal("name", result.Name); + Assert.Equal(typeof(string), result.ParameterType); + } + + [Fact] + public void ParameterInfo_Read_Null() + { + var json = "null"; + var result = _reflector.JsonSerializer.Deserialize(json); + _output.WriteLine($"ParameterInfo from null: {result}"); + Assert.Null(result); + } + + [Fact] + public void ParameterInfo_Read_MissingFields_Throws() + { + var json = "{\"name\":\"input\"}"; // missing member and memberType + Assert.ThrowsAny(() => _reflector.JsonSerializer.Deserialize(json)); + } + + #endregion + } +} From bdd0a81ba11f82e0e037efe3d7c328438c0a86e3 Mon Sep 17 00:00:00 2001 From: Ivan Murzak Date: Fri, 26 Dec 2025 03:19:38 -0800 Subject: [PATCH 18/23] feat: Add comprehensive tests for additional JSON converters including Enum, Exception, FieldInfo, MethodInfo, Type, UInt16, UInt32, UInt64, DateTime, and Guid --- .../AdditionalConverterTests.cs | 628 ++++++++++++++++++ 1 file changed, 628 insertions(+) create mode 100644 ReflectorNet.Tests/src/JsonConverterTests/AdditionalConverterTests.cs diff --git a/ReflectorNet.Tests/src/JsonConverterTests/AdditionalConverterTests.cs b/ReflectorNet.Tests/src/JsonConverterTests/AdditionalConverterTests.cs new file mode 100644 index 00000000..852d9d2e --- /dev/null +++ b/ReflectorNet.Tests/src/JsonConverterTests/AdditionalConverterTests.cs @@ -0,0 +1,628 @@ +using System; +using System.Reflection; +using System.Text.Json; +using com.IvanMurzak.ReflectorNet.Utils; +using Xunit; +using Xunit.Abstractions; + +namespace com.IvanMurzak.ReflectorNet.Tests.JsonConverterTests +{ + /// + /// Tests for additional JSON converters: Enum, Exception, FieldInfo, MethodInfo, Type, + /// UInt16, UInt32, UInt64, DateTime, Guid + /// + public class AdditionalConverterTests : BaseTest + { + private readonly Reflector _reflector; + + public AdditionalConverterTests(ITestOutputHelper output) : base(output) + { + _reflector = new Reflector(); + } + + // Sample enum for testing + public enum TestStatus + { + Unknown = 0, + Active = 1, + Inactive = 2, + Pending = 3 + } + + // Sample class for reflection tests + public class SampleClass + { + public string? Name; + public int Value; +#pragma warning disable CS0169 // Field is never used - needed for reflection tests + private string? _privateField; +#pragma warning restore CS0169 + + public void PublicMethod(string input) { } + public int CalculateValue(int a, int b) => a + b; + private void PrivateMethod() { } + } + + #region EnumJsonConverter Tests + + [Fact] + public void Enum_Serialize_Value() + { + var value = TestStatus.Active; + var json = _reflector.JsonSerializer.Serialize(value); + _output.WriteLine($"Enum value: {json}"); + Assert.Contains("Active", json); + } + + [Fact] + public void Enum_Serialize_Null() + { + TestStatus? value = null; + var json = _reflector.JsonSerializer.Serialize(value); + _output.WriteLine($"Enum null: {json}"); + Assert.Equal("null", json); + } + + [Fact] + public void Enum_Deserialize_FromString() + { + var json = "\"Active\""; + var result = _reflector.JsonSerializer.Deserialize(json); + _output.WriteLine($"Enum from string: {result}"); + Assert.Equal(TestStatus.Active, result); + } + + [Fact] + public void Enum_Deserialize_FromString_CaseInsensitive() + { + var json = "\"active\""; + var result = _reflector.JsonSerializer.Deserialize(json); + _output.WriteLine($"Enum case insensitive: {result}"); + Assert.Equal(TestStatus.Active, result); + } + + [Fact] + public void Enum_Deserialize_FromNumber() + { + var json = "2"; + var result = _reflector.JsonSerializer.Deserialize(json); + _output.WriteLine($"Enum from number: {result}"); + Assert.Equal(TestStatus.Inactive, result); + } + + [Fact] + public void Enum_Deserialize_Null_ToNullable() + { + var json = "null"; + var result = _reflector.JsonSerializer.Deserialize(json); + _output.WriteLine($"Enum null to nullable: {result}"); + Assert.Null(result); + } + + [Fact] + public void Enum_Deserialize_InvalidString_Throws() + { + var json = "\"NotAValidValue\""; + Assert.ThrowsAny(() => _reflector.JsonSerializer.Deserialize(json)); + } + + [Fact] + public void Enum_RoundTrip() + { + var original = TestStatus.Pending; + var json = _reflector.JsonSerializer.Serialize(original); + var deserialized = _reflector.JsonSerializer.Deserialize(json); + _output.WriteLine($"Enum roundtrip: {original} -> {json} -> {deserialized}"); + Assert.Equal(original, deserialized); + } + + #endregion + + #region ExceptionJsonConverter Tests + + [Fact] + public void Exception_Serialize_Simple() + { + var value = new Exception("Test error message"); + var json = _reflector.JsonSerializer.Serialize(value, typeof(Exception)); + _output.WriteLine($"Exception simple: {json}"); + Assert.Contains("Test error message", json); + Assert.Contains("type", json); + } + + [Fact] + public void Exception_Serialize_WithInnerException() + { + var inner = new InvalidOperationException("Inner error"); + var value = new Exception("Outer error", inner); + var json = _reflector.JsonSerializer.Serialize(value, typeof(Exception)); + _output.WriteLine($"Exception with inner: {json}"); + Assert.Contains("Outer error", json); + Assert.Contains("innerException", json); + } + + [Fact] + public void Exception_Serialize_Null() + { + Exception? value = null; + var json = _reflector.JsonSerializer.Serialize(value, typeof(Exception)); + _output.WriteLine($"Exception null: {json}"); + Assert.Equal("null", json); + } + + [Fact] + public void Exception_Deserialize_Simple() + { + var json = "{\"type\":\"System.Exception\",\"message\":\"Test message\"}"; + var result = _reflector.JsonSerializer.Deserialize(json); + _output.WriteLine($"Exception from json: {result?.Message}"); + Assert.NotNull(result); + Assert.Contains("Test message", result.Message); + } + + [Fact] + public void Exception_Deserialize_Null() + { + var json = "null"; + var result = _reflector.JsonSerializer.Deserialize(json); + _output.WriteLine($"Exception from null: {result}"); + Assert.Null(result); + } + + [Fact] + public void Exception_RoundTrip() + { + var original = new ArgumentException("Invalid argument"); + var json = _reflector.JsonSerializer.Serialize(original, typeof(Exception)); + var deserialized = _reflector.JsonSerializer.Deserialize(json); + _output.WriteLine($"Exception roundtrip: {original.Message} -> {json}"); + Assert.NotNull(deserialized); + Assert.Contains("Invalid argument", deserialized.Message); + } + + #endregion + + #region FieldInfoConverter Tests + + [Fact] + public void FieldInfo_Deserialize_PublicField() + { + var declaringType = typeof(SampleClass).GetTypeId(); + var json = $"{{\"name\":\"Name\",\"declaringType\":\"{declaringType}\"}}"; + var result = _reflector.JsonSerializer.Deserialize(json); + _output.WriteLine($"FieldInfo public: {result?.Name}"); + Assert.NotNull(result); + Assert.Equal("Name", result.Name); + } + + [Fact] + public void FieldInfo_Deserialize_ValueField() + { + var declaringType = typeof(SampleClass).GetTypeId(); + var json = $"{{\"name\":\"Value\",\"declaringType\":\"{declaringType}\"}}"; + var result = _reflector.JsonSerializer.Deserialize(json); + _output.WriteLine($"FieldInfo value: {result?.Name}"); + Assert.NotNull(result); + Assert.Equal("Value", result.Name); + Assert.Equal(typeof(int), result.FieldType); + } + + [Fact] + public void FieldInfo_Deserialize_PrivateField() + { + var declaringType = typeof(SampleClass).GetTypeId(); + var json = $"{{\"name\":\"_privateField\",\"declaringType\":\"{declaringType}\"}}"; + var result = _reflector.JsonSerializer.Deserialize(json); + _output.WriteLine($"FieldInfo private: {result?.Name}"); + Assert.NotNull(result); + Assert.Equal("_privateField", result.Name); + } + + [Fact] + public void FieldInfo_Deserialize_Null() + { + var json = "null"; + var result = _reflector.JsonSerializer.Deserialize(json); + _output.WriteLine($"FieldInfo from null: {result}"); + Assert.Null(result); + } + + [Fact] + public void FieldInfo_Deserialize_MissingName_Throws() + { + var declaringType = typeof(SampleClass).GetTypeId(); + var json = $"{{\"declaringType\":\"{declaringType}\"}}"; + Assert.ThrowsAny(() => _reflector.JsonSerializer.Deserialize(json)); + } + + [Fact] + public void FieldInfo_Deserialize_FieldNotFound_Throws() + { + var declaringType = typeof(SampleClass).GetTypeId(); + var json = $"{{\"name\":\"NonExistentField\",\"declaringType\":\"{declaringType}\"}}"; + Assert.ThrowsAny(() => _reflector.JsonSerializer.Deserialize(json)); + } + + #endregion + + #region MethodInfoConverter Tests + + [Fact] + public void MethodInfo_Deserialize_SimpleMethod() + { + var declaringType = typeof(SampleClass).GetTypeId(); + var stringType = typeof(string).GetTypeId(); + var json = $"{{\"name\":\"PublicMethod\",\"declaringType\":\"{declaringType}\",\"parameters\":[{{\"type\":\"{stringType}\"}}]}}"; + var result = _reflector.JsonSerializer.Deserialize(json); + _output.WriteLine($"MethodInfo simple: {result?.Name}"); + Assert.NotNull(result); + Assert.Equal("PublicMethod", result.Name); + } + + [Fact] + public void MethodInfo_Deserialize_MethodWithMultipleParams() + { + var declaringType = typeof(SampleClass).GetTypeId(); + var intType = typeof(int).GetTypeId(); + var json = $"{{\"name\":\"CalculateValue\",\"declaringType\":\"{declaringType}\",\"parameters\":[{{\"type\":\"{intType}\"}},{{\"type\":\"{intType}\"}}]}}"; + var result = _reflector.JsonSerializer.Deserialize(json); + _output.WriteLine($"MethodInfo multi params: {result?.Name}"); + Assert.NotNull(result); + Assert.Equal("CalculateValue", result.Name); + Assert.Equal(2, result.GetParameters().Length); + } + + [Fact] + public void MethodInfo_Deserialize_MissingDeclaringType_Throws() + { + var json = "{\"name\":\"PublicMethod\",\"parameters\":[]}"; + // Throws KeyNotFoundException when declaringType is missing + Assert.ThrowsAny(() => _reflector.JsonSerializer.Deserialize(json)); + } + + [Fact] + public void MethodInfo_Deserialize_MethodNotFound_Throws() + { + var declaringType = typeof(SampleClass).GetTypeId(); + var json = $"{{\"name\":\"NonExistentMethod\",\"declaringType\":\"{declaringType}\",\"parameters\":[]}}"; + Assert.ThrowsAny(() => _reflector.JsonSerializer.Deserialize(json)); + } + + #endregion + + #region TypeJsonConverter Tests + + [Fact] + public void Type_Serialize_Simple() + { + var value = typeof(string); + var json = _reflector.JsonSerializer.Serialize(value, typeof(Type)); + _output.WriteLine($"Type simple: {json}"); + Assert.Contains("System.String", json); + } + + [Fact] + public void Type_Serialize_Custom() + { + var value = typeof(SampleClass); + var json = _reflector.JsonSerializer.Serialize(value, typeof(Type)); + _output.WriteLine($"Type custom: {json}"); + Assert.Contains("SampleClass", json); + } + + [Fact] + public void Type_Serialize_Null() + { + Type? value = null; + var json = _reflector.JsonSerializer.Serialize(value, typeof(Type)); + _output.WriteLine($"Type null: {json}"); + Assert.Equal("null", json); + } + + [Fact] + public void Type_Deserialize_Simple() + { + var json = "\"System.String\""; + var result = _reflector.JsonSerializer.Deserialize(json); + _output.WriteLine($"Type from json: {result?.Name}"); + Assert.NotNull(result); + Assert.Equal(typeof(string), result); + } + + [Fact] + public void Type_Deserialize_Int32() + { + var json = "\"System.Int32\""; + var result = _reflector.JsonSerializer.Deserialize(json); + _output.WriteLine($"Type Int32: {result?.Name}"); + Assert.NotNull(result); + Assert.Equal(typeof(int), result); + } + + [Fact] + public void Type_Deserialize_Null() + { + var json = "null"; + var result = _reflector.JsonSerializer.Deserialize(json); + _output.WriteLine($"Type from null: {result}"); + Assert.Null(result); + } + + [Fact] + public void Type_RoundTrip() + { + var original = typeof(DateTime); + var json = _reflector.JsonSerializer.Serialize(original, typeof(Type)); + var deserialized = _reflector.JsonSerializer.Deserialize(json); + _output.WriteLine($"Type roundtrip: {original.Name} -> {json} -> {deserialized?.Name}"); + Assert.Equal(original, deserialized); + } + + #endregion + + #region UInt16JsonConverter Tests + + [Fact] + public void UInt16_Serialize_Value() + { + ushort value = 12345; + var json = _reflector.JsonSerializer.Serialize(value); + _output.WriteLine($"UInt16 value: {json}"); + Assert.Equal("12345", json); + } + + [Fact] + public void UInt16_Serialize_Zero() + { + ushort value = 0; + var json = _reflector.JsonSerializer.Serialize(value); + _output.WriteLine($"UInt16 zero: {json}"); + Assert.Equal("0", json); + } + + [Fact] + public void UInt16_Serialize_MaxValue() + { + ushort value = ushort.MaxValue; + var json = _reflector.JsonSerializer.Serialize(value); + _output.WriteLine($"UInt16 max: {json}"); + Assert.Equal("65535", json); + } + + [Fact] + public void UInt16_Deserialize_FromNumber() + { + var json = "100"; + var result = _reflector.JsonSerializer.Deserialize(json); + _output.WriteLine($"UInt16 from number: {result}"); + Assert.Equal((ushort)100, result); + } + + [Fact] + public void UInt16_Deserialize_FromString() + { + var json = "\"200\""; + var result = _reflector.JsonSerializer.Deserialize(json); + _output.WriteLine($"UInt16 from string: {result}"); + Assert.Equal((ushort)200, result); + } + + [Fact] + public void UInt16_Deserialize_Null_ToNullable() + { + var json = "null"; + var result = _reflector.JsonSerializer.Deserialize(json); + _output.WriteLine($"UInt16 null: {result}"); + Assert.Null(result); + } + + [Fact] + public void UInt16_RoundTrip() + { + ushort original = 50000; + var json = _reflector.JsonSerializer.Serialize(original); + var deserialized = _reflector.JsonSerializer.Deserialize(json); + _output.WriteLine($"UInt16 roundtrip: {original} -> {json} -> {deserialized}"); + Assert.Equal(original, deserialized); + } + + #endregion + + #region UInt32JsonConverter Tests + + [Fact] + public void UInt32_Serialize_Value() + { + uint value = 1234567890; + var json = _reflector.JsonSerializer.Serialize(value); + _output.WriteLine($"UInt32 value: {json}"); + Assert.Equal("1234567890", json); + } + + [Fact] + public void UInt32_Serialize_MaxValue() + { + uint value = uint.MaxValue; + var json = _reflector.JsonSerializer.Serialize(value); + _output.WriteLine($"UInt32 max: {json}"); + Assert.Equal("4294967295", json); + } + + [Fact] + public void UInt32_Deserialize_FromNumber() + { + var json = "999999"; + var result = _reflector.JsonSerializer.Deserialize(json); + _output.WriteLine($"UInt32 from number: {result}"); + Assert.Equal(999999u, result); + } + + [Fact] + public void UInt32_Deserialize_FromString() + { + var json = "\"100000\""; + var result = _reflector.JsonSerializer.Deserialize(json); + _output.WriteLine($"UInt32 from string: {result}"); + Assert.Equal(100000u, result); + } + + [Fact] + public void UInt32_RoundTrip() + { + uint original = 3000000000; + var json = _reflector.JsonSerializer.Serialize(original); + var deserialized = _reflector.JsonSerializer.Deserialize(json); + _output.WriteLine($"UInt32 roundtrip: {original} -> {json} -> {deserialized}"); + Assert.Equal(original, deserialized); + } + + #endregion + + #region UInt64JsonConverter Tests + + [Fact] + public void UInt64_Serialize_Value() + { + ulong value = 12345678901234567890UL; + var json = _reflector.JsonSerializer.Serialize(value); + _output.WriteLine($"UInt64 value: {json}"); + Assert.Contains("12345678901234567890", json); + } + + [Fact] + public void UInt64_Serialize_MaxValue() + { + ulong value = ulong.MaxValue; + var json = _reflector.JsonSerializer.Serialize(value); + _output.WriteLine($"UInt64 max: {json}"); + Assert.Contains("18446744073709551615", json); + } + + [Fact] + public void UInt64_Deserialize_FromNumber() + { + var json = "999999999"; + var result = _reflector.JsonSerializer.Deserialize(json); + _output.WriteLine($"UInt64 from number: {result}"); + Assert.Equal(999999999UL, result); + } + + [Fact] + public void UInt64_Deserialize_FromString() + { + var json = "\"10000000000\""; + var result = _reflector.JsonSerializer.Deserialize(json); + _output.WriteLine($"UInt64 from string: {result}"); + Assert.Equal(10000000000UL, result); + } + + [Fact] + public void UInt64_RoundTrip() + { + ulong original = 9999999999999UL; + var json = _reflector.JsonSerializer.Serialize(original); + var deserialized = _reflector.JsonSerializer.Deserialize(json); + _output.WriteLine($"UInt64 roundtrip: {original} -> {json} -> {deserialized}"); + Assert.Equal(original, deserialized); + } + + #endregion + + #region DateTimeJsonConverter Tests + + [Fact] + public void DateTime_Serialize_Value() + { + var value = new DateTime(2024, 12, 25, 10, 30, 45, DateTimeKind.Utc); + var json = _reflector.JsonSerializer.Serialize(value); + _output.WriteLine($"DateTime value: {json}"); + Assert.Contains("2024", json); + Assert.Contains("12", json); + Assert.Contains("25", json); + } + + [Fact] + public void DateTime_Serialize_MinValue() + { + var value = DateTime.MinValue; + var json = _reflector.JsonSerializer.Serialize(value); + _output.WriteLine($"DateTime min: {json}"); + Assert.Contains("0001", json); + } + + [Fact] + public void DateTime_Deserialize_Iso8601() + { + var json = "\"2024-06-15T14:30:00Z\""; + var result = _reflector.JsonSerializer.Deserialize(json); + _output.WriteLine($"DateTime from ISO: {result}"); + Assert.Equal(2024, result.Year); + Assert.Equal(6, result.Month); + Assert.Equal(15, result.Day); + } + + [Fact] + public void DateTime_RoundTrip() + { + var original = new DateTime(2025, 1, 1, 0, 0, 0, DateTimeKind.Utc); + var json = _reflector.JsonSerializer.Serialize(original); + var deserialized = _reflector.JsonSerializer.Deserialize(json); + _output.WriteLine($"DateTime roundtrip: {original} -> {json} -> {deserialized}"); + Assert.Equal(original.Year, deserialized.Year); + Assert.Equal(original.Month, deserialized.Month); + Assert.Equal(original.Day, deserialized.Day); + } + + #endregion + + #region GuidJsonConverter Tests + + [Fact] + public void Guid_Serialize_Value() + { + var value = Guid.Parse("12345678-1234-1234-1234-123456789abc"); + var json = _reflector.JsonSerializer.Serialize(value); + _output.WriteLine($"Guid value: {json}"); + Assert.Contains("12345678-1234-1234-1234-123456789abc", json); + } + + [Fact] + public void Guid_Serialize_Empty() + { + var value = Guid.Empty; + var json = _reflector.JsonSerializer.Serialize(value); + _output.WriteLine($"Guid empty: {json}"); + Assert.Contains("00000000-0000-0000-0000-000000000000", json); + } + + [Fact] + public void Guid_Deserialize_WithDashes() + { + var json = "\"abcdef12-3456-7890-abcd-ef1234567890\""; + var result = _reflector.JsonSerializer.Deserialize(json); + _output.WriteLine($"Guid from dashes: {result}"); + Assert.Equal(Guid.Parse("abcdef12-3456-7890-abcd-ef1234567890"), result); + } + + [Fact] + public void Guid_Deserialize_Null_ToNullable() + { + var json = "null"; + var result = _reflector.JsonSerializer.Deserialize(json); + _output.WriteLine($"Guid null: {result}"); + Assert.Null(result); + } + + [Fact] + public void Guid_RoundTrip() + { + var original = Guid.NewGuid(); + var json = _reflector.JsonSerializer.Serialize(original); + var deserialized = _reflector.JsonSerializer.Deserialize(json); + _output.WriteLine($"Guid roundtrip: {original} -> {json} -> {deserialized}"); + Assert.Equal(original, deserialized); + } + + #endregion + } +} From 3b6b9e1a9f6847c081bad99a354f9f000c020dc3 Mon Sep 17 00:00:00 2001 From: Ivan Murzak Date: Fri, 26 Dec 2025 03:25:53 -0800 Subject: [PATCH 19/23] Refactor JSON Converter Tests - Removed AdditionalConverterTests.cs and integrated its tests into existing test files. - Added EnumConverterTests.cs to handle enum serialization and deserialization. - Added ExceptionConverterTests.cs to cover exception serialization and deserialization. - Enhanced PrimitiveConverterTests.cs with tests for UInt16, UInt32, and UInt64 serialization and deserialization. - Updated ReflectionConverterTests.cs to include tests for FieldInfo and MethodInfo serialization and deserialization. - Added DateTime serialization and deserialization tests to DateTimeConverterTests.cs. - Added Guid serialization and deserialization tests to CommonTypesConverterTests.cs. --- .../AdditionalConverterTests.cs | 628 ------------------ .../CommonTypesConverterTests.cs | 50 ++ .../DateTimeConverterTests.cs | 47 ++ .../JsonConverterTests/EnumConverterTests.cs | 105 +++ .../ExceptionConverterTests.cs | 85 +++ .../PrimitiveConverterTests.cs | 168 +++++ .../ReflectionConverterTests.cs | 192 ++++++ 7 files changed, 647 insertions(+), 628 deletions(-) delete mode 100644 ReflectorNet.Tests/src/JsonConverterTests/AdditionalConverterTests.cs create mode 100644 ReflectorNet.Tests/src/JsonConverterTests/EnumConverterTests.cs create mode 100644 ReflectorNet.Tests/src/JsonConverterTests/ExceptionConverterTests.cs diff --git a/ReflectorNet.Tests/src/JsonConverterTests/AdditionalConverterTests.cs b/ReflectorNet.Tests/src/JsonConverterTests/AdditionalConverterTests.cs deleted file mode 100644 index 852d9d2e..00000000 --- a/ReflectorNet.Tests/src/JsonConverterTests/AdditionalConverterTests.cs +++ /dev/null @@ -1,628 +0,0 @@ -using System; -using System.Reflection; -using System.Text.Json; -using com.IvanMurzak.ReflectorNet.Utils; -using Xunit; -using Xunit.Abstractions; - -namespace com.IvanMurzak.ReflectorNet.Tests.JsonConverterTests -{ - /// - /// Tests for additional JSON converters: Enum, Exception, FieldInfo, MethodInfo, Type, - /// UInt16, UInt32, UInt64, DateTime, Guid - /// - public class AdditionalConverterTests : BaseTest - { - private readonly Reflector _reflector; - - public AdditionalConverterTests(ITestOutputHelper output) : base(output) - { - _reflector = new Reflector(); - } - - // Sample enum for testing - public enum TestStatus - { - Unknown = 0, - Active = 1, - Inactive = 2, - Pending = 3 - } - - // Sample class for reflection tests - public class SampleClass - { - public string? Name; - public int Value; -#pragma warning disable CS0169 // Field is never used - needed for reflection tests - private string? _privateField; -#pragma warning restore CS0169 - - public void PublicMethod(string input) { } - public int CalculateValue(int a, int b) => a + b; - private void PrivateMethod() { } - } - - #region EnumJsonConverter Tests - - [Fact] - public void Enum_Serialize_Value() - { - var value = TestStatus.Active; - var json = _reflector.JsonSerializer.Serialize(value); - _output.WriteLine($"Enum value: {json}"); - Assert.Contains("Active", json); - } - - [Fact] - public void Enum_Serialize_Null() - { - TestStatus? value = null; - var json = _reflector.JsonSerializer.Serialize(value); - _output.WriteLine($"Enum null: {json}"); - Assert.Equal("null", json); - } - - [Fact] - public void Enum_Deserialize_FromString() - { - var json = "\"Active\""; - var result = _reflector.JsonSerializer.Deserialize(json); - _output.WriteLine($"Enum from string: {result}"); - Assert.Equal(TestStatus.Active, result); - } - - [Fact] - public void Enum_Deserialize_FromString_CaseInsensitive() - { - var json = "\"active\""; - var result = _reflector.JsonSerializer.Deserialize(json); - _output.WriteLine($"Enum case insensitive: {result}"); - Assert.Equal(TestStatus.Active, result); - } - - [Fact] - public void Enum_Deserialize_FromNumber() - { - var json = "2"; - var result = _reflector.JsonSerializer.Deserialize(json); - _output.WriteLine($"Enum from number: {result}"); - Assert.Equal(TestStatus.Inactive, result); - } - - [Fact] - public void Enum_Deserialize_Null_ToNullable() - { - var json = "null"; - var result = _reflector.JsonSerializer.Deserialize(json); - _output.WriteLine($"Enum null to nullable: {result}"); - Assert.Null(result); - } - - [Fact] - public void Enum_Deserialize_InvalidString_Throws() - { - var json = "\"NotAValidValue\""; - Assert.ThrowsAny(() => _reflector.JsonSerializer.Deserialize(json)); - } - - [Fact] - public void Enum_RoundTrip() - { - var original = TestStatus.Pending; - var json = _reflector.JsonSerializer.Serialize(original); - var deserialized = _reflector.JsonSerializer.Deserialize(json); - _output.WriteLine($"Enum roundtrip: {original} -> {json} -> {deserialized}"); - Assert.Equal(original, deserialized); - } - - #endregion - - #region ExceptionJsonConverter Tests - - [Fact] - public void Exception_Serialize_Simple() - { - var value = new Exception("Test error message"); - var json = _reflector.JsonSerializer.Serialize(value, typeof(Exception)); - _output.WriteLine($"Exception simple: {json}"); - Assert.Contains("Test error message", json); - Assert.Contains("type", json); - } - - [Fact] - public void Exception_Serialize_WithInnerException() - { - var inner = new InvalidOperationException("Inner error"); - var value = new Exception("Outer error", inner); - var json = _reflector.JsonSerializer.Serialize(value, typeof(Exception)); - _output.WriteLine($"Exception with inner: {json}"); - Assert.Contains("Outer error", json); - Assert.Contains("innerException", json); - } - - [Fact] - public void Exception_Serialize_Null() - { - Exception? value = null; - var json = _reflector.JsonSerializer.Serialize(value, typeof(Exception)); - _output.WriteLine($"Exception null: {json}"); - Assert.Equal("null", json); - } - - [Fact] - public void Exception_Deserialize_Simple() - { - var json = "{\"type\":\"System.Exception\",\"message\":\"Test message\"}"; - var result = _reflector.JsonSerializer.Deserialize(json); - _output.WriteLine($"Exception from json: {result?.Message}"); - Assert.NotNull(result); - Assert.Contains("Test message", result.Message); - } - - [Fact] - public void Exception_Deserialize_Null() - { - var json = "null"; - var result = _reflector.JsonSerializer.Deserialize(json); - _output.WriteLine($"Exception from null: {result}"); - Assert.Null(result); - } - - [Fact] - public void Exception_RoundTrip() - { - var original = new ArgumentException("Invalid argument"); - var json = _reflector.JsonSerializer.Serialize(original, typeof(Exception)); - var deserialized = _reflector.JsonSerializer.Deserialize(json); - _output.WriteLine($"Exception roundtrip: {original.Message} -> {json}"); - Assert.NotNull(deserialized); - Assert.Contains("Invalid argument", deserialized.Message); - } - - #endregion - - #region FieldInfoConverter Tests - - [Fact] - public void FieldInfo_Deserialize_PublicField() - { - var declaringType = typeof(SampleClass).GetTypeId(); - var json = $"{{\"name\":\"Name\",\"declaringType\":\"{declaringType}\"}}"; - var result = _reflector.JsonSerializer.Deserialize(json); - _output.WriteLine($"FieldInfo public: {result?.Name}"); - Assert.NotNull(result); - Assert.Equal("Name", result.Name); - } - - [Fact] - public void FieldInfo_Deserialize_ValueField() - { - var declaringType = typeof(SampleClass).GetTypeId(); - var json = $"{{\"name\":\"Value\",\"declaringType\":\"{declaringType}\"}}"; - var result = _reflector.JsonSerializer.Deserialize(json); - _output.WriteLine($"FieldInfo value: {result?.Name}"); - Assert.NotNull(result); - Assert.Equal("Value", result.Name); - Assert.Equal(typeof(int), result.FieldType); - } - - [Fact] - public void FieldInfo_Deserialize_PrivateField() - { - var declaringType = typeof(SampleClass).GetTypeId(); - var json = $"{{\"name\":\"_privateField\",\"declaringType\":\"{declaringType}\"}}"; - var result = _reflector.JsonSerializer.Deserialize(json); - _output.WriteLine($"FieldInfo private: {result?.Name}"); - Assert.NotNull(result); - Assert.Equal("_privateField", result.Name); - } - - [Fact] - public void FieldInfo_Deserialize_Null() - { - var json = "null"; - var result = _reflector.JsonSerializer.Deserialize(json); - _output.WriteLine($"FieldInfo from null: {result}"); - Assert.Null(result); - } - - [Fact] - public void FieldInfo_Deserialize_MissingName_Throws() - { - var declaringType = typeof(SampleClass).GetTypeId(); - var json = $"{{\"declaringType\":\"{declaringType}\"}}"; - Assert.ThrowsAny(() => _reflector.JsonSerializer.Deserialize(json)); - } - - [Fact] - public void FieldInfo_Deserialize_FieldNotFound_Throws() - { - var declaringType = typeof(SampleClass).GetTypeId(); - var json = $"{{\"name\":\"NonExistentField\",\"declaringType\":\"{declaringType}\"}}"; - Assert.ThrowsAny(() => _reflector.JsonSerializer.Deserialize(json)); - } - - #endregion - - #region MethodInfoConverter Tests - - [Fact] - public void MethodInfo_Deserialize_SimpleMethod() - { - var declaringType = typeof(SampleClass).GetTypeId(); - var stringType = typeof(string).GetTypeId(); - var json = $"{{\"name\":\"PublicMethod\",\"declaringType\":\"{declaringType}\",\"parameters\":[{{\"type\":\"{stringType}\"}}]}}"; - var result = _reflector.JsonSerializer.Deserialize(json); - _output.WriteLine($"MethodInfo simple: {result?.Name}"); - Assert.NotNull(result); - Assert.Equal("PublicMethod", result.Name); - } - - [Fact] - public void MethodInfo_Deserialize_MethodWithMultipleParams() - { - var declaringType = typeof(SampleClass).GetTypeId(); - var intType = typeof(int).GetTypeId(); - var json = $"{{\"name\":\"CalculateValue\",\"declaringType\":\"{declaringType}\",\"parameters\":[{{\"type\":\"{intType}\"}},{{\"type\":\"{intType}\"}}]}}"; - var result = _reflector.JsonSerializer.Deserialize(json); - _output.WriteLine($"MethodInfo multi params: {result?.Name}"); - Assert.NotNull(result); - Assert.Equal("CalculateValue", result.Name); - Assert.Equal(2, result.GetParameters().Length); - } - - [Fact] - public void MethodInfo_Deserialize_MissingDeclaringType_Throws() - { - var json = "{\"name\":\"PublicMethod\",\"parameters\":[]}"; - // Throws KeyNotFoundException when declaringType is missing - Assert.ThrowsAny(() => _reflector.JsonSerializer.Deserialize(json)); - } - - [Fact] - public void MethodInfo_Deserialize_MethodNotFound_Throws() - { - var declaringType = typeof(SampleClass).GetTypeId(); - var json = $"{{\"name\":\"NonExistentMethod\",\"declaringType\":\"{declaringType}\",\"parameters\":[]}}"; - Assert.ThrowsAny(() => _reflector.JsonSerializer.Deserialize(json)); - } - - #endregion - - #region TypeJsonConverter Tests - - [Fact] - public void Type_Serialize_Simple() - { - var value = typeof(string); - var json = _reflector.JsonSerializer.Serialize(value, typeof(Type)); - _output.WriteLine($"Type simple: {json}"); - Assert.Contains("System.String", json); - } - - [Fact] - public void Type_Serialize_Custom() - { - var value = typeof(SampleClass); - var json = _reflector.JsonSerializer.Serialize(value, typeof(Type)); - _output.WriteLine($"Type custom: {json}"); - Assert.Contains("SampleClass", json); - } - - [Fact] - public void Type_Serialize_Null() - { - Type? value = null; - var json = _reflector.JsonSerializer.Serialize(value, typeof(Type)); - _output.WriteLine($"Type null: {json}"); - Assert.Equal("null", json); - } - - [Fact] - public void Type_Deserialize_Simple() - { - var json = "\"System.String\""; - var result = _reflector.JsonSerializer.Deserialize(json); - _output.WriteLine($"Type from json: {result?.Name}"); - Assert.NotNull(result); - Assert.Equal(typeof(string), result); - } - - [Fact] - public void Type_Deserialize_Int32() - { - var json = "\"System.Int32\""; - var result = _reflector.JsonSerializer.Deserialize(json); - _output.WriteLine($"Type Int32: {result?.Name}"); - Assert.NotNull(result); - Assert.Equal(typeof(int), result); - } - - [Fact] - public void Type_Deserialize_Null() - { - var json = "null"; - var result = _reflector.JsonSerializer.Deserialize(json); - _output.WriteLine($"Type from null: {result}"); - Assert.Null(result); - } - - [Fact] - public void Type_RoundTrip() - { - var original = typeof(DateTime); - var json = _reflector.JsonSerializer.Serialize(original, typeof(Type)); - var deserialized = _reflector.JsonSerializer.Deserialize(json); - _output.WriteLine($"Type roundtrip: {original.Name} -> {json} -> {deserialized?.Name}"); - Assert.Equal(original, deserialized); - } - - #endregion - - #region UInt16JsonConverter Tests - - [Fact] - public void UInt16_Serialize_Value() - { - ushort value = 12345; - var json = _reflector.JsonSerializer.Serialize(value); - _output.WriteLine($"UInt16 value: {json}"); - Assert.Equal("12345", json); - } - - [Fact] - public void UInt16_Serialize_Zero() - { - ushort value = 0; - var json = _reflector.JsonSerializer.Serialize(value); - _output.WriteLine($"UInt16 zero: {json}"); - Assert.Equal("0", json); - } - - [Fact] - public void UInt16_Serialize_MaxValue() - { - ushort value = ushort.MaxValue; - var json = _reflector.JsonSerializer.Serialize(value); - _output.WriteLine($"UInt16 max: {json}"); - Assert.Equal("65535", json); - } - - [Fact] - public void UInt16_Deserialize_FromNumber() - { - var json = "100"; - var result = _reflector.JsonSerializer.Deserialize(json); - _output.WriteLine($"UInt16 from number: {result}"); - Assert.Equal((ushort)100, result); - } - - [Fact] - public void UInt16_Deserialize_FromString() - { - var json = "\"200\""; - var result = _reflector.JsonSerializer.Deserialize(json); - _output.WriteLine($"UInt16 from string: {result}"); - Assert.Equal((ushort)200, result); - } - - [Fact] - public void UInt16_Deserialize_Null_ToNullable() - { - var json = "null"; - var result = _reflector.JsonSerializer.Deserialize(json); - _output.WriteLine($"UInt16 null: {result}"); - Assert.Null(result); - } - - [Fact] - public void UInt16_RoundTrip() - { - ushort original = 50000; - var json = _reflector.JsonSerializer.Serialize(original); - var deserialized = _reflector.JsonSerializer.Deserialize(json); - _output.WriteLine($"UInt16 roundtrip: {original} -> {json} -> {deserialized}"); - Assert.Equal(original, deserialized); - } - - #endregion - - #region UInt32JsonConverter Tests - - [Fact] - public void UInt32_Serialize_Value() - { - uint value = 1234567890; - var json = _reflector.JsonSerializer.Serialize(value); - _output.WriteLine($"UInt32 value: {json}"); - Assert.Equal("1234567890", json); - } - - [Fact] - public void UInt32_Serialize_MaxValue() - { - uint value = uint.MaxValue; - var json = _reflector.JsonSerializer.Serialize(value); - _output.WriteLine($"UInt32 max: {json}"); - Assert.Equal("4294967295", json); - } - - [Fact] - public void UInt32_Deserialize_FromNumber() - { - var json = "999999"; - var result = _reflector.JsonSerializer.Deserialize(json); - _output.WriteLine($"UInt32 from number: {result}"); - Assert.Equal(999999u, result); - } - - [Fact] - public void UInt32_Deserialize_FromString() - { - var json = "\"100000\""; - var result = _reflector.JsonSerializer.Deserialize(json); - _output.WriteLine($"UInt32 from string: {result}"); - Assert.Equal(100000u, result); - } - - [Fact] - public void UInt32_RoundTrip() - { - uint original = 3000000000; - var json = _reflector.JsonSerializer.Serialize(original); - var deserialized = _reflector.JsonSerializer.Deserialize(json); - _output.WriteLine($"UInt32 roundtrip: {original} -> {json} -> {deserialized}"); - Assert.Equal(original, deserialized); - } - - #endregion - - #region UInt64JsonConverter Tests - - [Fact] - public void UInt64_Serialize_Value() - { - ulong value = 12345678901234567890UL; - var json = _reflector.JsonSerializer.Serialize(value); - _output.WriteLine($"UInt64 value: {json}"); - Assert.Contains("12345678901234567890", json); - } - - [Fact] - public void UInt64_Serialize_MaxValue() - { - ulong value = ulong.MaxValue; - var json = _reflector.JsonSerializer.Serialize(value); - _output.WriteLine($"UInt64 max: {json}"); - Assert.Contains("18446744073709551615", json); - } - - [Fact] - public void UInt64_Deserialize_FromNumber() - { - var json = "999999999"; - var result = _reflector.JsonSerializer.Deserialize(json); - _output.WriteLine($"UInt64 from number: {result}"); - Assert.Equal(999999999UL, result); - } - - [Fact] - public void UInt64_Deserialize_FromString() - { - var json = "\"10000000000\""; - var result = _reflector.JsonSerializer.Deserialize(json); - _output.WriteLine($"UInt64 from string: {result}"); - Assert.Equal(10000000000UL, result); - } - - [Fact] - public void UInt64_RoundTrip() - { - ulong original = 9999999999999UL; - var json = _reflector.JsonSerializer.Serialize(original); - var deserialized = _reflector.JsonSerializer.Deserialize(json); - _output.WriteLine($"UInt64 roundtrip: {original} -> {json} -> {deserialized}"); - Assert.Equal(original, deserialized); - } - - #endregion - - #region DateTimeJsonConverter Tests - - [Fact] - public void DateTime_Serialize_Value() - { - var value = new DateTime(2024, 12, 25, 10, 30, 45, DateTimeKind.Utc); - var json = _reflector.JsonSerializer.Serialize(value); - _output.WriteLine($"DateTime value: {json}"); - Assert.Contains("2024", json); - Assert.Contains("12", json); - Assert.Contains("25", json); - } - - [Fact] - public void DateTime_Serialize_MinValue() - { - var value = DateTime.MinValue; - var json = _reflector.JsonSerializer.Serialize(value); - _output.WriteLine($"DateTime min: {json}"); - Assert.Contains("0001", json); - } - - [Fact] - public void DateTime_Deserialize_Iso8601() - { - var json = "\"2024-06-15T14:30:00Z\""; - var result = _reflector.JsonSerializer.Deserialize(json); - _output.WriteLine($"DateTime from ISO: {result}"); - Assert.Equal(2024, result.Year); - Assert.Equal(6, result.Month); - Assert.Equal(15, result.Day); - } - - [Fact] - public void DateTime_RoundTrip() - { - var original = new DateTime(2025, 1, 1, 0, 0, 0, DateTimeKind.Utc); - var json = _reflector.JsonSerializer.Serialize(original); - var deserialized = _reflector.JsonSerializer.Deserialize(json); - _output.WriteLine($"DateTime roundtrip: {original} -> {json} -> {deserialized}"); - Assert.Equal(original.Year, deserialized.Year); - Assert.Equal(original.Month, deserialized.Month); - Assert.Equal(original.Day, deserialized.Day); - } - - #endregion - - #region GuidJsonConverter Tests - - [Fact] - public void Guid_Serialize_Value() - { - var value = Guid.Parse("12345678-1234-1234-1234-123456789abc"); - var json = _reflector.JsonSerializer.Serialize(value); - _output.WriteLine($"Guid value: {json}"); - Assert.Contains("12345678-1234-1234-1234-123456789abc", json); - } - - [Fact] - public void Guid_Serialize_Empty() - { - var value = Guid.Empty; - var json = _reflector.JsonSerializer.Serialize(value); - _output.WriteLine($"Guid empty: {json}"); - Assert.Contains("00000000-0000-0000-0000-000000000000", json); - } - - [Fact] - public void Guid_Deserialize_WithDashes() - { - var json = "\"abcdef12-3456-7890-abcd-ef1234567890\""; - var result = _reflector.JsonSerializer.Deserialize(json); - _output.WriteLine($"Guid from dashes: {result}"); - Assert.Equal(Guid.Parse("abcdef12-3456-7890-abcd-ef1234567890"), result); - } - - [Fact] - public void Guid_Deserialize_Null_ToNullable() - { - var json = "null"; - var result = _reflector.JsonSerializer.Deserialize(json); - _output.WriteLine($"Guid null: {result}"); - Assert.Null(result); - } - - [Fact] - public void Guid_RoundTrip() - { - var original = Guid.NewGuid(); - var json = _reflector.JsonSerializer.Serialize(original); - var deserialized = _reflector.JsonSerializer.Deserialize(json); - _output.WriteLine($"Guid roundtrip: {original} -> {json} -> {deserialized}"); - Assert.Equal(original, deserialized); - } - - #endregion - } -} diff --git a/ReflectorNet.Tests/src/JsonConverterTests/CommonTypesConverterTests.cs b/ReflectorNet.Tests/src/JsonConverterTests/CommonTypesConverterTests.cs index c31b4a4b..b9ea43eb 100644 --- a/ReflectorNet.Tests/src/JsonConverterTests/CommonTypesConverterTests.cs +++ b/ReflectorNet.Tests/src/JsonConverterTests/CommonTypesConverterTests.cs @@ -324,5 +324,55 @@ public void Uri_Read_UnicodeCharacters() } #endregion + + #region GuidJsonConverter Tests + + [Fact] + public void Guid_Serialize_Value() + { + var value = Guid.Parse("12345678-1234-1234-1234-123456789abc"); + var json = _reflector.JsonSerializer.Serialize(value); + _output.WriteLine($"Guid value: {json}"); + Assert.Contains("12345678-1234-1234-1234-123456789abc", json); + } + + [Fact] + public void Guid_Serialize_Empty() + { + var value = Guid.Empty; + var json = _reflector.JsonSerializer.Serialize(value); + _output.WriteLine($"Guid empty: {json}"); + Assert.Contains("00000000-0000-0000-0000-000000000000", json); + } + + [Fact] + public void Guid_Deserialize_WithDashes() + { + var json = "\"abcdef12-3456-7890-abcd-ef1234567890\""; + var result = _reflector.JsonSerializer.Deserialize(json); + _output.WriteLine($"Guid from dashes: {result}"); + Assert.Equal(Guid.Parse("abcdef12-3456-7890-abcd-ef1234567890"), result); + } + + [Fact] + public void Guid_Deserialize_Null_ToNullable() + { + var json = "null"; + var result = _reflector.JsonSerializer.Deserialize(json); + _output.WriteLine($"Guid null: {result}"); + Assert.Null(result); + } + + [Fact] + public void Guid_RoundTrip() + { + var original = Guid.NewGuid(); + var json = _reflector.JsonSerializer.Serialize(original); + var deserialized = _reflector.JsonSerializer.Deserialize(json); + _output.WriteLine($"Guid roundtrip: {original} -> {json} -> {deserialized}"); + Assert.Equal(original, deserialized); + } + + #endregion } } diff --git a/ReflectorNet.Tests/src/JsonConverterTests/DateTimeConverterTests.cs b/ReflectorNet.Tests/src/JsonConverterTests/DateTimeConverterTests.cs index e787624c..fa36cd76 100644 --- a/ReflectorNet.Tests/src/JsonConverterTests/DateTimeConverterTests.cs +++ b/ReflectorNet.Tests/src/JsonConverterTests/DateTimeConverterTests.cs @@ -291,5 +291,52 @@ public void TimeOnly_RoundTrip_Various() #endregion #endif + + #region DateTimeJsonConverter Tests + + [Fact] + public void DateTime_Serialize_Value() + { + var value = new DateTime(2024, 12, 25, 10, 30, 45, DateTimeKind.Utc); + var json = _reflector.JsonSerializer.Serialize(value); + _output.WriteLine($"DateTime value: {json}"); + Assert.Contains("2024", json); + Assert.Contains("12", json); + Assert.Contains("25", json); + } + + [Fact] + public void DateTime_Serialize_MinValue() + { + var value = DateTime.MinValue; + var json = _reflector.JsonSerializer.Serialize(value); + _output.WriteLine($"DateTime min: {json}"); + Assert.Contains("0001", json); + } + + [Fact] + public void DateTime_Deserialize_Iso8601() + { + var json = "\"2024-06-15T14:30:00Z\""; + var result = _reflector.JsonSerializer.Deserialize(json); + _output.WriteLine($"DateTime from ISO: {result}"); + Assert.Equal(2024, result.Year); + Assert.Equal(6, result.Month); + Assert.Equal(15, result.Day); + } + + [Fact] + public void DateTime_RoundTrip() + { + var original = new DateTime(2025, 1, 1, 0, 0, 0, DateTimeKind.Utc); + var json = _reflector.JsonSerializer.Serialize(original); + var deserialized = _reflector.JsonSerializer.Deserialize(json); + _output.WriteLine($"DateTime roundtrip: {original} -> {json} -> {deserialized}"); + Assert.Equal(original.Year, deserialized.Year); + Assert.Equal(original.Month, deserialized.Month); + Assert.Equal(original.Day, deserialized.Day); + } + + #endregion } } diff --git a/ReflectorNet.Tests/src/JsonConverterTests/EnumConverterTests.cs b/ReflectorNet.Tests/src/JsonConverterTests/EnumConverterTests.cs new file mode 100644 index 00000000..32505f3a --- /dev/null +++ b/ReflectorNet.Tests/src/JsonConverterTests/EnumConverterTests.cs @@ -0,0 +1,105 @@ +using System; +using System.Text.Json; +using com.IvanMurzak.ReflectorNet.Utils; +using Xunit; +using Xunit.Abstractions; + +namespace com.IvanMurzak.ReflectorNet.Tests.JsonConverterTests +{ + /// + /// Tests for Enum JSON converter + /// + public class EnumConverterTests : BaseTest + { + private readonly Reflector _reflector; + + public EnumConverterTests(ITestOutputHelper output) : base(output) + { + _reflector = new Reflector(); + } + + // Sample enum for testing + public enum TestStatus + { + Unknown = 0, + Active = 1, + Inactive = 2, + Pending = 3 + } + + #region EnumJsonConverter Tests + + [Fact] + public void Enum_Serialize_Value() + { + var value = TestStatus.Active; + var json = _reflector.JsonSerializer.Serialize(value); + _output.WriteLine($"Enum value: {json}"); + Assert.Contains("Active", json); + } + + [Fact] + public void Enum_Serialize_Null() + { + TestStatus? value = null; + var json = _reflector.JsonSerializer.Serialize(value); + _output.WriteLine($"Enum null: {json}"); + Assert.Equal("null", json); + } + + [Fact] + public void Enum_Deserialize_FromString() + { + var json = "\"Active\""; + var result = _reflector.JsonSerializer.Deserialize(json); + _output.WriteLine($"Enum from string: {result}"); + Assert.Equal(TestStatus.Active, result); + } + + [Fact] + public void Enum_Deserialize_FromString_CaseInsensitive() + { + var json = "\"active\""; + var result = _reflector.JsonSerializer.Deserialize(json); + _output.WriteLine($"Enum case insensitive: {result}"); + Assert.Equal(TestStatus.Active, result); + } + + [Fact] + public void Enum_Deserialize_FromNumber() + { + var json = "2"; + var result = _reflector.JsonSerializer.Deserialize(json); + _output.WriteLine($"Enum from number: {result}"); + Assert.Equal(TestStatus.Inactive, result); + } + + [Fact] + public void Enum_Deserialize_Null_ToNullable() + { + var json = "null"; + var result = _reflector.JsonSerializer.Deserialize(json); + _output.WriteLine($"Enum null to nullable: {result}"); + Assert.Null(result); + } + + [Fact] + public void Enum_Deserialize_InvalidString_Throws() + { + var json = "\"NotAValidValue\""; + Assert.ThrowsAny(() => _reflector.JsonSerializer.Deserialize(json)); + } + + [Fact] + public void Enum_RoundTrip() + { + var original = TestStatus.Pending; + var json = _reflector.JsonSerializer.Serialize(original); + var deserialized = _reflector.JsonSerializer.Deserialize(json); + _output.WriteLine($"Enum roundtrip: {original} -> {json} -> {deserialized}"); + Assert.Equal(original, deserialized); + } + + #endregion + } +} diff --git a/ReflectorNet.Tests/src/JsonConverterTests/ExceptionConverterTests.cs b/ReflectorNet.Tests/src/JsonConverterTests/ExceptionConverterTests.cs new file mode 100644 index 00000000..50e6bcad --- /dev/null +++ b/ReflectorNet.Tests/src/JsonConverterTests/ExceptionConverterTests.cs @@ -0,0 +1,85 @@ +using System; +using System.Text.Json; +using com.IvanMurzak.ReflectorNet.Utils; +using Xunit; +using Xunit.Abstractions; + +namespace com.IvanMurzak.ReflectorNet.Tests.JsonConverterTests +{ + /// + /// Tests for Exception JSON converter + /// + public class ExceptionConverterTests : BaseTest + { + private readonly Reflector _reflector; + + public ExceptionConverterTests(ITestOutputHelper output) : base(output) + { + _reflector = new Reflector(); + } + + #region ExceptionJsonConverter Tests + + [Fact] + public void Exception_Serialize_Simple() + { + var value = new Exception("Test error message"); + var json = _reflector.JsonSerializer.Serialize(value, typeof(Exception)); + _output.WriteLine($"Exception simple: {json}"); + Assert.Contains("Test error message", json); + Assert.Contains("type", json); + } + + [Fact] + public void Exception_Serialize_WithInnerException() + { + var inner = new InvalidOperationException("Inner error"); + var value = new Exception("Outer error", inner); + var json = _reflector.JsonSerializer.Serialize(value, typeof(Exception)); + _output.WriteLine($"Exception with inner: {json}"); + Assert.Contains("Outer error", json); + Assert.Contains("innerException", json); + } + + [Fact] + public void Exception_Serialize_Null() + { + Exception? value = null; + var json = _reflector.JsonSerializer.Serialize(value, typeof(Exception)); + _output.WriteLine($"Exception null: {json}"); + Assert.Equal("null", json); + } + + [Fact] + public void Exception_Deserialize_Simple() + { + var json = "{\"type\":\"System.Exception\",\"message\":\"Test message\"}"; + var result = _reflector.JsonSerializer.Deserialize(json); + _output.WriteLine($"Exception from json: {result?.Message}"); + Assert.NotNull(result); + Assert.Contains("Test message", result.Message); + } + + [Fact] + public void Exception_Deserialize_Null() + { + var json = "null"; + var result = _reflector.JsonSerializer.Deserialize(json); + _output.WriteLine($"Exception from null: {result}"); + Assert.Null(result); + } + + [Fact] + public void Exception_RoundTrip() + { + var original = new ArgumentException("Invalid argument"); + var json = _reflector.JsonSerializer.Serialize(original, typeof(Exception)); + var deserialized = _reflector.JsonSerializer.Deserialize(json); + _output.WriteLine($"Exception roundtrip: {original.Message} -> {json}"); + Assert.NotNull(deserialized); + Assert.Contains("Invalid argument", deserialized.Message); + } + + #endregion + } +} diff --git a/ReflectorNet.Tests/src/JsonConverterTests/PrimitiveConverterTests.cs b/ReflectorNet.Tests/src/JsonConverterTests/PrimitiveConverterTests.cs index 10ab6fbf..fc1a002d 100644 --- a/ReflectorNet.Tests/src/JsonConverterTests/PrimitiveConverterTests.cs +++ b/ReflectorNet.Tests/src/JsonConverterTests/PrimitiveConverterTests.cs @@ -496,5 +496,173 @@ public void Half_RoundTrip() #endregion #endif + + #region UInt16JsonConverter Tests + + [Fact] + public void UInt16_Serialize_Value() + { + ushort value = 12345; + var json = _reflector.JsonSerializer.Serialize(value); + _output.WriteLine($"UInt16 value: {json}"); + Assert.Equal("12345", json); + } + + [Fact] + public void UInt16_Serialize_Zero() + { + ushort value = 0; + var json = _reflector.JsonSerializer.Serialize(value); + _output.WriteLine($"UInt16 zero: {json}"); + Assert.Equal("0", json); + } + + [Fact] + public void UInt16_Serialize_MaxValue() + { + ushort value = ushort.MaxValue; + var json = _reflector.JsonSerializer.Serialize(value); + _output.WriteLine($"UInt16 max: {json}"); + Assert.Equal("65535", json); + } + + [Fact] + public void UInt16_Deserialize_FromNumber() + { + var json = "100"; + var result = _reflector.JsonSerializer.Deserialize(json); + _output.WriteLine($"UInt16 from number: {result}"); + Assert.Equal((ushort)100, result); + } + + [Fact] + public void UInt16_Deserialize_FromString() + { + var json = "\"200\""; + var result = _reflector.JsonSerializer.Deserialize(json); + _output.WriteLine($"UInt16 from string: {result}"); + Assert.Equal((ushort)200, result); + } + + [Fact] + public void UInt16_Deserialize_Null_ToNullable() + { + var json = "null"; + var result = _reflector.JsonSerializer.Deserialize(json); + _output.WriteLine($"UInt16 null: {result}"); + Assert.Null(result); + } + + [Fact] + public void UInt16_RoundTrip() + { + ushort original = 50000; + var json = _reflector.JsonSerializer.Serialize(original); + var deserialized = _reflector.JsonSerializer.Deserialize(json); + _output.WriteLine($"UInt16 roundtrip: {original} -> {json} -> {deserialized}"); + Assert.Equal(original, deserialized); + } + + #endregion + + #region UInt32JsonConverter Tests + + [Fact] + public void UInt32_Serialize_Value() + { + uint value = 1234567890; + var json = _reflector.JsonSerializer.Serialize(value); + _output.WriteLine($"UInt32 value: {json}"); + Assert.Equal("1234567890", json); + } + + [Fact] + public void UInt32_Serialize_MaxValue() + { + uint value = uint.MaxValue; + var json = _reflector.JsonSerializer.Serialize(value); + _output.WriteLine($"UInt32 max: {json}"); + Assert.Equal("4294967295", json); + } + + [Fact] + public void UInt32_Deserialize_FromNumber() + { + var json = "999999"; + var result = _reflector.JsonSerializer.Deserialize(json); + _output.WriteLine($"UInt32 from number: {result}"); + Assert.Equal(999999u, result); + } + + [Fact] + public void UInt32_Deserialize_FromString() + { + var json = "\"100000\""; + var result = _reflector.JsonSerializer.Deserialize(json); + _output.WriteLine($"UInt32 from string: {result}"); + Assert.Equal(100000u, result); + } + + [Fact] + public void UInt32_RoundTrip() + { + uint original = 3000000000; + var json = _reflector.JsonSerializer.Serialize(original); + var deserialized = _reflector.JsonSerializer.Deserialize(json); + _output.WriteLine($"UInt32 roundtrip: {original} -> {json} -> {deserialized}"); + Assert.Equal(original, deserialized); + } + + #endregion + + #region UInt64JsonConverter Tests + + [Fact] + public void UInt64_Serialize_Value() + { + ulong value = 12345678901234567890UL; + var json = _reflector.JsonSerializer.Serialize(value); + _output.WriteLine($"UInt64 value: {json}"); + Assert.Contains("12345678901234567890", json); + } + + [Fact] + public void UInt64_Serialize_MaxValue() + { + ulong value = ulong.MaxValue; + var json = _reflector.JsonSerializer.Serialize(value); + _output.WriteLine($"UInt64 max: {json}"); + Assert.Contains("18446744073709551615", json); + } + + [Fact] + public void UInt64_Deserialize_FromNumber() + { + var json = "999999999"; + var result = _reflector.JsonSerializer.Deserialize(json); + _output.WriteLine($"UInt64 from number: {result}"); + Assert.Equal(999999999UL, result); + } + + [Fact] + public void UInt64_Deserialize_FromString() + { + var json = "\"10000000000\""; + var result = _reflector.JsonSerializer.Deserialize(json); + _output.WriteLine($"UInt64 from string: {result}"); + Assert.Equal(10000000000UL, result); + } + + [Fact] + public void UInt64_RoundTrip() + { + ulong original = 9999999999999UL; + var json = _reflector.JsonSerializer.Serialize(original); + var deserialized = _reflector.JsonSerializer.Deserialize(json); + _output.WriteLine($"UInt64 roundtrip: {original} -> {json} -> {deserialized}"); + Assert.Equal(original, deserialized); + } + + #endregion } } diff --git a/ReflectorNet.Tests/src/JsonConverterTests/ReflectionConverterTests.cs b/ReflectorNet.Tests/src/JsonConverterTests/ReflectionConverterTests.cs index 407f24ad..9567d874 100644 --- a/ReflectorNet.Tests/src/JsonConverterTests/ReflectionConverterTests.cs +++ b/ReflectorNet.Tests/src/JsonConverterTests/ReflectionConverterTests.cs @@ -276,5 +276,197 @@ public void ParameterInfo_Read_MissingFields_Throws() } #endregion + + // Sample class for field/method reflection tests + public class FieldReflectionSampleClass + { + public string? Name; + public int Value; +#pragma warning disable CS0169 // Field is never used - needed for reflection tests + private string? _privateField; +#pragma warning restore CS0169 + + public void PublicMethod(string input) { } + public int CalculateValue(int a, int b) => a + b; + private void PrivateMethod() { } + } + + #region FieldInfoConverter Tests + + [Fact] + public void FieldInfo_Deserialize_PublicField() + { + var declaringType = typeof(FieldReflectionSampleClass).GetTypeId(); + var json = $"{{\"name\":\"Name\",\"declaringType\":\"{declaringType}\"}}"; + var result = _reflector.JsonSerializer.Deserialize(json); + _output.WriteLine($"FieldInfo public: {result?.Name}"); + Assert.NotNull(result); + Assert.Equal("Name", result.Name); + } + + [Fact] + public void FieldInfo_Deserialize_ValueField() + { + var declaringType = typeof(FieldReflectionSampleClass).GetTypeId(); + var json = $"{{\"name\":\"Value\",\"declaringType\":\"{declaringType}\"}}"; + var result = _reflector.JsonSerializer.Deserialize(json); + _output.WriteLine($"FieldInfo value: {result?.Name}"); + Assert.NotNull(result); + Assert.Equal("Value", result.Name); + Assert.Equal(typeof(int), result.FieldType); + } + + [Fact] + public void FieldInfo_Deserialize_PrivateField() + { + var declaringType = typeof(FieldReflectionSampleClass).GetTypeId(); + var json = $"{{\"name\":\"_privateField\",\"declaringType\":\"{declaringType}\"}}"; + var result = _reflector.JsonSerializer.Deserialize(json); + _output.WriteLine($"FieldInfo private: {result?.Name}"); + Assert.NotNull(result); + Assert.Equal("_privateField", result.Name); + } + + [Fact] + public void FieldInfo_Deserialize_Null() + { + var json = "null"; + var result = _reflector.JsonSerializer.Deserialize(json); + _output.WriteLine($"FieldInfo from null: {result}"); + Assert.Null(result); + } + + [Fact] + public void FieldInfo_Deserialize_MissingName_Throws() + { + var declaringType = typeof(FieldReflectionSampleClass).GetTypeId(); + var json = $"{{\"declaringType\":\"{declaringType}\"}}"; + Assert.ThrowsAny(() => _reflector.JsonSerializer.Deserialize(json)); + } + + [Fact] + public void FieldInfo_Deserialize_FieldNotFound_Throws() + { + var declaringType = typeof(FieldReflectionSampleClass).GetTypeId(); + var json = $"{{\"name\":\"NonExistentField\",\"declaringType\":\"{declaringType}\"}}"; + Assert.ThrowsAny(() => _reflector.JsonSerializer.Deserialize(json)); + } + + #endregion + + #region MethodInfoConverter Tests + + [Fact] + public void MethodInfo_Deserialize_SimpleMethod() + { + var declaringType = typeof(FieldReflectionSampleClass).GetTypeId(); + var stringType = typeof(string).GetTypeId(); + var json = $"{{\"name\":\"PublicMethod\",\"declaringType\":\"{declaringType}\",\"parameters\":[{{\"type\":\"{stringType}\"}}]}}"; + var result = _reflector.JsonSerializer.Deserialize(json); + _output.WriteLine($"MethodInfo simple: {result?.Name}"); + Assert.NotNull(result); + Assert.Equal("PublicMethod", result.Name); + } + + [Fact] + public void MethodInfo_Deserialize_MethodWithMultipleParams() + { + var declaringType = typeof(FieldReflectionSampleClass).GetTypeId(); + var intType = typeof(int).GetTypeId(); + var json = $"{{\"name\":\"CalculateValue\",\"declaringType\":\"{declaringType}\",\"parameters\":[{{\"type\":\"{intType}\"}},{{\"type\":\"{intType}\"}}]}}"; + var result = _reflector.JsonSerializer.Deserialize(json); + _output.WriteLine($"MethodInfo multi params: {result?.Name}"); + Assert.NotNull(result); + Assert.Equal("CalculateValue", result.Name); + Assert.Equal(2, result.GetParameters().Length); + } + + [Fact] + public void MethodInfo_Deserialize_MissingDeclaringType_Throws() + { + var json = "{\"name\":\"PublicMethod\",\"parameters\":[]}"; + // Throws KeyNotFoundException when declaringType is missing + Assert.ThrowsAny(() => _reflector.JsonSerializer.Deserialize(json)); + } + + [Fact] + public void MethodInfo_Deserialize_MethodNotFound_Throws() + { + var declaringType = typeof(FieldReflectionSampleClass).GetTypeId(); + var json = $"{{\"name\":\"NonExistentMethod\",\"declaringType\":\"{declaringType}\",\"parameters\":[]}}"; + Assert.ThrowsAny(() => _reflector.JsonSerializer.Deserialize(json)); + } + + #endregion + + #region TypeJsonConverter Tests + + [Fact] + public void Type_Serialize_Simple() + { + var value = typeof(string); + var json = _reflector.JsonSerializer.Serialize(value, typeof(Type)); + _output.WriteLine($"Type simple: {json}"); + Assert.Contains("System.String", json); + } + + [Fact] + public void Type_Serialize_Custom() + { + var value = typeof(FieldReflectionSampleClass); + var json = _reflector.JsonSerializer.Serialize(value, typeof(Type)); + _output.WriteLine($"Type custom: {json}"); + Assert.Contains("FieldReflectionSampleClass", json); + } + + [Fact] + public void Type_Serialize_Null() + { + Type? value = null; + var json = _reflector.JsonSerializer.Serialize(value, typeof(Type)); + _output.WriteLine($"Type null: {json}"); + Assert.Equal("null", json); + } + + [Fact] + public void Type_Deserialize_Simple() + { + var json = "\"System.String\""; + var result = _reflector.JsonSerializer.Deserialize(json); + _output.WriteLine($"Type from json: {result?.Name}"); + Assert.NotNull(result); + Assert.Equal(typeof(string), result); + } + + [Fact] + public void Type_Deserialize_Int32() + { + var json = "\"System.Int32\""; + var result = _reflector.JsonSerializer.Deserialize(json); + _output.WriteLine($"Type Int32: {result?.Name}"); + Assert.NotNull(result); + Assert.Equal(typeof(int), result); + } + + [Fact] + public void Type_Deserialize_Null() + { + var json = "null"; + var result = _reflector.JsonSerializer.Deserialize(json); + _output.WriteLine($"Type from null: {result}"); + Assert.Null(result); + } + + [Fact] + public void Type_RoundTrip() + { + var original = typeof(DateTime); + var json = _reflector.JsonSerializer.Serialize(original, typeof(Type)); + var deserialized = _reflector.JsonSerializer.Deserialize(json); + _output.WriteLine($"Type roundtrip: {original.Name} -> {json} -> {deserialized?.Name}"); + Assert.Equal(original, deserialized); + } + + #endregion } } From a9c7d82a85c8bb5cb988353b746b13deac88aabe Mon Sep 17 00:00:00 2001 From: Ivan Murzak Date: Fri, 26 Dec 2025 03:29:11 -0800 Subject: [PATCH 20/23] feat: Add comprehensive tests for JSON converters including Bool, Char, BigInteger, DateTimeOffset, Numeric types, TimeSpan, and UIntPtr --- .../BasicTypesConverterTests.cs | 92 +++++++++ .../BigIntegerConverterTests.cs | 80 ++++++++ .../DateTimeOffsetConverterTests.cs | 79 ++++++++ .../NumericTypesConverterTests.cs | 180 ++++++++++++++++++ .../TimeSpanConverterTests.cs | 88 +++++++++ .../UIntPtrConverterTests.cs | 70 +++++++ 6 files changed, 589 insertions(+) create mode 100644 ReflectorNet.Tests/src/JsonConverterTests/BasicTypesConverterTests.cs create mode 100644 ReflectorNet.Tests/src/JsonConverterTests/BigIntegerConverterTests.cs create mode 100644 ReflectorNet.Tests/src/JsonConverterTests/DateTimeOffsetConverterTests.cs create mode 100644 ReflectorNet.Tests/src/JsonConverterTests/NumericTypesConverterTests.cs create mode 100644 ReflectorNet.Tests/src/JsonConverterTests/TimeSpanConverterTests.cs create mode 100644 ReflectorNet.Tests/src/JsonConverterTests/UIntPtrConverterTests.cs diff --git a/ReflectorNet.Tests/src/JsonConverterTests/BasicTypesConverterTests.cs b/ReflectorNet.Tests/src/JsonConverterTests/BasicTypesConverterTests.cs new file mode 100644 index 00000000..b1c713af --- /dev/null +++ b/ReflectorNet.Tests/src/JsonConverterTests/BasicTypesConverterTests.cs @@ -0,0 +1,92 @@ +using System; +using System.Text.Json; +using Xunit; +using Xunit.Abstractions; + +namespace com.IvanMurzak.ReflectorNet.Tests.JsonConverterTests +{ + /// + /// Tests for Bool and Char JSON converters + /// + public class BasicTypesConverterTests : BaseTest + { + private readonly Reflector _reflector; + + public BasicTypesConverterTests(ITestOutputHelper output) : base(output) + { + _reflector = new Reflector(); + } + + #region BoolJsonConverter Tests + + [Fact] + public void Bool_Serialize_True() + { + var value = true; + var json = _reflector.JsonSerializer.Serialize(value); + Assert.Equal("true", json); + } + + [Fact] + public void Bool_Serialize_False() + { + var value = false; + var json = _reflector.JsonSerializer.Serialize(value); + Assert.Equal("false", json); + } + + [Fact] + public void Bool_Deserialize_True() + { + var json = "true"; + var result = _reflector.JsonSerializer.Deserialize(json); + Assert.True(result); + } + + [Fact] + public void Bool_Deserialize_False() + { + var json = "false"; + var result = _reflector.JsonSerializer.Deserialize(json); + Assert.False(result); + } + + [Fact] + public void Bool_Deserialize_FromString() + { + var json = "\"true\""; + var result = _reflector.JsonSerializer.Deserialize(json); + Assert.True(result); + } + + #endregion + + #region CharJsonConverter Tests + + [Fact] + public void Char_Serialize_Value() + { + var value = 'A'; + var json = _reflector.JsonSerializer.Serialize(value); + Assert.Equal("\"A\"", json); + } + + [Fact] + public void Char_Deserialize_Value() + { + var json = "\"B\""; + var result = _reflector.JsonSerializer.Deserialize(json); + Assert.Equal('B', result); + } + + [Fact] + public void Char_Deserialize_FromNumber() + { + var json = "65"; + var result = _reflector.JsonSerializer.Deserialize(json); + Assert.Equal('A', result); + } + + #endregion + } +} diff --git a/ReflectorNet.Tests/src/JsonConverterTests/BigIntegerConverterTests.cs b/ReflectorNet.Tests/src/JsonConverterTests/BigIntegerConverterTests.cs new file mode 100644 index 00000000..4d19a390 --- /dev/null +++ b/ReflectorNet.Tests/src/JsonConverterTests/BigIntegerConverterTests.cs @@ -0,0 +1,80 @@ +using System; +using System.Numerics; +using System.Text.Json; +using Xunit; +using Xunit.Abstractions; + +namespace com.IvanMurzak.ReflectorNet.Tests.JsonConverterTests +{ + /// + /// Tests for BigInteger JSON converter + /// + public class BigIntegerConverterTests : BaseTest + { + private readonly Reflector _reflector; + + public BigIntegerConverterTests(ITestOutputHelper output) : base(output) + { + _reflector = new Reflector(); + } + + #region BigIntegerJsonConverter Tests + + [Fact] + public void BigInteger_Serialize_Value() + { + var value = BigInteger.Parse("123456789012345678901234567890"); + var json = _reflector.JsonSerializer.Serialize(value); + _output.WriteLine($"BigInteger value: {json}"); + Assert.Contains("123456789012345678901234567890", json); + } + + [Fact] + public void BigInteger_Serialize_Zero() + { + var value = BigInteger.Zero; + var json = _reflector.JsonSerializer.Serialize(value); + _output.WriteLine($"BigInteger zero: {json}"); + Assert.Equal("\"0\"", json); + } + + [Fact] + public void BigInteger_Serialize_Negative() + { + var value = BigInteger.Parse("-98765432109876543210"); + var json = _reflector.JsonSerializer.Serialize(value); + _output.WriteLine($"BigInteger negative: {json}"); + Assert.Contains("-98765432109876543210", json); + } + + [Fact] + public void BigInteger_Deserialize_FromNumber() + { + var json = "12345678901234567890"; + var result = _reflector.JsonSerializer.Deserialize(json); + _output.WriteLine($"BigInteger from number: {result}"); + Assert.Equal(BigInteger.Parse("12345678901234567890"), result); + } + + [Fact] + public void BigInteger_Deserialize_FromString() + { + var json = "\"999999999999999999999999999999\""; + var result = _reflector.JsonSerializer.Deserialize(json); + _output.WriteLine($"BigInteger from string: {result}"); + Assert.Equal(BigInteger.Parse("999999999999999999999999999999"), result); + } + + [Fact] + public void BigInteger_RoundTrip() + { + var original = BigInteger.Pow(2, 100); + var json = _reflector.JsonSerializer.Serialize(original); + var deserialized = _reflector.JsonSerializer.Deserialize(json); + _output.WriteLine($"BigInteger roundtrip: {original} -> {json} -> {deserialized}"); + Assert.Equal(original, deserialized); + } + + #endregion + } +} diff --git a/ReflectorNet.Tests/src/JsonConverterTests/DateTimeOffsetConverterTests.cs b/ReflectorNet.Tests/src/JsonConverterTests/DateTimeOffsetConverterTests.cs new file mode 100644 index 00000000..f540ca6d --- /dev/null +++ b/ReflectorNet.Tests/src/JsonConverterTests/DateTimeOffsetConverterTests.cs @@ -0,0 +1,79 @@ +using System; +using System.Text.Json; +using Xunit; +using Xunit.Abstractions; + +namespace com.IvanMurzak.ReflectorNet.Tests.JsonConverterTests +{ + /// + /// Tests for DateTimeOffset JSON converter + /// + public class DateTimeOffsetConverterTests : BaseTest + { + private readonly Reflector _reflector; + + public DateTimeOffsetConverterTests(ITestOutputHelper output) : base(output) + { + _reflector = new Reflector(); + } + + #region DateTimeOffsetJsonConverter Tests + + [Fact] + public void DateTimeOffset_Serialize_Value() + { + var value = new DateTimeOffset(2024, 12, 25, 10, 30, 45, TimeSpan.FromHours(2)); + var json = _reflector.JsonSerializer.Serialize(value); + _output.WriteLine($"DateTimeOffset value: {json}"); + Assert.Contains("2024-12-25T10:30:45", json); + // The + is encoded as \u002B in JSON string + Assert.Contains("02:00", json); + } + + [Fact] + public void DateTimeOffset_Serialize_Utc() + { + var value = new DateTimeOffset(2024, 12, 25, 10, 30, 45, TimeSpan.Zero); + var json = _reflector.JsonSerializer.Serialize(value); + _output.WriteLine($"DateTimeOffset UTC: {json}"); + Assert.Contains("2024-12-25T10:30:45", json); + // The + is encoded as \u002B in JSON string + Assert.Contains("00:00", json); + } + + [Fact] + public void DateTimeOffset_Serialize_MinValue() + { + var value = DateTimeOffset.MinValue; + var json = _reflector.JsonSerializer.Serialize(value); + _output.WriteLine($"DateTimeOffset min: {json}"); + Assert.Contains("0001-01-01T00:00:00", json); + // The + is encoded as \u002B in JSON string + Assert.Contains("00:00", json); + } + + [Fact] + public void DateTimeOffset_Deserialize_Iso8601() + { + var json = "\"2024-06-15T14:30:00+03:00\""; + var result = _reflector.JsonSerializer.Deserialize(json); + _output.WriteLine($"DateTimeOffset from ISO: {result}"); + Assert.Equal(2024, result.Year); + Assert.Equal(6, result.Month); + Assert.Equal(15, result.Day); + Assert.Equal(TimeSpan.FromHours(3), result.Offset); + } + + [Fact] + public void DateTimeOffset_RoundTrip() + { + var original = new DateTimeOffset(2025, 1, 1, 0, 0, 0, TimeSpan.FromHours(-5)); + var json = _reflector.JsonSerializer.Serialize(original); + var deserialized = _reflector.JsonSerializer.Deserialize(json); + _output.WriteLine($"DateTimeOffset roundtrip: {original} -> {json} -> {deserialized}"); + Assert.Equal(original, deserialized); + } + + #endregion + } +} diff --git a/ReflectorNet.Tests/src/JsonConverterTests/NumericTypesConverterTests.cs b/ReflectorNet.Tests/src/JsonConverterTests/NumericTypesConverterTests.cs new file mode 100644 index 00000000..0aedc0fc --- /dev/null +++ b/ReflectorNet.Tests/src/JsonConverterTests/NumericTypesConverterTests.cs @@ -0,0 +1,180 @@ +using System; +using System.Text.Json; +using Xunit; +using Xunit.Abstractions; + +namespace com.IvanMurzak.ReflectorNet.Tests.JsonConverterTests +{ + /// + /// Tests for standard numeric types JSON converters: Byte, SByte, Int16, Int32, Int64, Single, Double, Decimal + /// + public class NumericTypesConverterTests : BaseTest + { + private readonly Reflector _reflector; + + public NumericTypesConverterTests(ITestOutputHelper output) : base(output) + { + _reflector = new Reflector(); + } + + #region ByteJsonConverter Tests + + [Fact] + public void Byte_Serialize_Value() + { + byte value = 255; + var json = _reflector.JsonSerializer.Serialize(value); + Assert.Equal("255", json); + } + + [Fact] + public void Byte_Deserialize_Value() + { + var json = "128"; + var result = _reflector.JsonSerializer.Deserialize(json); + Assert.Equal((byte)128, result); + } + + #endregion + + #region SByteJsonConverter Tests + + [Fact] + public void SByte_Serialize_Value() + { + sbyte value = -128; + var json = _reflector.JsonSerializer.Serialize(value); + Assert.Equal("-128", json); + } + + [Fact] + public void SByte_Deserialize_Value() + { + var json = "-50"; + var result = _reflector.JsonSerializer.Deserialize(json); + Assert.Equal((sbyte)-50, result); + } + + #endregion + + #region Int16JsonConverter Tests + + [Fact] + public void Int16_Serialize_Value() + { + short value = -32000; + var json = _reflector.JsonSerializer.Serialize(value); + Assert.Equal("-32000", json); + } + + [Fact] + public void Int16_Deserialize_Value() + { + var json = "32000"; + var result = _reflector.JsonSerializer.Deserialize(json); + Assert.Equal((short)32000, result); + } + + #endregion + + #region Int32JsonConverter Tests + + [Fact] + public void Int32_Serialize_Value() + { + int value = -2000000000; + var json = _reflector.JsonSerializer.Serialize(value); + Assert.Equal("-2000000000", json); + } + + [Fact] + public void Int32_Deserialize_Value() + { + var json = "2000000000"; + var result = _reflector.JsonSerializer.Deserialize(json); + Assert.Equal(2000000000, result); + } + + #endregion + + #region Int64JsonConverter Tests + + [Fact] + public void Int64_Serialize_Value() + { + long value = -9000000000000000000; + var json = _reflector.JsonSerializer.Serialize(value); + Assert.Contains("-9000000000000000000", json); + } + + [Fact] + public void Int64_Deserialize_Value() + { + var json = "9000000000000000000"; + var result = _reflector.JsonSerializer.Deserialize(json); + Assert.Equal(9000000000000000000, result); + } + + #endregion + + #region SingleJsonConverter Tests + + [Fact] + public void Single_Serialize_Value() + { + float value = 3.14f; + var json = _reflector.JsonSerializer.Serialize(value); + Assert.Contains("3.14", json); + } + + [Fact] + public void Single_Deserialize_Value() + { + var json = "1.5"; + var result = _reflector.JsonSerializer.Deserialize(json); + Assert.Equal(1.5f, result); + } + + #endregion + + #region DoubleJsonConverter Tests + + [Fact] + public void Double_Serialize_Value() + { + double value = 3.14159265359; + var json = _reflector.JsonSerializer.Serialize(value); + Assert.Contains("3.14159265359", json); + } + + [Fact] + public void Double_Deserialize_Value() + { + var json = "2.71828"; + var result = _reflector.JsonSerializer.Deserialize(json); + Assert.Equal(2.71828, result); + } + + #endregion + + #region DecimalJsonConverter Tests + + [Fact] + public void Decimal_Serialize_Value() + { + decimal value = 123.456m; + var json = _reflector.JsonSerializer.Serialize(value); + Assert.Contains("123.456", json); + } + + [Fact] + public void Decimal_Deserialize_Value() + { + var json = "789.012"; + var result = _reflector.JsonSerializer.Deserialize(json); + Assert.Equal(789.012m, result); + } + + #endregion + } +} diff --git a/ReflectorNet.Tests/src/JsonConverterTests/TimeSpanConverterTests.cs b/ReflectorNet.Tests/src/JsonConverterTests/TimeSpanConverterTests.cs new file mode 100644 index 00000000..1c9ac4e5 --- /dev/null +++ b/ReflectorNet.Tests/src/JsonConverterTests/TimeSpanConverterTests.cs @@ -0,0 +1,88 @@ +using System; +using System.Text.Json; +using Xunit; +using Xunit.Abstractions; + +namespace com.IvanMurzak.ReflectorNet.Tests.JsonConverterTests +{ + /// + /// Tests for TimeSpan JSON converter + /// + public class TimeSpanConverterTests : BaseTest + { + private readonly Reflector _reflector; + + public TimeSpanConverterTests(ITestOutputHelper output) : base(output) + { + _reflector = new Reflector(); + } + + #region TimeSpanJsonConverter Tests + + [Fact] + public void TimeSpan_Serialize_Value() + { + var value = new TimeSpan(1, 2, 3, 4, 5); + var json = _reflector.JsonSerializer.Serialize(value); + _output.WriteLine($"TimeSpan value: {json}"); + Assert.Contains("1.02:03:04.0050000", json); + } + + [Fact] + public void TimeSpan_Serialize_Zero() + { + var value = TimeSpan.Zero; + var json = _reflector.JsonSerializer.Serialize(value); + _output.WriteLine($"TimeSpan zero: {json}"); + Assert.Contains("00:00:00", json); + } + + [Fact] + public void TimeSpan_Serialize_MinValue() + { + var value = TimeSpan.MinValue; + var json = _reflector.JsonSerializer.Serialize(value); + _output.WriteLine($"TimeSpan min: {json}"); + Assert.Contains("-10675199.02:48:05.4775808", json); + } + + [Fact] + public void TimeSpan_Serialize_MaxValue() + { + var value = TimeSpan.MaxValue; + var json = _reflector.JsonSerializer.Serialize(value); + _output.WriteLine($"TimeSpan max: {json}"); + Assert.Contains("10675199.02:48:05.4775807", json); + } + + [Fact] + public void TimeSpan_Deserialize_FromString() + { + var json = "\"1.02:03:04.0050000\""; + var result = _reflector.JsonSerializer.Deserialize(json); + _output.WriteLine($"TimeSpan from string: {result}"); + Assert.Equal(new TimeSpan(1, 2, 3, 4, 5), result); + } + + [Fact] + public void TimeSpan_Deserialize_Null_ToNullable() + { + var json = "null"; + var result = _reflector.JsonSerializer.Deserialize(json); + _output.WriteLine($"TimeSpan null: {result}"); + Assert.Null(result); + } + + [Fact] + public void TimeSpan_RoundTrip() + { + var original = new TimeSpan(1234567890); + var json = _reflector.JsonSerializer.Serialize(original); + var deserialized = _reflector.JsonSerializer.Deserialize(json); + _output.WriteLine($"TimeSpan roundtrip: {original} -> {json} -> {deserialized}"); + Assert.Equal(original, deserialized); + } + + #endregion + } +} diff --git a/ReflectorNet.Tests/src/JsonConverterTests/UIntPtrConverterTests.cs b/ReflectorNet.Tests/src/JsonConverterTests/UIntPtrConverterTests.cs new file mode 100644 index 00000000..6a590d7e --- /dev/null +++ b/ReflectorNet.Tests/src/JsonConverterTests/UIntPtrConverterTests.cs @@ -0,0 +1,70 @@ +using System; +using System.Text.Json; +using Xunit; +using Xunit.Abstractions; + +namespace com.IvanMurzak.ReflectorNet.Tests.JsonConverterTests +{ + /// + /// Tests for UIntPtr JSON converter + /// + public class UIntPtrConverterTests : BaseTest + { + private readonly Reflector _reflector; + + public UIntPtrConverterTests(ITestOutputHelper output) : base(output) + { + _reflector = new Reflector(); + } + + #region UIntPtrJsonConverter Tests + + [Fact] + public void UIntPtr_Serialize_Value() + { + var value = new UIntPtr(12345); + var json = _reflector.JsonSerializer.Serialize(value); + _output.WriteLine($"UIntPtr value: {json}"); + Assert.Equal("12345", json); + } + + [Fact] + public void UIntPtr_Serialize_Zero() + { + var value = UIntPtr.Zero; + var json = _reflector.JsonSerializer.Serialize(value); + _output.WriteLine($"UIntPtr zero: {json}"); + Assert.Equal("0", json); + } + + [Fact] + public void UIntPtr_Deserialize_FromNumber() + { + var json = "67890"; + var result = _reflector.JsonSerializer.Deserialize(json); + _output.WriteLine($"UIntPtr from number: {result}"); + Assert.Equal(new UIntPtr(67890), result); + } + + [Fact] + public void UIntPtr_Deserialize_FromString() + { + var json = "\"98765\""; + var result = _reflector.JsonSerializer.Deserialize(json); + _output.WriteLine($"UIntPtr from string: {result}"); + Assert.Equal(new UIntPtr(98765), result); + } + + [Fact] + public void UIntPtr_RoundTrip() + { + var original = new UIntPtr(123456789); + var json = _reflector.JsonSerializer.Serialize(original); + var deserialized = _reflector.JsonSerializer.Deserialize(json); + _output.WriteLine($"UIntPtr roundtrip: {original} -> {json} -> {deserialized}"); + Assert.Equal(original, deserialized); + } + + #endregion + } +} From 23d4842691054f7a9a3b46567c32354e33b56243 Mon Sep 17 00:00:00 2001 From: Ivan Murzak Date: Fri, 26 Dec 2025 03:38:54 -0800 Subject: [PATCH 21/23] fix: Update null checks for improved readability in JSON converters --- .../src/SchemaTests/SchemaSerializationValidationTests.cs | 2 +- ReflectorNet/src/Converter/Json/GuidJsonConverter.cs | 2 +- ReflectorNet/src/Converter/Json/IntPtrJsonConverter.cs | 2 +- ReflectorNet/src/Converter/Json/TimeOnlyJsonConverter.cs | 2 +- ReflectorNet/src/Converter/Json/UIntPtrJsonConverter.cs | 2 +- ReflectorNet/src/Converter/Json/UriJsonConverter.cs | 2 +- ReflectorNet/src/Converter/Json/VersionJsonConverter.cs | 2 +- 7 files changed, 7 insertions(+), 7 deletions(-) diff --git a/ReflectorNet.Tests/src/SchemaTests/SchemaSerializationValidationTests.cs b/ReflectorNet.Tests/src/SchemaTests/SchemaSerializationValidationTests.cs index 8859f3d2..f31ea548 100644 --- a/ReflectorNet.Tests/src/SchemaTests/SchemaSerializationValidationTests.cs +++ b/ReflectorNet.Tests/src/SchemaTests/SchemaSerializationValidationTests.cs @@ -107,7 +107,7 @@ private void ValidateBasicSchemaConformance(JsonNode schema, object serialized, // SerializedMember has structure: { "name": "...", "typeName": "...", "value": ... } // We need to extract just the "value" field to compare against the schema - // Note: If value is null, the "value" field might be missing or null + // Note: If value == null, the "value" field might be missing or null JsonNode? valueNode = null; if (jsonNode is JsonObject jsonObject) { diff --git a/ReflectorNet/src/Converter/Json/GuidJsonConverter.cs b/ReflectorNet/src/Converter/Json/GuidJsonConverter.cs index f8a97859..bc2b8148 100644 --- a/ReflectorNet/src/Converter/Json/GuidJsonConverter.cs +++ b/ReflectorNet/src/Converter/Json/GuidJsonConverter.cs @@ -57,7 +57,7 @@ public override bool CanConvert(Type typeToConvert) public override void Write(Utf8JsonWriter writer, object? value, JsonSerializerOptions options) { - if (value is null) + if (value == null) { writer.WriteNullValue(); return; diff --git a/ReflectorNet/src/Converter/Json/IntPtrJsonConverter.cs b/ReflectorNet/src/Converter/Json/IntPtrJsonConverter.cs index 3dddf357..d45b93c1 100644 --- a/ReflectorNet/src/Converter/Json/IntPtrJsonConverter.cs +++ b/ReflectorNet/src/Converter/Json/IntPtrJsonConverter.cs @@ -49,7 +49,7 @@ public override bool CanConvert(Type typeToConvert) public override void Write(Utf8JsonWriter writer, object? value, JsonSerializerOptions options) { - if (value is null) + if (value == null) { writer.WriteNullValue(); return; diff --git a/ReflectorNet/src/Converter/Json/TimeOnlyJsonConverter.cs b/ReflectorNet/src/Converter/Json/TimeOnlyJsonConverter.cs index 93ac471f..6d25171e 100644 --- a/ReflectorNet/src/Converter/Json/TimeOnlyJsonConverter.cs +++ b/ReflectorNet/src/Converter/Json/TimeOnlyJsonConverter.cs @@ -27,7 +27,7 @@ public override TimeOnly Read(ref Utf8JsonReader reader, Type typeToConvert, Jso throw new JsonException($"Expected string token for TimeOnly, but got {reader.TokenType}"); var stringValue = reader.GetString(); - if (stringValue is null) + if (stringValue == null) throw new JsonException("Expected non-null string value for TimeOnly."); if (TimeOnly.TryParseExact(stringValue, Format, null, System.Globalization.DateTimeStyles.None, out var result)) diff --git a/ReflectorNet/src/Converter/Json/UIntPtrJsonConverter.cs b/ReflectorNet/src/Converter/Json/UIntPtrJsonConverter.cs index db6f4bb7..cea56e2e 100644 --- a/ReflectorNet/src/Converter/Json/UIntPtrJsonConverter.cs +++ b/ReflectorNet/src/Converter/Json/UIntPtrJsonConverter.cs @@ -50,7 +50,7 @@ public override bool CanConvert(Type typeToConvert) public override void Write(Utf8JsonWriter writer, object value, JsonSerializerOptions options) { - if (value is null) + if (value == null) { writer.WriteNullValue(); return; diff --git a/ReflectorNet/src/Converter/Json/UriJsonConverter.cs b/ReflectorNet/src/Converter/Json/UriJsonConverter.cs index e448fa37..c421e119 100644 --- a/ReflectorNet/src/Converter/Json/UriJsonConverter.cs +++ b/ReflectorNet/src/Converter/Json/UriJsonConverter.cs @@ -36,7 +36,7 @@ public class UriJsonConverter : JsonConverter public override void Write(Utf8JsonWriter writer, Uri? value, JsonSerializerOptions options) { - if (value is null) + if (value == null) { writer.WriteNullValue(); return; diff --git a/ReflectorNet/src/Converter/Json/VersionJsonConverter.cs b/ReflectorNet/src/Converter/Json/VersionJsonConverter.cs index d85b00ec..3913d468 100644 --- a/ReflectorNet/src/Converter/Json/VersionJsonConverter.cs +++ b/ReflectorNet/src/Converter/Json/VersionJsonConverter.cs @@ -37,7 +37,7 @@ public class VersionJsonConverter : JsonConverter public override void Write(Utf8JsonWriter writer, Version? value, JsonSerializerOptions options) { - if (value is null) + if (value == null) { writer.WriteNullValue(); return; From 47b6a5d322c2a69a3ff3d5fa196388eb4bd10171 Mon Sep 17 00:00:00 2001 From: Ivan Murzak Date: Fri, 26 Dec 2025 03:48:08 -0800 Subject: [PATCH 22/23] fix: Enhance security and error handling in JSON converters for Assembly, IntPtr, and UIntPtr --- .../Converter/Json/AssemblyJsonConverter.cs | 16 +++++------- .../src/Converter/Json/IntPtrJsonConverter.cs | 25 ++++++++++++++----- .../Converter/Json/UIntPtrJsonConverter.cs | 25 ++++++++++++++----- 3 files changed, 44 insertions(+), 22 deletions(-) diff --git a/ReflectorNet/src/Converter/Json/AssemblyJsonConverter.cs b/ReflectorNet/src/Converter/Json/AssemblyJsonConverter.cs index 3e17530a..8800c85b 100644 --- a/ReflectorNet/src/Converter/Json/AssemblyJsonConverter.cs +++ b/ReflectorNet/src/Converter/Json/AssemblyJsonConverter.cs @@ -30,20 +30,16 @@ public class AssemblyJsonConverter : JsonConverter if (string.IsNullOrWhiteSpace(assemblyName)) return null; + // Security: Only resolve assemblies already loaded in the AppDomain. + // We intentionally do NOT call Assembly.Load() to prevent loading + // arbitrary assemblies from untrusted JSON input. var assembly = AppDomain.CurrentDomain.GetAssemblies() .FirstOrDefault(a => a.FullName == assemblyName || a.GetName().Name == assemblyName); - if (assembly != null) - return assembly; + if (assembly is null) + throw new JsonException($"Assembly '{assemblyName}' is not loaded. For security reasons, only already-loaded assemblies can be resolved."); - try - { - return Assembly.Load(assemblyName); - } - catch - { - throw new JsonException($"Unable to find or load assembly: {assemblyName}"); - } + return assembly; } public override void Write(Utf8JsonWriter writer, Assembly? value, JsonSerializerOptions options) diff --git a/ReflectorNet/src/Converter/Json/IntPtrJsonConverter.cs b/ReflectorNet/src/Converter/Json/IntPtrJsonConverter.cs index d45b93c1..4d485233 100644 --- a/ReflectorNet/src/Converter/Json/IntPtrJsonConverter.cs +++ b/ReflectorNet/src/Converter/Json/IntPtrJsonConverter.cs @@ -32,19 +32,32 @@ public override bool CanConvert(Type typeToConvert) return IntPtr.Zero; } + long value; if (reader.TokenType == JsonTokenType.Number) { - return new IntPtr(reader.GetInt64()); + value = reader.GetInt64(); } - - if (reader.TokenType == JsonTokenType.String) + else if (reader.TokenType == JsonTokenType.String) { var stringValue = reader.GetString(); - if (long.TryParse(stringValue, out var result)) - return new IntPtr(result); + if (!long.TryParse(stringValue, out value)) + throw new JsonException($"Unable to parse '{stringValue}' as IntPtr."); + } + else + { + throw new JsonException($"Expected number or string token for IntPtr, but got {reader.TokenType}"); + } + + // Validate value fits in platform's IntPtr size to avoid overflow + if (IntPtr.Size == 4) + { + if (value < int.MinValue || value > int.MaxValue) + throw new JsonException($"Value {value} is outside the range of IntPtr on this 32-bit platform."); + + return new IntPtr((int)value); } - throw new JsonException($"Expected number or string token for IntPtr, but got {reader.TokenType}"); + return new IntPtr(value); } public override void Write(Utf8JsonWriter writer, object? value, JsonSerializerOptions options) diff --git a/ReflectorNet/src/Converter/Json/UIntPtrJsonConverter.cs b/ReflectorNet/src/Converter/Json/UIntPtrJsonConverter.cs index cea56e2e..65904c8b 100644 --- a/ReflectorNet/src/Converter/Json/UIntPtrJsonConverter.cs +++ b/ReflectorNet/src/Converter/Json/UIntPtrJsonConverter.cs @@ -33,19 +33,32 @@ public override bool CanConvert(Type typeToConvert) return UIntPtr.Zero; } + ulong value; if (reader.TokenType == JsonTokenType.Number) { - return new UIntPtr(reader.GetUInt64()); + value = reader.GetUInt64(); } - - if (reader.TokenType == JsonTokenType.String) + else if (reader.TokenType == JsonTokenType.String) { var stringValue = reader.GetString(); - if (ulong.TryParse(stringValue, out var result)) - return new UIntPtr(result); + if (!ulong.TryParse(stringValue, out value)) + throw new JsonException($"Unable to parse '{stringValue}' as UIntPtr."); + } + else + { + throw new JsonException($"Expected number or string token for UIntPtr, but got {reader.TokenType}"); + } + + // Validate value fits in platform's UIntPtr size to avoid overflow + if (UIntPtr.Size == 4) + { + if (value > uint.MaxValue) + throw new JsonException($"Value {value} is outside the range of UIntPtr on this 32-bit platform."); + + return new UIntPtr((uint)value); } - throw new JsonException($"Expected number or string token for UIntPtr, but got {reader.TokenType}"); + return new UIntPtr(value); } public override void Write(Utf8JsonWriter writer, object value, JsonSerializerOptions options) From a3f54af12e5fb23dfaed166b55512218ac2cac07 Mon Sep 17 00:00:00 2001 From: Ivan Murzak Date: Fri, 26 Dec 2025 04:04:10 -0800 Subject: [PATCH 23/23] fix: Remove unused namespace import in HalfJsonConverter --- ReflectorNet/src/Converter/Json/HalfJsonConverter.cs | 1 - 1 file changed, 1 deletion(-) diff --git a/ReflectorNet/src/Converter/Json/HalfJsonConverter.cs b/ReflectorNet/src/Converter/Json/HalfJsonConverter.cs index cee933e9..170d2448 100644 --- a/ReflectorNet/src/Converter/Json/HalfJsonConverter.cs +++ b/ReflectorNet/src/Converter/Json/HalfJsonConverter.cs @@ -9,7 +9,6 @@ using System; using System.Text.Json; using System.Text.Json.Serialization; -using com.IvanMurzak.ReflectorNet.Utils; namespace com.IvanMurzak.ReflectorNet.Json {