From d9acacc2e0a02cac0c82bd23affdb6de23fc61b6 Mon Sep 17 00:00:00 2001 From: lior1997k Date: Sat, 2 May 2026 16:38:02 +0300 Subject: [PATCH 1/4] fix: sanitize JSON Schema $ref type IDs for array and generic type names GetSchemaTypeId() now percent-encodes [ ] < > characters in type IDs to produce valid JSON Schema $ref URI-reference keys. This fixes invalid $defs keys like System.String[] and IEnumerable which violate RFC 3986. Also updates TestSchemaTypeId assertions to match the new sanitized output. Ultraworked with Sisyphus Co-authored-by: Sisyphus --- .../src/SchemaTests/TestSchemaTypeId.cs | 18 +++++++++--------- ReflectorNet/src/Utils/TypeUtils.Name.cs | 16 ++++++++++++++-- 2 files changed, 23 insertions(+), 11 deletions(-) diff --git a/ReflectorNet.Tests/src/SchemaTests/TestSchemaTypeId.cs b/ReflectorNet.Tests/src/SchemaTests/TestSchemaTypeId.cs index 06372e5..4564f79 100644 --- a/ReflectorNet.Tests/src/SchemaTests/TestSchemaTypeId.cs +++ b/ReflectorNet.Tests/src/SchemaTests/TestSchemaTypeId.cs @@ -18,7 +18,7 @@ public void GetSchemaTypeId_SimpleArray_ShouldAppendArray() var result = type.GetSchemaTypeId(); // Assert - Assert.Equal($"System.Int32{TypeUtils.ArraySuffix}", result); + Assert.Equal("System.Int32%5B%5D", result); _output.WriteLine($"int[] -> {result}"); } @@ -32,7 +32,7 @@ public void GetSchemaTypeId_NestedArray_ShouldAppendMultipleArrays() var result = type.GetSchemaTypeId(); // Assert - Assert.Equal($"System.Int32{TypeUtils.ArraySuffix}{TypeUtils.ArraySuffix}", result); + Assert.Equal("System.Int32%5B%5D%5B%5D", result); _output.WriteLine($"int[][] -> {result}"); } @@ -46,7 +46,7 @@ public void GetSchemaTypeId_TripleNestedArray_ShouldAppendThreeArrays() var result = type.GetSchemaTypeId(); // Assert - Assert.Equal($"System.Int32{TypeUtils.ArraySuffix}{TypeUtils.ArraySuffix}{TypeUtils.ArraySuffix}", result); + Assert.Equal("System.Int32%5B%5D%5B%5D%5B%5D", result); _output.WriteLine($"int[][][] -> {result}"); } @@ -60,7 +60,7 @@ public void GetSchemaTypeId_StringArray_ShouldWorkForAnyType() var result = type.GetSchemaTypeId(); // Assert - Assert.Equal($"System.String{TypeUtils.ArraySuffix}", result); + Assert.Equal("System.String%5B%5D", result); _output.WriteLine($"string[] -> {result}"); } @@ -102,7 +102,7 @@ public void GetSchemaTypeId_NullableArrayType_ShouldHandleUnderlyingArrayType() var result = type.GetSchemaTypeId(); // Assert - Assert.Equal($"System.Int32{TypeUtils.ArraySuffix}", result); + Assert.Equal("System.Int32%5B%5D", result); _output.WriteLine($"int?[] -> {result}"); } @@ -116,7 +116,7 @@ public void GetSchemaTypeId_IEnumerableOfInt_ShouldReturnGenericFormat() var result = type.GetSchemaTypeId(); // Assert - Assert.Equal("System.Collections.Generic.IEnumerable", result); + Assert.Equal("System.Collections.Generic.IEnumerable%3CSystem.Int32%3E", result); _output.WriteLine($"IEnumerable -> {result}"); } @@ -130,7 +130,7 @@ public void GetSchemaTypeId_ICollectionOfString_ShouldReturnGenericFormat() var result = type.GetSchemaTypeId(); // Assert - Assert.Equal("System.Collections.Generic.ICollection", result); + Assert.Equal("System.Collections.Generic.ICollection%3CSystem.String%3E", result); _output.WriteLine($"ICollection -> {result}"); } @@ -144,7 +144,7 @@ public void GetSchemaTypeId_IListOfInt_ShouldReturnGenericFormat() var result = type.GetSchemaTypeId(); // Assert - Assert.Equal("System.Collections.Generic.IList", result); + Assert.Equal("System.Collections.Generic.IList%3CSystem.Int32%3E", result); _output.WriteLine($"IList -> {result}"); } @@ -172,7 +172,7 @@ public void GetSchemaTypeId_ArrayOfCustomClass_ShouldAppendArray() var result = type.GetSchemaTypeId(); // Assert - Assert.Equal($"com.IvanMurzak.ReflectorNet.Tests.SchemaTests.TestType{TypeUtils.ArraySuffix}", result); + Assert.Equal("com.IvanMurzak.ReflectorNet.Tests.SchemaTests.TestType%5B%5D", result); _output.WriteLine($"TestType[] -> {result}"); } } diff --git a/ReflectorNet/src/Utils/TypeUtils.Name.cs b/ReflectorNet/src/Utils/TypeUtils.Name.cs index feeaf50..79268f8 100644 --- a/ReflectorNet/src/Utils/TypeUtils.Name.cs +++ b/ReflectorNet/src/Utils/TypeUtils.Name.cs @@ -115,8 +115,20 @@ public static string GetTypeId(Type type) return Sanitize(type); } - public static string GetSchemaTypeId() => GetTypeId(typeof(T)); - public static string GetSchemaTypeId(Type type) => GetTypeId(type); + public static string GetSchemaTypeId() => GetSchemaTypeId(typeof(T)); + public static string GetSchemaTypeId(Type type) + { + var typeId = GetTypeId(type); + return SanitizeForJsonSchemaRef(typeId); + } + + private static string SanitizeForJsonSchemaRef(string typeId) + { + if (string.IsNullOrEmpty(typeId)) + return typeId; + return typeId.Replace("[", "%5B").Replace("]", "%5D") + .Replace("<", "%3C").Replace(">", "%3E"); + } public static bool IsNameMatch(Type? type, string? typeName) { From 3ade8b3f262b5874fd0f3ea2cf66fbfe1187a96d Mon Sep 17 00:00:00 2001 From: lior1997k Date: Mon, 4 May 2026 10:05:31 +0300 Subject: [PATCH 2/4] fix: sanitize '+' in nested class names for JSON Schema $ref URI references --- .../src/SchemaTests/TestSchemaTypeId.cs | 15 +++++++++++++++ ReflectorNet/src/Utils/TypeUtils.Name.cs | 3 ++- 2 files changed, 17 insertions(+), 1 deletion(-) diff --git a/ReflectorNet.Tests/src/SchemaTests/TestSchemaTypeId.cs b/ReflectorNet.Tests/src/SchemaTests/TestSchemaTypeId.cs index 4564f79..40d99b6 100644 --- a/ReflectorNet.Tests/src/SchemaTests/TestSchemaTypeId.cs +++ b/ReflectorNet.Tests/src/SchemaTests/TestSchemaTypeId.cs @@ -1,4 +1,5 @@ using System.Collections.Generic; +using com.IvanMurzak.ReflectorNet.OuterAssembly.Model; using com.IvanMurzak.ReflectorNet.Utils; using Xunit.Abstractions; @@ -8,6 +9,20 @@ public class TestSchemaTypeId : BaseTest { public TestSchemaTypeId(ITestOutputHelper output) : base(output) { } + [Fact] + public void GetSchemaTypeId_NestedClass_ShouldEncodePlusSymbol() + { + // Arrange + var type = typeof(ParentClass.NestedClass); + + // Act + var result = type.GetSchemaTypeId(); + + // Assert + Assert.Equal("com.IvanMurzak.ReflectorNet.OuterAssembly.Model.ParentClass%2BNestedClass", result); + _output.WriteLine($"ParentClass.NestedClass -> {result}"); + } + [Fact] public void GetSchemaTypeId_SimpleArray_ShouldAppendArray() { diff --git a/ReflectorNet/src/Utils/TypeUtils.Name.cs b/ReflectorNet/src/Utils/TypeUtils.Name.cs index 79268f8..4c15e3f 100644 --- a/ReflectorNet/src/Utils/TypeUtils.Name.cs +++ b/ReflectorNet/src/Utils/TypeUtils.Name.cs @@ -127,7 +127,8 @@ private static string SanitizeForJsonSchemaRef(string typeId) if (string.IsNullOrEmpty(typeId)) return typeId; return typeId.Replace("[", "%5B").Replace("]", "%5D") - .Replace("<", "%3C").Replace(">", "%3E"); + .Replace("<", "%3C").Replace(">", "%3E") + .Replace("+", "%2B"); } public static bool IsNameMatch(Type? type, string? typeName) From 0eb8bdf2fa5522723ee7d8caed466fb67702ae80 Mon Sep 17 00:00:00 2001 From: lior1997k Date: Mon, 4 May 2026 11:14:20 +0300 Subject: [PATCH 3/4] refactor: update ArraySuffix constant to '%5B%5D' and use it in schema tests instead of hardcoding --- .../src/SchemaTests/TestSchemaTypeId.cs | 12 ++-- .../src/TypeUtilsTests/GetTypeIdTests.cs | 60 +++++++++---------- ReflectorNet/src/Utils/TypeUtils.Name.cs | 2 +- 3 files changed, 37 insertions(+), 37 deletions(-) diff --git a/ReflectorNet.Tests/src/SchemaTests/TestSchemaTypeId.cs b/ReflectorNet.Tests/src/SchemaTests/TestSchemaTypeId.cs index 40d99b6..31c07fc 100644 --- a/ReflectorNet.Tests/src/SchemaTests/TestSchemaTypeId.cs +++ b/ReflectorNet.Tests/src/SchemaTests/TestSchemaTypeId.cs @@ -33,7 +33,7 @@ public void GetSchemaTypeId_SimpleArray_ShouldAppendArray() var result = type.GetSchemaTypeId(); // Assert - Assert.Equal("System.Int32%5B%5D", result); + Assert.Equal($"System.Int32{TypeUtils.ArraySuffix}", result); _output.WriteLine($"int[] -> {result}"); } @@ -47,7 +47,7 @@ public void GetSchemaTypeId_NestedArray_ShouldAppendMultipleArrays() var result = type.GetSchemaTypeId(); // Assert - Assert.Equal("System.Int32%5B%5D%5B%5D", result); + Assert.Equal($"System.Int32{TypeUtils.ArraySuffix}{TypeUtils.ArraySuffix}", result); _output.WriteLine($"int[][] -> {result}"); } @@ -61,7 +61,7 @@ public void GetSchemaTypeId_TripleNestedArray_ShouldAppendThreeArrays() var result = type.GetSchemaTypeId(); // Assert - Assert.Equal("System.Int32%5B%5D%5B%5D%5B%5D", result); + Assert.Equal($"System.Int32{TypeUtils.ArraySuffix}{TypeUtils.ArraySuffix}{TypeUtils.ArraySuffix}", result); _output.WriteLine($"int[][][] -> {result}"); } @@ -75,7 +75,7 @@ public void GetSchemaTypeId_StringArray_ShouldWorkForAnyType() var result = type.GetSchemaTypeId(); // Assert - Assert.Equal("System.String%5B%5D", result); + Assert.Equal($"System.String{TypeUtils.ArraySuffix}", result); _output.WriteLine($"string[] -> {result}"); } @@ -117,7 +117,7 @@ public void GetSchemaTypeId_NullableArrayType_ShouldHandleUnderlyingArrayType() var result = type.GetSchemaTypeId(); // Assert - Assert.Equal("System.Int32%5B%5D", result); + Assert.Equal($"System.Int32{TypeUtils.ArraySuffix}", result); _output.WriteLine($"int?[] -> {result}"); } @@ -187,7 +187,7 @@ public void GetSchemaTypeId_ArrayOfCustomClass_ShouldAppendArray() var result = type.GetSchemaTypeId(); // Assert - Assert.Equal("com.IvanMurzak.ReflectorNet.Tests.SchemaTests.TestType%5B%5D", result); + Assert.Equal($"com.IvanMurzak.ReflectorNet.Tests.SchemaTests.TestType{TypeUtils.ArraySuffix}", result); _output.WriteLine($"TestType[] -> {result}"); } } diff --git a/ReflectorNet.Tests/src/TypeUtilsTests/GetTypeIdTests.cs b/ReflectorNet.Tests/src/TypeUtilsTests/GetTypeIdTests.cs index 607be57..d03bc77 100644 --- a/ReflectorNet.Tests/src/TypeUtilsTests/GetTypeIdTests.cs +++ b/ReflectorNet.Tests/src/TypeUtilsTests/GetTypeIdTests.cs @@ -87,22 +87,22 @@ public GetTypeIdTests(ITestOutputHelper output) : base(output) { } public static readonly Dictionary BuiltInArrayTypes = new Dictionary { // Simple arrays - ["System.Int32[]"] = typeof(int[]), - ["System.String[]"] = typeof(string[]), - ["System.Boolean[]"] = typeof(bool[]), - ["System.Double[]"] = typeof(double[]), - ["System.Object[]"] = typeof(object[]), - ["System.Byte[]"] = typeof(byte[]), - ["System.DateTime[]"] = typeof(DateTime[]), - ["System.Guid[]"] = typeof(Guid[]), + ["System.Int32%5B%5D"] = typeof(int[]), + ["System.String%5B%5D"] = typeof(string[]), + ["System.Boolean%5B%5D"] = typeof(bool[]), + ["System.Double%5B%5D"] = typeof(double[]), + ["System.Object%5B%5D"] = typeof(object[]), + ["System.Byte%5B%5D"] = typeof(byte[]), + ["System.DateTime%5B%5D"] = typeof(DateTime[]), + ["System.Guid%5B%5D"] = typeof(Guid[]), // Jagged arrays - ["System.Int32[][]"] = typeof(int[][]), - ["System.String[][]"] = typeof(string[][]), - ["System.Object[][]"] = typeof(object[][]), + ["System.Int32%5B%5D%5B%5D"] = typeof(int[][]), + ["System.String%5B%5D%5B%5D"] = typeof(string[][]), + ["System.Object%5B%5D%5B%5D"] = typeof(object[][]), // Triple jagged arrays - ["System.Int32[][][]"] = typeof(int[][][]), + ["System.Int32%5B%5D%5B%5D%5B%5D"] = typeof(int[][][]), // Multi-dimensional arrays ["System.Int32[,]"] = typeof(int[,]), @@ -118,11 +118,11 @@ public GetTypeIdTests(ITestOutputHelper output) : base(output) { } // Mixed arrays (Array of 2D arrays) // Note: C# syntax int[][,] is (int[,])[] -> System.Int32[,][] - ["System.Int32[,][]"] = typeof(int[][,]), + ["System.Int32[,]%5B%5D"] = typeof(int[][,]), // Mixed arrays (2D array of arrays) // Note: C# syntax int[,][] is (int[])[,] -> System.Int32[][,] - ["System.Int32[][,]"] = typeof(int[,][]), + ["System.Int32%5B%5D[,]"] = typeof(int[,][]), }; #endregion @@ -185,12 +185,12 @@ public GetTypeIdTests(ITestOutputHelper output) : base(output) { } public static readonly Dictionary NestedGenericTypes = new Dictionary { // List of arrays - ["System.Collections.Generic.List"] = typeof(List), - ["System.Collections.Generic.List"] = typeof(List), + ["System.Collections.Generic.List"] = typeof(List), + ["System.Collections.Generic.List"] = typeof(List), // Array of lists - ["System.Collections.Generic.List[]"] = typeof(List[]), - ["System.Collections.Generic.List[]"] = typeof(List[]), + ["System.Collections.Generic.List%5B%5D"] = typeof(List[]), + ["System.Collections.Generic.List%5B%5D"] = typeof(List[]), // List of lists ["System.Collections.Generic.List>"] = typeof(List>), @@ -232,8 +232,8 @@ public GetTypeIdTests(ITestOutputHelper output) : base(output) { } ["com.IvanMurzak.ReflectorNet.Tests.Model.SolarSystem+CelestialBody"] = typeof(SolarSystem.CelestialBody), // Arrays of custom types - ["com.IvanMurzak.ReflectorNet.Tests.Model.Vector3[]"] = typeof(Vector3[]), - ["com.IvanMurzak.ReflectorNet.Tests.Model.GameObjectRef[]"] = typeof(GameObjectRef[]), + ["com.IvanMurzak.ReflectorNet.Tests.Model.Vector3%5B%5D"] = typeof(Vector3[]), + ["com.IvanMurzak.ReflectorNet.Tests.Model.GameObjectRef%5B%5D"] = typeof(GameObjectRef[]), // Generic with custom types ["System.Collections.Generic.List"] = typeof(List), @@ -258,7 +258,7 @@ public GetTypeIdTests(ITestOutputHelper output) : base(output) { } ["com.IvanMurzak.ReflectorNet.Model.MethodData"] = typeof(MethodData), // Arrays - ["com.IvanMurzak.ReflectorNet.Model.SerializedMember[]"] = typeof(SerializedMember[]), + ["com.IvanMurzak.ReflectorNet.Model.SerializedMember%5B%5D"] = typeof(SerializedMember[]), // Generics with ReflectorNet types ["System.Collections.Generic.List"] = typeof(List), @@ -353,18 +353,18 @@ public GetTypeIdTests(ITestOutputHelper output) : base(output) { } public static readonly Dictionary OuterAssemblyArrayTypes = new Dictionary { // Simple arrays - ["com.IvanMurzak.ReflectorNet.OuterAssembly.Model.OuterSimpleClass[]"] = typeof(OuterSimpleClass[]), - ["com.IvanMurzak.ReflectorNet.OuterAssembly.Model.OuterSimpleStruct[]"] = typeof(OuterSimpleStruct[]), - ["com.IvanMurzak.ReflectorNet.OuterAssembly.Model.OuterEnum[]"] = typeof(OuterEnum[]), + ["com.IvanMurzak.ReflectorNet.OuterAssembly.Model.OuterSimpleClass%5B%5D"] = typeof(OuterSimpleClass[]), + ["com.IvanMurzak.ReflectorNet.OuterAssembly.Model.OuterSimpleStruct%5B%5D"] = typeof(OuterSimpleStruct[]), + ["com.IvanMurzak.ReflectorNet.OuterAssembly.Model.OuterEnum%5B%5D"] = typeof(OuterEnum[]), // Jagged arrays - ["com.IvanMurzak.ReflectorNet.OuterAssembly.Model.OuterSimpleClass[][]"] = typeof(OuterSimpleClass[][]), + ["com.IvanMurzak.ReflectorNet.OuterAssembly.Model.OuterSimpleClass%5B%5D%5B%5D"] = typeof(OuterSimpleClass[][]), // Generic arrays - ["com.IvanMurzak.ReflectorNet.OuterAssembly.Model.OuterGenericClass[]"] = typeof(OuterGenericClass[]), + ["com.IvanMurzak.ReflectorNet.OuterAssembly.Model.OuterGenericClass%5B%5D"] = typeof(OuterGenericClass[]), // Nested type arrays - ["com.IvanMurzak.ReflectorNet.OuterAssembly.Model.OuterContainer+NestedClass[]"] = typeof(OuterContainer.NestedClass[]), + ["com.IvanMurzak.ReflectorNet.OuterAssembly.Model.OuterContainer+NestedClass%5B%5D"] = typeof(OuterContainer.NestedClass[]), }; #endregion @@ -393,13 +393,13 @@ public GetTypeIdTests(ITestOutputHelper output) : base(output) { } ["System.Collections.Generic.List>"] = typeof(List>), // Array of generic outer assembly type - ["com.IvanMurzak.ReflectorNet.OuterAssembly.Model.OuterGenericClass[]"] = typeof(OuterGenericClass[]), + ["com.IvanMurzak.ReflectorNet.OuterAssembly.Model.OuterGenericClass%5B%5D"] = typeof(OuterGenericClass[]), // Generic with array type argument - ["System.Collections.Generic.List"] = typeof(List), + ["System.Collections.Generic.List"] = typeof(List), // Dictionary with array value type - ["System.Collections.Generic.Dictionary"] = typeof(Dictionary), + ["System.Collections.Generic.Dictionary"] = typeof(Dictionary), // Cross-assembly generic combinations ["com.IvanMurzak.ReflectorNet.OuterAssembly.Model.OuterGenericClass"] = typeof(OuterGenericClass), diff --git a/ReflectorNet/src/Utils/TypeUtils.Name.cs b/ReflectorNet/src/Utils/TypeUtils.Name.cs index 4c15e3f..35f9b24 100644 --- a/ReflectorNet/src/Utils/TypeUtils.Name.cs +++ b/ReflectorNet/src/Utils/TypeUtils.Name.cs @@ -5,7 +5,7 @@ namespace com.IvanMurzak.ReflectorNet.Utils { public static partial class TypeUtils { - public const string ArraySuffix = "[]"; + public const string ArraySuffix = "%5B%5D"; /// /// Returns the sanitized type name. From 46d88b15306d07c109d360f62a966a98a5d559f2 Mon Sep 17 00:00:00 2001 From: lior1997k Date: Mon, 4 May 2026 10:22:14 +0300 Subject: [PATCH 4/4] test: add comprehensive nested class JSON Schema coverage - Add deeply nested (LevelOne.LevelTwo.LevelThree.LevelFour) test types - Add generic nested (GenericOuter.GenericInner) test types - Add type ID sanitization tests for +, <, > characters - Add schema generation tests for nested class / validation - Add return schema, argument schema, and array schema tests for nested types - Add cross-assembly nested class schema validation All 1438 tests pass (including 27 new ones). --- .../src/Model/NestedClass.cs | 31 +++ .../src/SchemaTests/NestedClassSchemaTests.cs | 203 ++++++++++++++++++ .../src/SchemaTests/TestSchemaTypeId.cs | 23 +- 3 files changed, 252 insertions(+), 5 deletions(-) create mode 100644 ReflectorNet.Tests/src/SchemaTests/NestedClassSchemaTests.cs diff --git a/ReflectorNet.Tests.OuterAssembly/src/Model/NestedClass.cs b/ReflectorNet.Tests.OuterAssembly/src/Model/NestedClass.cs index 44661fe..ee53456 100644 --- a/ReflectorNet.Tests.OuterAssembly/src/Model/NestedClass.cs +++ b/ReflectorNet.Tests.OuterAssembly/src/Model/NestedClass.cs @@ -65,4 +65,35 @@ public WrapperClass(T? valueField, T? valueProperty) /// public T? EchoNullable(T? value) => value; } + + public class LevelOne + { + public class LevelTwo + { + public class LevelThree + { + public class LevelFour + { + public string DeepProperty { get; set; } = "deep"; + } + + public LevelFour? NestedInstance { get; set; } + } + + public LevelThree? NestedInstance { get; set; } + } + + public LevelTwo? NestedInstance { get; set; } + } + + public class GenericOuter + { + public class GenericInner + { + public T? OuterValue { get; set; } + public U? InnerValue { get; set; } + } + + public GenericInner? SelfReferencingInner { get; set; } + } } \ No newline at end of file diff --git a/ReflectorNet.Tests/src/SchemaTests/NestedClassSchemaTests.cs b/ReflectorNet.Tests/src/SchemaTests/NestedClassSchemaTests.cs new file mode 100644 index 0000000..ddd8656 --- /dev/null +++ b/ReflectorNet.Tests/src/SchemaTests/NestedClassSchemaTests.cs @@ -0,0 +1,203 @@ +using System; +using System.Linq; +using System.Reflection; +using System.Text.Json.Nodes; +using com.IvanMurzak.ReflectorNet.OuterAssembly.Model; +using com.IvanMurzak.ReflectorNet.Utils; +using Xunit.Abstractions; + +namespace com.IvanMurzak.ReflectorNet.Tests.SchemaTests +{ + public class NestedClassSchemaTests : SchemaTestBase + { + public NestedClassSchemaTests(ITestOutputHelper output) : base(output) { } + + [Fact] + public void Schema_NestedClass_GeneratesValidSchema() + { + var reflector = new Reflector(); + var schema = JsonSchemaValidation(typeof(ParentClass.NestedClass), reflector); + + Assert.NotNull(schema); + Assert.Equal(JsonSchema.Object, schema[JsonSchema.Type]?.ToString()); + Assert.True(schema.AsObject().ContainsKey(JsonSchema.Properties)); + + var properties = schema[JsonSchema.Properties]!.AsObject(); + Assert.True(properties.ContainsKey("NestedField")); + Assert.True(properties.ContainsKey("NestedProperty")); + Assert.False(properties.ContainsKey("NestedStaticField")); + Assert.False(properties.ContainsKey("NestedStaticProperty")); + } + + [Fact] + public void Schema_DeeplyNestedClass_GeneratesValidSchema() + { + var reflector = new Reflector(); + var schema = JsonSchemaValidation(typeof(LevelOne.LevelTwo.LevelThree.LevelFour), reflector); + + Assert.NotNull(schema); + Assert.Equal(JsonSchema.Object, schema[JsonSchema.Type]?.ToString()); + Assert.True(schema.AsObject().ContainsKey(JsonSchema.Properties)); + + var properties = schema[JsonSchema.Properties]!.AsObject(); + Assert.True(properties.ContainsKey("DeepProperty")); + } + + [Fact] + public void Schema_DeeplyNestedHierarchy_GeneratesValidSchema() + { + var reflector = new Reflector(); + var schema = JsonSchemaValidation(typeof(LevelOne), reflector); + + Assert.NotNull(schema); + Assert.Equal(JsonSchema.Object, schema[JsonSchema.Type]?.ToString()); + Assert.True(schema.AsObject().ContainsKey(JsonSchema.Properties)); + + var properties = schema[JsonSchema.Properties]!.AsObject(); + Assert.True(properties.ContainsKey("NestedInstance")); + + var nestedInstanceSchema = properties["NestedInstance"]!.AsObject(); + Assert.True(nestedInstanceSchema.ContainsKey(JsonSchema.Ref) || nestedInstanceSchema.ContainsKey(JsonSchema.Type)); + + AssertAllRefsDefined(schema); + } + + [Fact] + public void Schema_GenericNestedClass_GeneratesValidSchema() + { + var reflector = new Reflector(); + var type = typeof(GenericOuter.GenericInner); + var schema = JsonSchemaValidation(type, reflector); + + Assert.NotNull(schema); + Assert.Equal(JsonSchema.Object, schema[JsonSchema.Type]?.ToString()); + Assert.True(schema.AsObject().ContainsKey(JsonSchema.Properties)); + + var properties = schema[JsonSchema.Properties]!.AsObject(); + Assert.True(properties.ContainsKey("OuterValue")); + Assert.True(properties.ContainsKey("InnerValue")); + } + + [Fact] + public void Schema_GenericNestedClass_SelfReferencing_GeneratesValidSchema() + { + var reflector = new Reflector(); + var type = typeof(GenericOuter); + var schema = JsonSchemaValidation(type, reflector); + + Assert.NotNull(schema); + Assert.Equal(JsonSchema.Object, schema[JsonSchema.Type]?.ToString()); + Assert.True(schema.AsObject().ContainsKey(JsonSchema.Properties)); + + var properties = schema[JsonSchema.Properties]!.AsObject(); + Assert.True(properties.ContainsKey("SelfReferencingInner")); + + AssertAllRefsDefined(schema); + } + + [Fact] + public void Schema_NestedClass_RefsAreDefinedInDefs() + { + var reflector = new Reflector(); + var type = typeof(LevelOne); + var schema = reflector.GetSchema(type); + + Assert.NotNull(schema); + Assert.True(schema.AsObject().ContainsKey(JsonSchema.Defs), "Schema should contain $defs for nested types"); + + var defines = schema[JsonSchema.Defs]!.AsObject(); + Assert.NotNull(defines); + + var levelTwoId = typeof(LevelOne.LevelTwo).GetSchemaTypeId(); + var levelThreeId = typeof(LevelOne.LevelTwo.LevelThree).GetSchemaTypeId(); + var levelFourId = typeof(LevelOne.LevelTwo.LevelThree.LevelFour).GetSchemaTypeId(); + + Assert.True(defines.ContainsKey(levelTwoId), $"$defs should contain {levelTwoId}"); + Assert.True(defines.ContainsKey(levelThreeId), $"$defs should contain {levelThreeId}"); + Assert.True(defines.ContainsKey(levelFourId), $"$defs should contain {levelFourId}"); + + AssertAllRefsDefined(schema); + } + + [Fact] + public void Schema_NestedClass_ReturnSchema_HasValidRefs() + { + var reflector = new Reflector(); + var methodInfo = GetType().GetMethod(nameof(NestedClassReturnMethod), BindingFlags.NonPublic | BindingFlags.Instance)!; + var schema = reflector.GetReturnSchema(methodInfo); + + Assert.NotNull(schema); + AssertCustomTypeReturnSchema(schema, new[] { "NestedField", "NestedProperty" }, shouldBeRequired: true); + AssertAllRefsDefined(schema); + } + + [Fact] + public void Schema_DeeplyNestedClass_ReturnSchema_HasValidRefs() + { + var reflector = new Reflector(); + var methodInfo = GetType().GetMethod(nameof(DeeplyNestedClassReturnMethod), BindingFlags.NonPublic | BindingFlags.Instance)!; + var schema = reflector.GetReturnSchema(methodInfo); + + Assert.NotNull(schema); + AssertCustomTypeReturnSchema(schema, new[] { "DeepProperty" }, shouldBeRequired: true); + AssertAllRefsDefined(schema); + } + + [Fact] + public void Schema_GenericNestedClass_ReturnSchema_HasValidRefs() + { + var reflector = new Reflector(); + var methodInfo = GetType().GetMethod(nameof(GenericNestedClassReturnMethod), BindingFlags.NonPublic | BindingFlags.Instance)!; + var schema = reflector.GetReturnSchema(methodInfo); + + Assert.NotNull(schema); + AssertCustomTypeReturnSchema(schema, new[] { "OuterValue", "InnerValue" }, shouldBeRequired: true); + AssertAllRefsDefined(schema); + } + + [Fact] + public void Schema_NestedClass_Array_ReturnSchema_HasValidRefs() + { + var reflector = new Reflector(); + var methodInfo = GetType().GetMethod(nameof(NestedClassArrayReturnMethod), BindingFlags.NonPublic | BindingFlags.Instance)!; + var schema = reflector.GetReturnSchema(methodInfo); + + Assert.NotNull(schema); + AssertArrayReturnSchema(schema, JsonSchema.Object, shouldBeRequired: true); + AssertAllRefsDefined(schema); + } + + [Fact] + public void Schema_NestedClass_ArgumentSchema_HasValidRefs() + { + var reflector = new Reflector(); + var methodInfo = GetType().GetMethod(nameof(NestedClassArgumentMethod), BindingFlags.NonPublic | BindingFlags.Instance)!; + var schema = reflector.GetArgumentsSchema(methodInfo); + + Assert.NotNull(schema); + Assert.True(schema.AsObject().ContainsKey(JsonSchema.Defs), "Argument schema should contain $defs"); + AssertAllRefsDefined(schema); + } + + [Fact] + public void Schema_CrossAssemblyNestedClass_GeneratesValidSchema() + { + var reflector = new Reflector(); + var schema = JsonSchemaValidation(typeof(StaticParentClass.NestedClass), reflector); + + Assert.NotNull(schema); + Assert.Equal(JsonSchema.Object, schema[JsonSchema.Type]?.ToString()); + Assert.True(schema.AsObject().ContainsKey(JsonSchema.Properties)); + + var properties = schema[JsonSchema.Properties]!.AsObject(); + Assert.True(properties.ContainsKey("NestedField")); + Assert.True(properties.ContainsKey("NestedProperty")); + } + + private ParentClass.NestedClass NestedClassReturnMethod() => new ParentClass.NestedClass(); + private LevelOne.LevelTwo.LevelThree.LevelFour DeeplyNestedClassReturnMethod() => new LevelOne.LevelTwo.LevelThree.LevelFour(); + private GenericOuter.GenericInner GenericNestedClassReturnMethod() => new GenericOuter.GenericInner(); + private ParentClass.NestedClass[] NestedClassArrayReturnMethod() => new ParentClass.NestedClass[0]; + private void NestedClassArgumentMethod(ParentClass.NestedClass nested) { } + } +} diff --git a/ReflectorNet.Tests/src/SchemaTests/TestSchemaTypeId.cs b/ReflectorNet.Tests/src/SchemaTests/TestSchemaTypeId.cs index 31c07fc..94227da 100644 --- a/ReflectorNet.Tests/src/SchemaTests/TestSchemaTypeId.cs +++ b/ReflectorNet.Tests/src/SchemaTests/TestSchemaTypeId.cs @@ -12,17 +12,30 @@ public TestSchemaTypeId(ITestOutputHelper output) : base(output) { } [Fact] public void GetSchemaTypeId_NestedClass_ShouldEncodePlusSymbol() { - // Arrange var type = typeof(ParentClass.NestedClass); - - // Act var result = type.GetSchemaTypeId(); - - // Assert Assert.Equal("com.IvanMurzak.ReflectorNet.OuterAssembly.Model.ParentClass%2BNestedClass", result); _output.WriteLine($"ParentClass.NestedClass -> {result}"); } + [Fact] + public void GetSchemaTypeId_DeeplyNestedClass_ShouldEncodeAllPlusSymbols() + { + var type = typeof(LevelOne.LevelTwo.LevelThree.LevelFour); + var result = type.GetSchemaTypeId(); + Assert.Equal("com.IvanMurzak.ReflectorNet.OuterAssembly.Model.LevelOne%2BLevelTwo%2BLevelThree%2BLevelFour", result); + _output.WriteLine($"LevelOne.LevelTwo.LevelThree.LevelFour -> {result}"); + } + + [Fact] + public void GetSchemaTypeId_GenericNestedClass_ShouldEncodePlusAndAngleBrackets() + { + var type = typeof(GenericOuter.GenericInner); + var result = type.GetSchemaTypeId(); + Assert.Equal("com.IvanMurzak.ReflectorNet.OuterAssembly.Model.GenericOuter%3CSystem.Int32%3E%2BGenericInner%3CSystem.String%3E", result); + _output.WriteLine($"GenericOuter.GenericInner -> {result}"); + } + [Fact] public void GetSchemaTypeId_SimpleArray_ShouldAppendArray() {