From ec6d63f3ce1666175e55f762b41f42dc7b6f6d9c Mon Sep 17 00:00:00 2001 From: Ivan Murzak Date: Wed, 15 Oct 2025 23:16:04 -0700 Subject: [PATCH 01/24] feat: enhance JSON schema generation with $defs support for complex types and add 'result' property --- .../SchemaTests/ReturnSchemaTests.cs | 216 +++++++++++++++--- ReflectorNet/ReflectorNet.csproj | 2 +- .../src/Utils/Json/JsonSchema.Consts.cs | 1 + ReflectorNet/src/Utils/Json/JsonSchema.cs | 141 ++++++++---- 4 files changed, 272 insertions(+), 88 deletions(-) diff --git a/ReflectorNet.Tests/SchemaTests/ReturnSchemaTests.cs b/ReflectorNet.Tests/SchemaTests/ReturnSchemaTests.cs index 0e36055f..edeb5b66 100644 --- a/ReflectorNet.Tests/SchemaTests/ReturnSchemaTests.cs +++ b/ReflectorNet.Tests/SchemaTests/ReturnSchemaTests.cs @@ -134,7 +134,17 @@ public void GetReturnSchema_StringMethod_ReturnsStringSchema() // Assert Assert.NotNull(schema); - Assert.Equal(JsonSchema.String, schema[JsonSchema.Type]?.ToString()); + Assert.Equal(JsonSchema.Object, schema[JsonSchema.Type]?.ToString()); + Assert.True(schema.AsObject().ContainsKey(JsonSchema.Properties)); + Assert.True(schema.AsObject().ContainsKey(JsonSchema.Required)); + + var properties = schema[JsonSchema.Properties]!.AsObject(); + Assert.True(properties.ContainsKey(JsonSchema.Result)); + Assert.Equal(JsonSchema.String, properties[JsonSchema.Result]![JsonSchema.Type]?.ToString()); + + var required = schema[JsonSchema.Required]!.AsArray(); + Assert.Single(required); + Assert.Equal(JsonSchema.Result, required[0]?.ToString()); } [Fact] @@ -149,7 +159,10 @@ public void GetReturnSchema_IntMethod_ReturnsIntegerSchema() // Assert Assert.NotNull(schema); - Assert.Equal(JsonSchema.Integer, schema[JsonSchema.Type]?.ToString()); + Assert.Equal(JsonSchema.Object, schema[JsonSchema.Type]?.ToString()); + var properties = schema[JsonSchema.Properties]!.AsObject(); + Assert.True(properties.ContainsKey(JsonSchema.Result)); + Assert.Equal(JsonSchema.Integer, properties[JsonSchema.Result]![JsonSchema.Type]?.ToString()); } [Fact] @@ -164,7 +177,10 @@ public void GetReturnSchema_BoolMethod_ReturnsBooleanSchema() // Assert Assert.NotNull(schema); - Assert.Equal(JsonSchema.Boolean, schema[JsonSchema.Type]?.ToString()); + Assert.Equal(JsonSchema.Object, schema[JsonSchema.Type]?.ToString()); + var properties = schema[JsonSchema.Properties]!.AsObject(); + Assert.True(properties.ContainsKey(JsonSchema.Result)); + Assert.Equal(JsonSchema.Boolean, properties[JsonSchema.Result]![JsonSchema.Type]?.ToString()); } [Fact] @@ -179,7 +195,10 @@ public void GetReturnSchema_DoubleMethod_ReturnsNumberSchema() // Assert Assert.NotNull(schema); - Assert.Equal(JsonSchema.Number, schema[JsonSchema.Type]?.ToString()); + Assert.Equal(JsonSchema.Object, schema[JsonSchema.Type]?.ToString()); + var properties = schema[JsonSchema.Properties]!.AsObject(); + Assert.True(properties.ContainsKey(JsonSchema.Result)); + Assert.Equal(JsonSchema.Number, properties[JsonSchema.Result]![JsonSchema.Type]?.ToString()); } #endregion @@ -198,10 +217,12 @@ public void GetReturnSchema_TaskString_UnwrapsToStringSchema() // Assert Assert.NotNull(schema); - Assert.Equal(JsonSchema.String, schema[JsonSchema.Type]?.ToString()); + Assert.Equal(JsonSchema.Object, schema[JsonSchema.Type]?.ToString()); + Assert.True(schema.AsObject().ContainsKey(JsonSchema.Properties)); - // Verify it's not a schema for Task, but for string - Assert.False(schema.AsObject().ContainsKey(JsonSchema.Properties)); + var properties = schema[JsonSchema.Properties]!.AsObject(); + Assert.True(properties.ContainsKey(JsonSchema.Result)); + Assert.Equal(JsonSchema.String, properties[JsonSchema.Result]![JsonSchema.Type]?.ToString()); } [Fact] @@ -216,7 +237,10 @@ public void GetReturnSchema_TaskInt_UnwrapsToIntegerSchema() // Assert Assert.NotNull(schema); - Assert.Equal(JsonSchema.Integer, schema[JsonSchema.Type]?.ToString()); + Assert.Equal(JsonSchema.Object, schema[JsonSchema.Type]?.ToString()); + var properties = schema[JsonSchema.Properties]!.AsObject(); + Assert.True(properties.ContainsKey(JsonSchema.Result)); + Assert.Equal(JsonSchema.Integer, properties[JsonSchema.Result]![JsonSchema.Type]?.ToString()); } [Fact] @@ -231,7 +255,10 @@ public void GetReturnSchema_TaskBool_UnwrapsToBooleanSchema() // Assert Assert.NotNull(schema); - Assert.Equal(JsonSchema.Boolean, schema[JsonSchema.Type]?.ToString()); + Assert.Equal(JsonSchema.Object, schema[JsonSchema.Type]?.ToString()); + var properties = schema[JsonSchema.Properties]!.AsObject(); + Assert.True(properties.ContainsKey(JsonSchema.Result)); + Assert.Equal(JsonSchema.Boolean, properties[JsonSchema.Result]![JsonSchema.Type]?.ToString()); } [Fact] @@ -250,8 +277,24 @@ public void GetReturnSchema_TaskCustomType_UnwrapsToCustomTypeSchema() Assert.True(schema.AsObject().ContainsKey(JsonSchema.Properties)); var properties = schema[JsonSchema.Properties]!.AsObject(); - Assert.True(properties.ContainsKey("Name")); - Assert.True(properties.ContainsKey("Value")); + Assert.True(properties.ContainsKey(JsonSchema.Result)); + + // The result property should contain a $ref to the CustomReturnType in $defs + var resultSchema = properties[JsonSchema.Result]!.AsObject(); + Assert.True(resultSchema.ContainsKey(JsonSchema.Ref) || resultSchema.ContainsKey(JsonSchema.Properties)); + + // If it's a ref, verify $defs exists + if (resultSchema.ContainsKey(JsonSchema.Ref)) + { + Assert.True(schema.AsObject().ContainsKey(JsonSchema.Defs)); + } + // If it's not a ref, verify it has the expected properties + else + { + var nestedProperties = resultSchema[JsonSchema.Properties]!.AsObject(); + Assert.True(nestedProperties.ContainsKey("Name")); + Assert.True(nestedProperties.ContainsKey("Value")); + } } #endregion @@ -270,7 +313,10 @@ public void GetReturnSchema_ValueTaskString_UnwrapsToStringSchema() // Assert Assert.NotNull(schema); - Assert.Equal(JsonSchema.String, schema[JsonSchema.Type]?.ToString()); + Assert.Equal(JsonSchema.Object, schema[JsonSchema.Type]?.ToString()); + var properties = schema[JsonSchema.Properties]!.AsObject(); + Assert.True(properties.ContainsKey(JsonSchema.Result)); + Assert.Equal(JsonSchema.String, properties[JsonSchema.Result]![JsonSchema.Type]?.ToString()); } [Fact] @@ -285,7 +331,10 @@ public void GetReturnSchema_ValueTaskInt_UnwrapsToIntegerSchema() // Assert Assert.NotNull(schema); - Assert.Equal(JsonSchema.Integer, schema[JsonSchema.Type]?.ToString()); + Assert.Equal(JsonSchema.Object, schema[JsonSchema.Type]?.ToString()); + var properties = schema[JsonSchema.Properties]!.AsObject(); + Assert.True(properties.ContainsKey(JsonSchema.Result)); + Assert.Equal(JsonSchema.Integer, properties[JsonSchema.Result]![JsonSchema.Type]?.ToString()); } [Fact] @@ -304,8 +353,11 @@ public void GetReturnSchema_ValueTaskCustomType_UnwrapsToCustomTypeSchema() Assert.True(schema.AsObject().ContainsKey(JsonSchema.Properties)); var properties = schema[JsonSchema.Properties]!.AsObject(); - Assert.True(properties.ContainsKey("Name")); - Assert.True(properties.ContainsKey("Value")); + Assert.True(properties.ContainsKey(JsonSchema.Result)); + + // The result property should contain a $ref to the CustomReturnType in $defs + var resultSchema = properties[JsonSchema.Result]!.AsObject(); + Assert.True(resultSchema.ContainsKey(JsonSchema.Ref) || resultSchema.ContainsKey(JsonSchema.Properties)); } #endregion @@ -328,12 +380,25 @@ public void GetReturnSchema_CustomType_ReturnsValidObjectSchema() Assert.True(schema.AsObject().ContainsKey(JsonSchema.Properties)); var properties = schema[JsonSchema.Properties]!.AsObject(); - Assert.True(properties.ContainsKey("Name")); - Assert.True(properties.ContainsKey("Value")); - - // Verify property types - Assert.Equal(JsonSchema.String, properties["Name"]![JsonSchema.Type]?.ToString()); - Assert.Equal(JsonSchema.Integer, properties["Value"]![JsonSchema.Type]?.ToString()); + Assert.True(properties.ContainsKey(JsonSchema.Result)); + + // The result property should contain the CustomReturnType schema (either inline or ref) + var resultSchema = properties[JsonSchema.Result]!.AsObject(); + + // It could be a $ref or an inline schema + if (resultSchema.ContainsKey(JsonSchema.Ref)) + { + // If it's a ref, $defs should exist with the type definition + Assert.True(schema.AsObject().ContainsKey(JsonSchema.Defs)); + } + else + { + // If it's inline, verify the properties + Assert.True(resultSchema.ContainsKey(JsonSchema.Properties)); + var customTypeProperties = resultSchema[JsonSchema.Properties]!.AsObject(); + Assert.True(customTypeProperties.ContainsKey("Name")); + Assert.True(customTypeProperties.ContainsKey("Value")); + } } [Fact] @@ -352,10 +417,25 @@ public void GetReturnSchema_ComplexType_ReturnsValidNestedSchema() Assert.True(schema.AsObject().ContainsKey(JsonSchema.Properties)); var properties = schema[JsonSchema.Properties]!.AsObject(); - Assert.True(properties.ContainsKey("StringProperty")); - Assert.True(properties.ContainsKey("IntProperty")); - Assert.True(properties.ContainsKey("NestedObject")); - Assert.True(properties.ContainsKey("StringArray")); + Assert.True(properties.ContainsKey(JsonSchema.Result)); + + // The result property should contain the ComplexReturnType schema + var resultSchema = properties[JsonSchema.Result]!.AsObject(); + + // It could be a $ref or an inline schema + if (resultSchema.ContainsKey(JsonSchema.Ref)) + { + Assert.True(schema.AsObject().ContainsKey(JsonSchema.Defs)); + } + else + { + Assert.True(resultSchema.ContainsKey(JsonSchema.Properties)); + var complexTypeProperties = resultSchema[JsonSchema.Properties]!.AsObject(); + Assert.True(complexTypeProperties.ContainsKey("StringProperty")); + Assert.True(complexTypeProperties.ContainsKey("IntProperty")); + Assert.True(complexTypeProperties.ContainsKey("NestedObject")); + Assert.True(complexTypeProperties.ContainsKey("StringArray")); + } } #endregion @@ -374,11 +454,33 @@ public void GetReturnSchema_StringArray_ReturnsArraySchema() // Assert Assert.NotNull(schema); - Assert.Equal(JsonSchema.Array, schema[JsonSchema.Type]?.ToString()); - Assert.True(schema.AsObject().ContainsKey(JsonSchema.Items)); + Assert.Equal(JsonSchema.Object, schema[JsonSchema.Type]?.ToString()); + Assert.True(schema.AsObject().ContainsKey(JsonSchema.Properties)); - var items = schema[JsonSchema.Items]!; - Assert.Equal(JsonSchema.String, items[JsonSchema.Type]?.ToString()); + var properties = schema[JsonSchema.Properties]!.AsObject(); + Assert.True(properties.ContainsKey(JsonSchema.Result)); + + // The result property should be an array schema (either inline or ref) + var resultNode = properties[JsonSchema.Result]!; + + if (resultNode is JsonObject resultSchema && resultSchema.ContainsKey(JsonSchema.Ref)) + { + // If it's a ref, verify $defs contains the array schema + Assert.True(schema.AsObject().ContainsKey(JsonSchema.Defs)); + } + else if (resultNode is JsonObject resultInlineSchema) + { + // If it's inline, verify it's an array schema + Assert.Equal(JsonSchema.Array, resultInlineSchema[JsonSchema.Type]?.ToString()); + Assert.True(resultInlineSchema.ContainsKey(JsonSchema.Items)); + + var items = resultInlineSchema[JsonSchema.Items]!; + Assert.Equal(JsonSchema.String, items[JsonSchema.Type]?.ToString()); + } + else + { + Assert.Fail("Expected result to be a schema object"); + } } [Fact] @@ -393,11 +495,33 @@ public void GetReturnSchema_ListInt_ReturnsArraySchema() // Assert Assert.NotNull(schema); - Assert.Equal(JsonSchema.Array, schema[JsonSchema.Type]?.ToString()); - Assert.True(schema.AsObject().ContainsKey(JsonSchema.Items)); + Assert.Equal(JsonSchema.Object, schema[JsonSchema.Type]?.ToString()); + Assert.True(schema.AsObject().ContainsKey(JsonSchema.Properties)); - var items = schema[JsonSchema.Items]!; - Assert.Equal(JsonSchema.Integer, items[JsonSchema.Type]?.ToString()); + var properties = schema[JsonSchema.Properties]!.AsObject(); + Assert.True(properties.ContainsKey(JsonSchema.Result)); + + // The result property should be an array schema (either inline or ref) + var resultNode = properties[JsonSchema.Result]!; + + if (resultNode is JsonObject resultSchema && resultSchema.ContainsKey(JsonSchema.Ref)) + { + // If it's a ref, verify $defs contains the array schema + Assert.True(schema.AsObject().ContainsKey(JsonSchema.Defs)); + } + else if (resultNode is JsonObject resultInlineSchema) + { + // If it's inline, verify it's an array schema + Assert.Equal(JsonSchema.Array, resultInlineSchema[JsonSchema.Type]?.ToString()); + Assert.True(resultInlineSchema.ContainsKey(JsonSchema.Items)); + + var items = resultInlineSchema[JsonSchema.Items]!; + Assert.Equal(JsonSchema.Integer, items[JsonSchema.Type]?.ToString()); + } + else + { + Assert.Fail("Expected result to be a schema object"); + } } #endregion @@ -416,8 +540,19 @@ public void GetReturnSchema_CustomType_WithJustRef_ReturnsRefSchema() // Assert Assert.NotNull(schema); - Assert.True(schema.AsObject().ContainsKey(JsonSchema.Ref)); - Assert.Contains("CustomReturnType", schema[JsonSchema.Ref]?.ToString()); + 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(JsonSchema.Result)); + + // The result property should contain a $ref to CustomReturnType + var resultSchema = properties[JsonSchema.Result]!.AsObject(); + Assert.True(resultSchema.ContainsKey(JsonSchema.Ref)); + Assert.Contains("CustomReturnType", resultSchema[JsonSchema.Ref]?.ToString()); + + // $defs should exist with the full type definition + Assert.True(schema.AsObject().ContainsKey(JsonSchema.Defs)); } [Fact] @@ -432,9 +567,16 @@ public void GetReturnSchema_PrimitiveType_WithJustRef_ReturnsFullSchema() // Assert 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(JsonSchema.Result)); + // Primitive types should be inlined even with justRef=true - Assert.Equal(JsonSchema.String, schema[JsonSchema.Type]?.ToString()); - Assert.False(schema.AsObject().ContainsKey(JsonSchema.Ref)); + var resultSchema = properties[JsonSchema.Result]!.AsObject(); + Assert.Equal(JsonSchema.String, resultSchema[JsonSchema.Type]?.ToString()); + Assert.False(resultSchema.ContainsKey(JsonSchema.Ref)); } #endregion diff --git a/ReflectorNet/ReflectorNet.csproj b/ReflectorNet/ReflectorNet.csproj index caa87a30..2948a2ec 100644 --- a/ReflectorNet/ReflectorNet.csproj +++ b/ReflectorNet/ReflectorNet.csproj @@ -9,7 +9,7 @@ com.IvanMurzak.ReflectorNet - 2.1.0 + 2.1.1 Ivan Murzak Copyright © Ivan Murzak 2025 ReflectorNet is an advanced .NET reflection toolkit designed for AI-driven scenarios. Effortlessly search for C# methods using natural language queries, invoke any method by supplying arguments as JSON, and receive results as JSON. The library also provides a powerful API to inspect, modify, and manage in-memory object instances dynamically via JSON data. Ideal for automation, testing, and AI integration workflows. diff --git a/ReflectorNet/src/Utils/Json/JsonSchema.Consts.cs b/ReflectorNet/src/Utils/Json/JsonSchema.Consts.cs index f45a0b32..54ff73d9 100644 --- a/ReflectorNet/src/Utils/Json/JsonSchema.Consts.cs +++ b/ReflectorNet/src/Utils/Json/JsonSchema.Consts.cs @@ -17,6 +17,7 @@ public partial class JsonSchema public const string Array = "array"; public const string AnyOf = "anyOf"; public const string Required = "required"; + public const string Result = "result"; public const string Error = "error"; public const string AdditionalProperties = "additionalProperties"; diff --git a/ReflectorNet/src/Utils/Json/JsonSchema.cs b/ReflectorNet/src/Utils/Json/JsonSchema.cs index fcc250a1..bd0ad86f 100644 --- a/ReflectorNet/src/Utils/Json/JsonSchema.cs +++ b/ReflectorNet/src/Utils/Json/JsonSchema.cs @@ -168,6 +168,57 @@ public JsonNode GetSchema(Reflector reflector, Type type, bool justRef = false) } return schema; } + /// + /// Generates a JSON Schema with proper $defs handling for complex types. + /// This method handles type deduplication by placing complex type definitions in the $defs section + /// and using $ref references to avoid schema repetition. + /// + /// The Reflector instance used for type analysis and schema generation. + /// The Type for which to generate the schema. + /// Whether to use compact references for complex types. Default is false. + /// Optional JsonObject to accumulate type definitions. If null, a new object is created. + /// A tuple containing the generated schema and the definitions object. + private (JsonNode schema, JsonObject? defines) GenerateSchemaWithDefs(Reflector reflector, Type type, bool justRef = false, JsonObject? defines = null) + { + var isPrimitive = TypeUtils.IsPrimitive(type); + JsonNode schema; + + if (isPrimitive) + { + schema = GetSchema(reflector, type, justRef: justRef); + } + else + { + schema = GetSchema(reflector, type, justRef: true); + var typeId = type.GetTypeId(); + + defines ??= new JsonObject(); + + if (!defines.ContainsKey(typeId)) + { + var fullSchema = GetSchema(reflector, type, justRef: false); + defines[typeId] = fullSchema; + } + + // Add generic type parameters recursively if any + foreach (var genericArgument in TypeUtils.GetGenericTypes(type)) + { + if (TypeUtils.IsPrimitive(genericArgument)) + continue; + + var genericTypeId = genericArgument.GetTypeId(); + if (defines.ContainsKey(genericTypeId)) + continue; + + var genericSchema = GetSchema(reflector, genericArgument, justRef: false); + if (genericSchema != null) + defines[genericTypeId] = genericSchema; + } + } + + return (schema, defines); + } + /// /// Generates a comprehensive JSON Schema for method parameters, creating schemas suitable for /// dynamic method invocation, API documentation, form generation, and parameter validation. @@ -225,39 +276,16 @@ public JsonNode GetArgumentsSchema(Reflector reflector, MethodInfo method, bool // [SchemaDraft] = JsonValue.Create(SchemaDraftValue), [Type] = Object, [Properties] = properties, - [Required] = required, - [Defs] = defines + [Required] = required }; foreach (var parameter in parameters) { - var parameterSchema = default(JsonNode); - var isPrimitive = TypeUtils.IsPrimitive(parameter.ParameterType); - if (isPrimitive) - { - parameterSchema = GetSchema(reflector, parameter.ParameterType, justRef: justRef); - if (parameterSchema == null) - continue; - } - else - { - var typeId = parameter.ParameterType.GetTypeId(); - if (defines.ContainsKey(typeId)) - { - parameterSchema = GetSchema(reflector, parameter.ParameterType, justRef: true); - } - else - { - var fullSchema = GetSchema(reflector, parameter.ParameterType, justRef: false); - if (fullSchema == null) - continue; - defines[typeId] = fullSchema; - parameterSchema = GetSchema(reflector, parameter.ParameterType, justRef: true); - } + var (parameterSchema, updatedDefines) = GenerateSchemaWithDefs(reflector, parameter.ParameterType, justRef, defines); + defines = updatedDefines; - if (parameterSchema == null) - continue; - } + if (parameterSchema == null) + continue; properties[parameter.Name!] = parameterSchema; @@ -271,25 +299,10 @@ public JsonNode GetArgumentsSchema(Reflector reflector, MethodInfo method, bool // Check if the parameter has a default value if (!parameter.HasDefaultValue) required.Add(parameter.Name!); - - // Add generic type parameters recursively if any - foreach (var genericArgument in TypeUtils.GetGenericTypes(parameter.ParameterType)) - { - if (TypeUtils.IsPrimitive(genericArgument)) - continue; - - var typeId = genericArgument.GetTypeId(); - if (defines.ContainsKey(typeId)) - continue; - - var genericSchema = GetSchema(reflector, genericArgument, justRef: false); - if (genericSchema != null) - defines[typeId] = genericSchema; - } } - if (defines.Count == 0) - schema.Remove(Defs); + if (defines != null && defines.Count > 0) + schema[Defs] = defines; return schema; } @@ -298,20 +311,28 @@ public JsonNode GetArgumentsSchema(Reflector reflector, MethodInfo method, bool /// This method analyzes method return types and produces schemas that describe the structure /// of the returned data, enabling type-safe result handling in dynamic execution environments. /// + /// Schema Structure: + /// - Root object schema with "properties" containing the "result" property + /// - "required" array listing the "result" property (for non-void returns) + /// - "$defs" section containing complex type definitions to avoid duplication + /// - Return type wrapped as a property named "result" + /// - Proper JSON Schema Draft 2020-12 compliance + /// /// Schema Generation Features: /// - Async unwrapping: Automatically unwraps Task<T> and ValueTask<T> to return schema for T /// - Void handling: Returns null for void, Task, and ValueTask (methods with no return value) /// - Type resolution: Generates appropriate schemas for complex return types /// - Reference optimization: Supports both full schema definitions and compact $ref references /// - Documentation: Extracts descriptions from return type attributes + /// - Proper $defs handling: Complex types are placed in $defs section with $ref references /// /// Return Type Handling: /// - void → null (no return value) /// - Task → null (async method with no return value) /// - ValueTask → null (async method with no return value) - /// - Task<string> → schema for string (unwrapped) - /// - ValueTask<int> → schema for int (unwrapped) - /// - Any other type → schema for that type + /// - Task<string> → object schema with "result" property of type string (unwrapped) + /// - ValueTask<int> → object schema with "result" property of type int (unwrapped) + /// - Any other type → object schema with "result" property of that type with $defs support /// /// Use Cases: /// - API documentation generation for response schemas @@ -330,7 +351,10 @@ public JsonNode GetArgumentsSchema(Reflector reflector, MethodInfo method, bool throw new ArgumentNullException(nameof(methodInfo)); var returnType = methodInfo.ReturnType; - var unwrappedType = Nullable.GetUnderlyingType(returnType) ?? returnType; + var unwrappedType = Nullable.GetUnderlyingType(returnType); + var isNullable = unwrappedType != null; + + unwrappedType ??= returnType; // Handle void, Task, and ValueTask - these have no return value if (unwrappedType == typeof(void) || @@ -346,7 +370,24 @@ public JsonNode GetArgumentsSchema(Reflector reflector, MethodInfo method, bool unwrappedType = unwrappedType.GetGenericArguments()[0]; } - return GetSchema(reflector, unwrappedType, justRef); + // Generate schema for the return type using the shared method + var (resultSchema, defines) = GenerateSchemaWithDefs(reflector, unwrappedType, justRef); + + // Create the root schema object in the same format as GetArgumentsSchema + var schema = new JsonObject { [Type] = Object }; + + if (resultSchema != null) + { + schema[Properties] = new JsonObject { [Result] = resultSchema }; + + if (!isNullable) + schema[Required] = new JsonArray { Result }; + } + + if (defines != null && defines.Count > 0) + schema[Defs] = defines; + + return schema; } } } \ No newline at end of file From c1736e9311d57add62a59a6e18de978df90e5459 Mon Sep 17 00:00:00 2001 From: Ivan Murzak Date: Wed, 15 Oct 2025 23:28:15 -0700 Subject: [PATCH 02/24] feat: enhance nullability checks for Task and ValueTask return types in JSON schema generation --- .../SchemaTests/ReturnSchemaTests.cs | 520 ++++++++++++++++++ ReflectorNet/src/Utils/Json/JsonSchema.cs | 52 +- 2 files changed, 571 insertions(+), 1 deletion(-) diff --git a/ReflectorNet.Tests/SchemaTests/ReturnSchemaTests.cs b/ReflectorNet.Tests/SchemaTests/ReturnSchemaTests.cs index edeb5b66..76839cd0 100644 --- a/ReflectorNet.Tests/SchemaTests/ReturnSchemaTests.cs +++ b/ReflectorNet.Tests/SchemaTests/ReturnSchemaTests.cs @@ -54,6 +54,27 @@ private void VoidMethod() { } private System.Collections.Generic.List ListIntMethod() => new System.Collections.Generic.List(); private System.Collections.Generic.Dictionary DictionaryMethod() => new System.Collections.Generic.Dictionary(); + // Nullable return types + private string? NullableStringMethod() => "test"; + private int? NullableIntMethod() => 42; + private bool? NullableBoolMethod() => true; + private double? NullableDoubleMethod() => 3.14; + private DateTime? NullableDateTimeMethod() => DateTime.Now; + private CustomReturnType? NullableCustomTypeMethod() => new CustomReturnType(); + private ComplexReturnType? NullableComplexTypeMethod() => new ComplexReturnType(); + private string[]? NullableStringArrayMethod() => new[] { "test" }; + + // Task return types (nullable wrapped in Task) + private Task TaskNullableStringMethod() => Task.FromResult("test"); + private Task TaskNullableIntMethod() => Task.FromResult(42); + private Task TaskNullableBoolMethod() => Task.FromResult(true); + private Task TaskNullableCustomTypeMethod() => Task.FromResult(new CustomReturnType()); + + // ValueTask return types (nullable wrapped in ValueTask) + private ValueTask ValueTaskNullableStringMethod() => ValueTask.FromResult("test"); + private ValueTask ValueTaskNullableIntMethod() => ValueTask.FromResult(42); + private ValueTask ValueTaskNullableCustomTypeMethod() => ValueTask.FromResult(new CustomReturnType()); + #endregion #region Test Classes @@ -203,6 +224,138 @@ public void GetReturnSchema_DoubleMethod_ReturnsNumberSchema() #endregion + #region Nullable Primitive Return Type Tests + + [Fact] + public void GetReturnSchema_NullableStringMethod_ReturnsStringSchemaWithoutRequired() + { + // Arrange + var reflector = new Reflector(); + var methodInfo = GetType().GetMethod(nameof(NullableStringMethod), BindingFlags.NonPublic | BindingFlags.Instance)!; + + // Act + var schema = reflector.GetReturnSchema(methodInfo); + + // Assert + 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(JsonSchema.Result)); + Assert.Equal(JsonSchema.String, properties[JsonSchema.Result]![JsonSchema.Type]?.ToString()); + + // Verify that "result" is NOT in the required array for nullable return types + if (schema.AsObject().ContainsKey(JsonSchema.Required)) + { + var required = schema[JsonSchema.Required]!.AsArray(); + Assert.DoesNotContain(required, r => r?.ToString() == JsonSchema.Result); + } + } + + [Fact] + public void GetReturnSchema_NullableIntMethod_ReturnsIntegerSchemaWithoutRequired() + { + // Arrange + var reflector = new Reflector(); + var methodInfo = GetType().GetMethod(nameof(NullableIntMethod), BindingFlags.NonPublic | BindingFlags.Instance)!; + + // Act + var schema = reflector.GetReturnSchema(methodInfo); + + // Assert + Assert.NotNull(schema); + Assert.Equal(JsonSchema.Object, schema[JsonSchema.Type]?.ToString()); + var properties = schema[JsonSchema.Properties]!.AsObject(); + Assert.True(properties.ContainsKey(JsonSchema.Result)); + Assert.Equal(JsonSchema.Integer, properties[JsonSchema.Result]![JsonSchema.Type]?.ToString()); + + // Verify that "result" is NOT in the required array for nullable return types + if (schema.AsObject().ContainsKey(JsonSchema.Required)) + { + var required = schema[JsonSchema.Required]!.AsArray(); + Assert.DoesNotContain(required, r => r?.ToString() == JsonSchema.Result); + } + } + + [Fact] + public void GetReturnSchema_NullableBoolMethod_ReturnsBooleanSchemaWithoutRequired() + { + // Arrange + var reflector = new Reflector(); + var methodInfo = GetType().GetMethod(nameof(NullableBoolMethod), BindingFlags.NonPublic | BindingFlags.Instance)!; + + // Act + var schema = reflector.GetReturnSchema(methodInfo); + + // Assert + Assert.NotNull(schema); + Assert.Equal(JsonSchema.Object, schema[JsonSchema.Type]?.ToString()); + var properties = schema[JsonSchema.Properties]!.AsObject(); + Assert.True(properties.ContainsKey(JsonSchema.Result)); + Assert.Equal(JsonSchema.Boolean, properties[JsonSchema.Result]![JsonSchema.Type]?.ToString()); + + // Verify that "result" is NOT in the required array for nullable return types + if (schema.AsObject().ContainsKey(JsonSchema.Required)) + { + var required = schema[JsonSchema.Required]!.AsArray(); + Assert.DoesNotContain(required, r => r?.ToString() == JsonSchema.Result); + } + } + + [Fact] + public void GetReturnSchema_NullableDoubleMethod_ReturnsNumberSchemaWithoutRequired() + { + // Arrange + var reflector = new Reflector(); + var methodInfo = GetType().GetMethod(nameof(NullableDoubleMethod), BindingFlags.NonPublic | BindingFlags.Instance)!; + + // Act + var schema = reflector.GetReturnSchema(methodInfo); + + // Assert + Assert.NotNull(schema); + Assert.Equal(JsonSchema.Object, schema[JsonSchema.Type]?.ToString()); + var properties = schema[JsonSchema.Properties]!.AsObject(); + Assert.True(properties.ContainsKey(JsonSchema.Result)); + Assert.Equal(JsonSchema.Number, properties[JsonSchema.Result]![JsonSchema.Type]?.ToString()); + + // Verify that "result" is NOT in the required array for nullable return types + if (schema.AsObject().ContainsKey(JsonSchema.Required)) + { + var required = schema[JsonSchema.Required]!.AsArray(); + Assert.DoesNotContain(required, r => r?.ToString() == JsonSchema.Result); + } + } + + [Fact] + public void GetReturnSchema_NullableDateTimeMethod_ReturnsStringSchemaWithoutRequired() + { + // Arrange + var reflector = new Reflector(); + var methodInfo = GetType().GetMethod(nameof(NullableDateTimeMethod), BindingFlags.NonPublic | BindingFlags.Instance)!; + + // Act + var schema = reflector.GetReturnSchema(methodInfo); + + // Assert + Assert.NotNull(schema); + Assert.Equal(JsonSchema.Object, schema[JsonSchema.Type]?.ToString()); + var properties = schema[JsonSchema.Properties]!.AsObject(); + Assert.True(properties.ContainsKey(JsonSchema.Result)); + // DateTime is typically serialized as string in JSON + Assert.Equal(JsonSchema.String, properties[JsonSchema.Result]![JsonSchema.Type]?.ToString()); + + // Verify that "result" is NOT in the required array for nullable return types + if (schema.AsObject().ContainsKey(JsonSchema.Required)) + { + var required = schema[JsonSchema.Required]!.AsArray(); + Assert.DoesNotContain(required, r => r?.ToString() == JsonSchema.Result); + } + } + + #endregion + #region Task Unwrapping Tests [Fact] @@ -299,6 +452,130 @@ public void GetReturnSchema_TaskCustomType_UnwrapsToCustomTypeSchema() #endregion + #region Task Nullable Unwrapping Tests + + [Fact] + public void GetReturnSchema_TaskNullableString_UnwrapsToStringSchemaWithoutRequired() + { + // Arrange + var reflector = new Reflector(); + var methodInfo = GetType().GetMethod(nameof(TaskNullableStringMethod), BindingFlags.NonPublic | BindingFlags.Instance)!; + + // Act + var schema = reflector.GetReturnSchema(methodInfo); + + // Assert + 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(JsonSchema.Result)); + Assert.Equal(JsonSchema.String, properties[JsonSchema.Result]![JsonSchema.Type]?.ToString()); + + // Verify that "result" is NOT in the required array for nullable return types + if (schema.AsObject().ContainsKey(JsonSchema.Required)) + { + var required = schema[JsonSchema.Required]!.AsArray(); + Assert.DoesNotContain(required, r => r?.ToString() == JsonSchema.Result); + } + } + + [Fact] + public void GetReturnSchema_TaskNullableInt_UnwrapsToIntegerSchemaWithoutRequired() + { + // Arrange + var reflector = new Reflector(); + var methodInfo = GetType().GetMethod(nameof(TaskNullableIntMethod), BindingFlags.NonPublic | BindingFlags.Instance)!; + + // Act + var schema = reflector.GetReturnSchema(methodInfo); + + // Assert + Assert.NotNull(schema); + Assert.Equal(JsonSchema.Object, schema[JsonSchema.Type]?.ToString()); + var properties = schema[JsonSchema.Properties]!.AsObject(); + Assert.True(properties.ContainsKey(JsonSchema.Result)); + Assert.Equal(JsonSchema.Integer, properties[JsonSchema.Result]![JsonSchema.Type]?.ToString()); + + // Verify that "result" is NOT in the required array for nullable return types + if (schema.AsObject().ContainsKey(JsonSchema.Required)) + { + var required = schema[JsonSchema.Required]!.AsArray(); + Assert.DoesNotContain(required, r => r?.ToString() == JsonSchema.Result); + } + } + + [Fact] + public void GetReturnSchema_TaskNullableBool_UnwrapsToBooleanSchemaWithoutRequired() + { + // Arrange + var reflector = new Reflector(); + var methodInfo = GetType().GetMethod(nameof(TaskNullableBoolMethod), BindingFlags.NonPublic | BindingFlags.Instance)!; + + // Act + var schema = reflector.GetReturnSchema(methodInfo); + + // Assert + Assert.NotNull(schema); + Assert.Equal(JsonSchema.Object, schema[JsonSchema.Type]?.ToString()); + var properties = schema[JsonSchema.Properties]!.AsObject(); + Assert.True(properties.ContainsKey(JsonSchema.Result)); + Assert.Equal(JsonSchema.Boolean, properties[JsonSchema.Result]![JsonSchema.Type]?.ToString()); + + // Verify that "result" is NOT in the required array for nullable return types + if (schema.AsObject().ContainsKey(JsonSchema.Required)) + { + var required = schema[JsonSchema.Required]!.AsArray(); + Assert.DoesNotContain(required, r => r?.ToString() == JsonSchema.Result); + } + } + + [Fact] + public void GetReturnSchema_TaskNullableCustomType_UnwrapsToCustomTypeSchemaWithoutRequired() + { + // Arrange + var reflector = new Reflector(); + var methodInfo = GetType().GetMethod(nameof(TaskNullableCustomTypeMethod), BindingFlags.NonPublic | BindingFlags.Instance)!; + + // Act + var schema = reflector.GetReturnSchema(methodInfo); + + // Assert + 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(JsonSchema.Result)); + + // The result property should contain a $ref to the CustomReturnType in $defs + var resultSchema = properties[JsonSchema.Result]!.AsObject(); + Assert.True(resultSchema.ContainsKey(JsonSchema.Ref) || resultSchema.ContainsKey(JsonSchema.Properties)); + + // If it's a ref, verify $defs exists + if (resultSchema.ContainsKey(JsonSchema.Ref)) + { + Assert.True(schema.AsObject().ContainsKey(JsonSchema.Defs)); + } + // If it's not a ref, verify it has the expected properties + else + { + var nestedProperties = resultSchema[JsonSchema.Properties]!.AsObject(); + Assert.True(nestedProperties.ContainsKey("Name")); + Assert.True(nestedProperties.ContainsKey("Value")); + } + + // Verify that "result" is NOT in the required array for nullable return types + if (schema.AsObject().ContainsKey(JsonSchema.Required)) + { + var required = schema[JsonSchema.Required]!.AsArray(); + Assert.DoesNotContain(required, r => r?.ToString() == JsonSchema.Result); + } + } + + #endregion + #region ValueTask Unwrapping Tests [Fact] @@ -362,6 +639,105 @@ public void GetReturnSchema_ValueTaskCustomType_UnwrapsToCustomTypeSchema() #endregion + #region ValueTask Nullable Unwrapping Tests + + [Fact] + public void GetReturnSchema_ValueTaskNullableString_UnwrapsToStringSchemaWithoutRequired() + { + // Arrange + var reflector = new Reflector(); + var methodInfo = GetType().GetMethod(nameof(ValueTaskNullableStringMethod), BindingFlags.NonPublic | BindingFlags.Instance)!; + + // Act + var schema = reflector.GetReturnSchema(methodInfo); + + // Assert + 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(JsonSchema.Result)); + Assert.Equal(JsonSchema.String, properties[JsonSchema.Result]![JsonSchema.Type]?.ToString()); + + // Verify that "result" is NOT in the required array for nullable return types + if (schema.AsObject().ContainsKey(JsonSchema.Required)) + { + var required = schema[JsonSchema.Required]!.AsArray(); + Assert.DoesNotContain(required, r => r?.ToString() == JsonSchema.Result); + } + } + + [Fact] + public void GetReturnSchema_ValueTaskNullableInt_UnwrapsToIntegerSchemaWithoutRequired() + { + // Arrange + var reflector = new Reflector(); + var methodInfo = GetType().GetMethod(nameof(ValueTaskNullableIntMethod), BindingFlags.NonPublic | BindingFlags.Instance)!; + + // Act + var schema = reflector.GetReturnSchema(methodInfo); + + // Assert + Assert.NotNull(schema); + Assert.Equal(JsonSchema.Object, schema[JsonSchema.Type]?.ToString()); + var properties = schema[JsonSchema.Properties]!.AsObject(); + Assert.True(properties.ContainsKey(JsonSchema.Result)); + Assert.Equal(JsonSchema.Integer, properties[JsonSchema.Result]![JsonSchema.Type]?.ToString()); + + // Verify that "result" is NOT in the required array for nullable return types + if (schema.AsObject().ContainsKey(JsonSchema.Required)) + { + var required = schema[JsonSchema.Required]!.AsArray(); + Assert.DoesNotContain(required, r => r?.ToString() == JsonSchema.Result); + } + } + + [Fact] + public void GetReturnSchema_ValueTaskNullableCustomType_UnwrapsToCustomTypeSchemaWithoutRequired() + { + // Arrange + var reflector = new Reflector(); + var methodInfo = GetType().GetMethod(nameof(ValueTaskNullableCustomTypeMethod), BindingFlags.NonPublic | BindingFlags.Instance)!; + + // Act + var schema = reflector.GetReturnSchema(methodInfo); + + // Assert + 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(JsonSchema.Result)); + + // The result property should contain a $ref to the CustomReturnType in $defs + var resultSchema = properties[JsonSchema.Result]!.AsObject(); + Assert.True(resultSchema.ContainsKey(JsonSchema.Ref) || resultSchema.ContainsKey(JsonSchema.Properties)); + + // If it's a ref, verify $defs exists + if (resultSchema.ContainsKey(JsonSchema.Ref)) + { + Assert.True(schema.AsObject().ContainsKey(JsonSchema.Defs)); + } + // If it's not a ref, verify it has the expected properties + else + { + var nestedProperties = resultSchema[JsonSchema.Properties]!.AsObject(); + Assert.True(nestedProperties.ContainsKey("Name")); + Assert.True(nestedProperties.ContainsKey("Value")); + } + + // Verify that "result" is NOT in the required array for nullable return types + if (schema.AsObject().ContainsKey(JsonSchema.Required)) + { + var required = schema[JsonSchema.Required]!.AsArray(); + Assert.DoesNotContain(required, r => r?.ToString() == JsonSchema.Result); + } + } + + #endregion + #region Custom Type Tests [Fact] @@ -440,6 +816,98 @@ public void GetReturnSchema_ComplexType_ReturnsValidNestedSchema() #endregion + #region Nullable Custom Type Tests + + [Fact] + public void GetReturnSchema_NullableCustomType_ReturnsValidObjectSchemaWithoutRequired() + { + // Arrange + var reflector = new Reflector(); + var methodInfo = GetType().GetMethod(nameof(NullableCustomTypeMethod), BindingFlags.NonPublic | BindingFlags.Instance)!; + + // Act + var schema = reflector.GetReturnSchema(methodInfo); + + // Assert + 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(JsonSchema.Result)); + + // The result property should contain the CustomReturnType schema (either inline or ref) + var resultSchema = properties[JsonSchema.Result]!.AsObject(); + + // It could be a $ref or an inline schema + if (resultSchema.ContainsKey(JsonSchema.Ref)) + { + // If it's a ref, $defs should exist with the type definition + Assert.True(schema.AsObject().ContainsKey(JsonSchema.Defs)); + } + else + { + // If it's inline, verify the properties + Assert.True(resultSchema.ContainsKey(JsonSchema.Properties)); + var customTypeProperties = resultSchema[JsonSchema.Properties]!.AsObject(); + Assert.True(customTypeProperties.ContainsKey("Name")); + Assert.True(customTypeProperties.ContainsKey("Value")); + } + + // Verify that "result" is NOT in the required array for nullable return types + if (schema.AsObject().ContainsKey(JsonSchema.Required)) + { + var required = schema[JsonSchema.Required]!.AsArray(); + Assert.DoesNotContain(required, r => r?.ToString() == JsonSchema.Result); + } + } + + [Fact] + public void GetReturnSchema_NullableComplexType_ReturnsValidNestedSchemaWithoutRequired() + { + // Arrange + var reflector = new Reflector(); + var methodInfo = GetType().GetMethod(nameof(NullableComplexTypeMethod), BindingFlags.NonPublic | BindingFlags.Instance)!; + + // Act + var schema = reflector.GetReturnSchema(methodInfo); + + // Assert + 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(JsonSchema.Result)); + + // The result property should contain the ComplexReturnType schema + var resultSchema = properties[JsonSchema.Result]!.AsObject(); + + // It could be a $ref or an inline schema + if (resultSchema.ContainsKey(JsonSchema.Ref)) + { + Assert.True(schema.AsObject().ContainsKey(JsonSchema.Defs)); + } + else + { + Assert.True(resultSchema.ContainsKey(JsonSchema.Properties)); + var complexTypeProperties = resultSchema[JsonSchema.Properties]!.AsObject(); + Assert.True(complexTypeProperties.ContainsKey("StringProperty")); + Assert.True(complexTypeProperties.ContainsKey("IntProperty")); + Assert.True(complexTypeProperties.ContainsKey("NestedObject")); + Assert.True(complexTypeProperties.ContainsKey("StringArray")); + } + + // Verify that "result" is NOT in the required array for nullable return types + if (schema.AsObject().ContainsKey(JsonSchema.Required)) + { + var required = schema[JsonSchema.Required]!.AsArray(); + Assert.DoesNotContain(required, r => r?.ToString() == JsonSchema.Result); + } + } + + #endregion + #region Collection Tests [Fact] @@ -526,6 +994,58 @@ public void GetReturnSchema_ListInt_ReturnsArraySchema() #endregion + #region Nullable Collection Tests + + [Fact] + public void GetReturnSchema_NullableStringArray_ReturnsArraySchemaWithoutRequired() + { + // Arrange + var reflector = new Reflector(); + var methodInfo = GetType().GetMethod(nameof(NullableStringArrayMethod), BindingFlags.NonPublic | BindingFlags.Instance)!; + + // Act + var schema = reflector.GetReturnSchema(methodInfo); + + // Assert + 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(JsonSchema.Result)); + + // The result property should be an array schema (either inline or ref) + var resultNode = properties[JsonSchema.Result]!; + + if (resultNode is JsonObject resultSchema && resultSchema.ContainsKey(JsonSchema.Ref)) + { + // If it's a ref, verify $defs contains the array schema + Assert.True(schema.AsObject().ContainsKey(JsonSchema.Defs)); + } + else if (resultNode is JsonObject resultInlineSchema) + { + // If it's inline, verify it's an array schema + Assert.Equal(JsonSchema.Array, resultInlineSchema[JsonSchema.Type]?.ToString()); + Assert.True(resultInlineSchema.ContainsKey(JsonSchema.Items)); + + var items = resultInlineSchema[JsonSchema.Items]!; + Assert.Equal(JsonSchema.String, items[JsonSchema.Type]?.ToString()); + } + else + { + Assert.Fail("Expected result to be a schema object"); + } + + // Verify that "result" is NOT in the required array for nullable return types + if (schema.AsObject().ContainsKey(JsonSchema.Required)) + { + var required = schema[JsonSchema.Required]!.AsArray(); + Assert.DoesNotContain(required, r => r?.ToString() == JsonSchema.Result); + } + } + + #endregion + #region JustRef Parameter Tests [Fact] diff --git a/ReflectorNet/src/Utils/Json/JsonSchema.cs b/ReflectorNet/src/Utils/Json/JsonSchema.cs index bd0ad86f..f86c006e 100644 --- a/ReflectorNet/src/Utils/Json/JsonSchema.cs +++ b/ReflectorNet/src/Utils/Json/JsonSchema.cs @@ -367,7 +367,57 @@ public JsonNode GetArgumentsSchema(Reflector reflector, MethodInfo method, bool { var genericDefinition = unwrappedType.GetGenericTypeDefinition(); if (genericDefinition == typeof(Task<>) || genericDefinition == typeof(ValueTask<>)) - unwrappedType = unwrappedType.GetGenericArguments()[0]; + { + var taskGenericArg = unwrappedType.GetGenericArguments()[0]; + + // Check if T in Task is nullable (e.g., Task or Task) + var nullableUnderlyingType = Nullable.GetUnderlyingType(taskGenericArg); + if (nullableUnderlyingType != null) + { + // T is a nullable value type (e.g., int?, bool?) + isNullable = true; + unwrappedType = nullableUnderlyingType; + } + else + { + unwrappedType = taskGenericArg; + } + } + } + + // Check for nullable reference types using NullabilityInfoContext + if (!isNullable && (unwrappedType.IsClass || unwrappedType.IsInterface || unwrappedType.IsArray)) + { + try + { +#if NET5_0_OR_GREATER + var nullabilityContext = new System.Reflection.NullabilityInfoContext(); + var nullabilityInfo = nullabilityContext.Create(methodInfo.ReturnParameter); + + // For Task or ValueTask, check the generic argument's nullability + if (returnType.IsGenericType) + { + var genericDef = returnType.GetGenericTypeDefinition(); + if (genericDef == typeof(Task<>) || genericDef == typeof(ValueTask<>)) + { + isNullable = nullabilityInfo.GenericTypeArguments.Length > 0 && + nullabilityInfo.GenericTypeArguments[0].ReadState == System.Reflection.NullabilityState.Nullable; + } + else + { + isNullable = nullabilityInfo.ReadState == System.Reflection.NullabilityState.Nullable; + } + } + else + { + isNullable = nullabilityInfo.ReadState == System.Reflection.NullabilityState.Nullable; + } +#endif + } + catch + { + // If we can't determine nullability, assume not nullable + } } // Generate schema for the return type using the shared method From 01c6f4446478dfebb0bac49a24ff29ffbaa586f5 Mon Sep 17 00:00:00 2001 From: Ivan Murzak Date: Wed, 15 Oct 2025 23:40:05 -0700 Subject: [PATCH 03/24] feat: add support for nullable Task return types in JSON schema generation --- .../SchemaTests/ReturnSchemaTests.cs | 110 ++++++++++++++++++ ReflectorNet/src/Utils/Json/JsonSchema.cs | 63 ++++++---- 2 files changed, 149 insertions(+), 24 deletions(-) diff --git a/ReflectorNet.Tests/SchemaTests/ReturnSchemaTests.cs b/ReflectorNet.Tests/SchemaTests/ReturnSchemaTests.cs index 76839cd0..a889a229 100644 --- a/ReflectorNet.Tests/SchemaTests/ReturnSchemaTests.cs +++ b/ReflectorNet.Tests/SchemaTests/ReturnSchemaTests.cs @@ -75,6 +75,14 @@ private void VoidMethod() { } private ValueTask ValueTaskNullableIntMethod() => ValueTask.FromResult(42); private ValueTask ValueTaskNullableCustomTypeMethod() => ValueTask.FromResult(new CustomReturnType()); + // Task? return types (nullable T wrapped in nullable Task) + private Task? NullableTaskNullableStringMethod() => Task.FromResult("test"); + private Task? NullableTaskNullableIntMethod() => Task.FromResult(42); + private Task? NullableTaskNullableCustomTypeMethod() => Task.FromResult(new CustomReturnType()); + + // NOTE: ValueTask? is not a valid scenario because ValueTask is a struct, not a class + // So it cannot be made nullable in the reference type sense. We removed these tests. + #endregion #region Test Classes @@ -576,6 +584,105 @@ public void GetReturnSchema_TaskNullableCustomType_UnwrapsToCustomTypeSchemaWith #endregion + #region Nullable Task Unwrapping Tests + + [Fact] + public void GetReturnSchema_NullableTaskNullableString_UnwrapsToStringSchemaWithoutRequired() + { + // Arrange + var reflector = new Reflector(); + var methodInfo = GetType().GetMethod(nameof(NullableTaskNullableStringMethod), BindingFlags.NonPublic | BindingFlags.Instance)!; + + // Act + var schema = reflector.GetReturnSchema(methodInfo); + + // Assert + 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(JsonSchema.Result)); + Assert.Equal(JsonSchema.String, properties[JsonSchema.Result]![JsonSchema.Type]?.ToString()); + + // Verify that "result" is NOT in the required array for nullable return types + if (schema.AsObject().ContainsKey(JsonSchema.Required)) + { + var required = schema[JsonSchema.Required]!.AsArray(); + Assert.DoesNotContain(required, r => r?.ToString() == JsonSchema.Result); + } + } + + [Fact] + public void GetReturnSchema_NullableTaskNullableInt_UnwrapsToIntegerSchemaWithoutRequired() + { + // Arrange + var reflector = new Reflector(); + var methodInfo = GetType().GetMethod(nameof(NullableTaskNullableIntMethod), BindingFlags.NonPublic | BindingFlags.Instance)!; + + // Act + var schema = reflector.GetReturnSchema(methodInfo); + + // Assert + Assert.NotNull(schema); + Assert.Equal(JsonSchema.Object, schema[JsonSchema.Type]?.ToString()); + var properties = schema[JsonSchema.Properties]!.AsObject(); + Assert.True(properties.ContainsKey(JsonSchema.Result)); + Assert.Equal(JsonSchema.Integer, properties[JsonSchema.Result]![JsonSchema.Type]?.ToString()); + + // Verify that "result" is NOT in the required array for nullable return types + if (schema.AsObject().ContainsKey(JsonSchema.Required)) + { + var required = schema[JsonSchema.Required]!.AsArray(); + Assert.DoesNotContain(required, r => r?.ToString() == JsonSchema.Result); + } + } + + [Fact] + public void GetReturnSchema_NullableTaskNullableCustomType_UnwrapsToCustomTypeSchemaWithoutRequired() + { + // Arrange + var reflector = new Reflector(); + var methodInfo = GetType().GetMethod(nameof(NullableTaskNullableCustomTypeMethod), BindingFlags.NonPublic | BindingFlags.Instance)!; + + // Act + var schema = reflector.GetReturnSchema(methodInfo); + + // Assert + 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(JsonSchema.Result)); + + // The result property should contain a $ref to the CustomReturnType in $defs + var resultSchema = properties[JsonSchema.Result]!.AsObject(); + Assert.True(resultSchema.ContainsKey(JsonSchema.Ref) || resultSchema.ContainsKey(JsonSchema.Properties)); + + // If it's a ref, verify $defs exists + if (resultSchema.ContainsKey(JsonSchema.Ref)) + { + Assert.True(schema.AsObject().ContainsKey(JsonSchema.Defs)); + } + // If it's not a ref, verify it has the expected properties + else + { + var nestedProperties = resultSchema[JsonSchema.Properties]!.AsObject(); + Assert.True(nestedProperties.ContainsKey("Name")); + Assert.True(nestedProperties.ContainsKey("Value")); + } + + // Verify that "result" is NOT in the required array for nullable return types + if (schema.AsObject().ContainsKey(JsonSchema.Required)) + { + var required = schema[JsonSchema.Required]!.AsArray(); + Assert.DoesNotContain(required, r => r?.ToString() == JsonSchema.Result); + } + } + + #endregion + #region ValueTask Unwrapping Tests [Fact] @@ -738,6 +845,9 @@ public void GetReturnSchema_ValueTaskNullableCustomType_UnwrapsToCustomTypeSchem #endregion + // NOTE: Nullable ValueTask tests removed because ValueTask is a struct and cannot be made nullable + // in the reference type sense (ValueTask? is not valid) + #region Custom Type Tests [Fact] diff --git a/ReflectorNet/src/Utils/Json/JsonSchema.cs b/ReflectorNet/src/Utils/Json/JsonSchema.cs index f86c006e..05d62358 100644 --- a/ReflectorNet/src/Utils/Json/JsonSchema.cs +++ b/ReflectorNet/src/Utils/Json/JsonSchema.cs @@ -351,40 +351,55 @@ public JsonNode GetArgumentsSchema(Reflector reflector, MethodInfo method, bool throw new ArgumentNullException(nameof(methodInfo)); var returnType = methodInfo.ReturnType; - var unwrappedType = Nullable.GetUnderlyingType(returnType); - var isNullable = unwrappedType != null; - - unwrappedType ??= returnType; // Handle void, Task, and ValueTask - these have no return value - if (unwrappedType == typeof(void) || - unwrappedType == typeof(Task) || - unwrappedType == typeof(ValueTask)) + if (returnType == typeof(void) || + returnType == typeof(Task) || + returnType == typeof(ValueTask)) return null; - // Unwrap Task and ValueTask to get the actual return type T + // First, check if the return type itself is a nullable Task/ValueTask (e.g., Task? or ValueTask?) + var isTaskNullable = false; +#if NET5_0_OR_GREATER + try + { + var nullabilityContext = new System.Reflection.NullabilityInfoContext(); + var returnTypeNullabilityInfo = nullabilityContext.Create(methodInfo.ReturnParameter); + + // Check if the Task/ValueTask itself is nullable + if (returnType.IsGenericType && + (returnType.GetGenericTypeDefinition() == typeof(Task<>) || returnType.GetGenericTypeDefinition() == typeof(ValueTask<>))) + { + isTaskNullable = returnTypeNullabilityInfo.ReadState == System.Reflection.NullabilityState.Nullable; + } + } + catch + { + // If we can't determine nullability, assume not nullable + } +#endif + + // Unwrap Task/ValueTask first, then compute nullability on the inner T + var unwrappedType = returnType; + var isNullable = isTaskNullable; // If the Task itself is nullable, the result is also nullable + if (unwrappedType.IsGenericType) { var genericDefinition = unwrappedType.GetGenericTypeDefinition(); if (genericDefinition == typeof(Task<>) || genericDefinition == typeof(ValueTask<>)) { - var taskGenericArg = unwrappedType.GetGenericArguments()[0]; - - // Check if T in Task is nullable (e.g., Task or Task) - var nullableUnderlyingType = Nullable.GetUnderlyingType(taskGenericArg); - if (nullableUnderlyingType != null) - { - // T is a nullable value type (e.g., int?, bool?) - isNullable = true; - unwrappedType = nullableUnderlyingType; - } - else - { - unwrappedType = taskGenericArg; - } + unwrappedType = unwrappedType.GetGenericArguments()[0]; } } + // Recompute nullability after unwrapping async wrappers + var nullableUnderlyingType = Nullable.GetUnderlyingType(unwrappedType); + if (nullableUnderlyingType != null) + { + isNullable = true; + unwrappedType = nullableUnderlyingType; + } + // Check for nullable reference types using NullabilityInfoContext if (!isNullable && (unwrappedType.IsClass || unwrappedType.IsInterface || unwrappedType.IsArray)) { @@ -400,8 +415,8 @@ public JsonNode GetArgumentsSchema(Reflector reflector, MethodInfo method, bool var genericDef = returnType.GetGenericTypeDefinition(); if (genericDef == typeof(Task<>) || genericDef == typeof(ValueTask<>)) { - isNullable = nullabilityInfo.GenericTypeArguments.Length > 0 && - nullabilityInfo.GenericTypeArguments[0].ReadState == System.Reflection.NullabilityState.Nullable; + isNullable = isNullable || (nullabilityInfo.GenericTypeArguments.Length > 0 && + nullabilityInfo.GenericTypeArguments[0].ReadState == System.Reflection.NullabilityState.Nullable); } else { From 5921a947ad5a8ccee82c1c0eadf644e1b68187ca Mon Sep 17 00:00:00 2001 From: Ivan Murzak Date: Wed, 15 Oct 2025 23:49:35 -0700 Subject: [PATCH 04/24] Refactor return schema tests to use theory data-driven approach - Consolidated void return type tests into a single theory with inline data for void, Task, and ValueTask methods. - Combined primitive return type tests into a single theory with inline data for string, int, bool, and double methods. - Merged nullable primitive return type tests into a single theory with inline data for nullable types. - Unified task primitive unwrapping tests into a single theory with inline data for task methods. - Streamlined nullable task primitive unwrapping tests into a single theory with inline data. - Refactored value task unwrapping tests into a single theory with inline data for value task methods. - Added helper methods in SchemaTestBase for asserting return schema structures, including primitive, custom type, and array schemas. --- .../SchemaTests/ReturnSchemaTests.cs | 1005 ++--------------- .../SchemaTests/SchemaTestBase.cs | 128 +++ 2 files changed, 209 insertions(+), 924 deletions(-) diff --git a/ReflectorNet.Tests/SchemaTests/ReturnSchemaTests.cs b/ReflectorNet.Tests/SchemaTests/ReturnSchemaTests.cs index a889a229..4fba14ca 100644 --- a/ReflectorNet.Tests/SchemaTests/ReturnSchemaTests.cs +++ b/ReflectorNet.Tests/SchemaTests/ReturnSchemaTests.cs @@ -105,45 +105,13 @@ public class ComplexReturnType #region Void Return Type Tests - [Fact] - public void GetReturnSchema_VoidMethod_ReturnsNull() + [Theory] + [InlineData(nameof(VoidMethod))] + [InlineData(nameof(TaskMethod))] + [InlineData(nameof(ValueTaskMethod))] + public void GetReturnSchema_VoidMethods_ReturnsNull(string methodName) { - // Arrange - var reflector = new Reflector(); - var methodInfo = GetType().GetMethod(nameof(VoidMethod), BindingFlags.NonPublic | BindingFlags.Instance)!; - - // Act - var schema = reflector.GetReturnSchema(methodInfo); - - // Assert - Assert.Null(schema); - } - - [Fact] - public void GetReturnSchema_TaskMethod_ReturnsNull() - { - // Arrange - var reflector = new Reflector(); - var methodInfo = GetType().GetMethod(nameof(TaskMethod), BindingFlags.NonPublic | BindingFlags.Instance)!; - - // Act - var schema = reflector.GetReturnSchema(methodInfo); - - // Assert - Assert.Null(schema); - } - - [Fact] - public void GetReturnSchema_ValueTaskMethod_ReturnsNull() - { - // Arrange - var reflector = new Reflector(); - var methodInfo = GetType().GetMethod(nameof(ValueTaskMethod), BindingFlags.NonPublic | BindingFlags.Instance)!; - - // Act - var schema = reflector.GetReturnSchema(methodInfo); - - // Assert + var schema = GetReturnSchemaForMethod(methodName); Assert.Null(schema); } @@ -151,696 +119,133 @@ public void GetReturnSchema_ValueTaskMethod_ReturnsNull() #region Primitive Return Type Tests - [Fact] - public void GetReturnSchema_StringMethod_ReturnsStringSchema() - { - // Arrange - var reflector = new Reflector(); - var methodInfo = GetType().GetMethod(nameof(StringMethod), BindingFlags.NonPublic | BindingFlags.Instance)!; - - // Act - var schema = reflector.GetReturnSchema(methodInfo); - - // Assert - Assert.NotNull(schema); - Assert.Equal(JsonSchema.Object, schema[JsonSchema.Type]?.ToString()); - Assert.True(schema.AsObject().ContainsKey(JsonSchema.Properties)); - Assert.True(schema.AsObject().ContainsKey(JsonSchema.Required)); - - var properties = schema[JsonSchema.Properties]!.AsObject(); - Assert.True(properties.ContainsKey(JsonSchema.Result)); - Assert.Equal(JsonSchema.String, properties[JsonSchema.Result]![JsonSchema.Type]?.ToString()); - - var required = schema[JsonSchema.Required]!.AsArray(); - Assert.Single(required); - Assert.Equal(JsonSchema.Result, required[0]?.ToString()); - } - - [Fact] - public void GetReturnSchema_IntMethod_ReturnsIntegerSchema() + [Theory] + [InlineData(nameof(StringMethod), JsonSchema.String)] + [InlineData(nameof(IntMethod), JsonSchema.Integer)] + [InlineData(nameof(BoolMethod), JsonSchema.Boolean)] + [InlineData(nameof(DoubleMethod), JsonSchema.Number)] + public void GetReturnSchema_PrimitiveMethod_ReturnsCorrectSchema(string methodName, string expectedType) { - // Arrange - var reflector = new Reflector(); - var methodInfo = GetType().GetMethod(nameof(IntMethod), BindingFlags.NonPublic | BindingFlags.Instance)!; - - // Act - var schema = reflector.GetReturnSchema(methodInfo); - - // Assert - Assert.NotNull(schema); - Assert.Equal(JsonSchema.Object, schema[JsonSchema.Type]?.ToString()); - var properties = schema[JsonSchema.Properties]!.AsObject(); - Assert.True(properties.ContainsKey(JsonSchema.Result)); - Assert.Equal(JsonSchema.Integer, properties[JsonSchema.Result]![JsonSchema.Type]?.ToString()); - } - - [Fact] - public void GetReturnSchema_BoolMethod_ReturnsBooleanSchema() - { - // Arrange - var reflector = new Reflector(); - var methodInfo = GetType().GetMethod(nameof(BoolMethod), BindingFlags.NonPublic | BindingFlags.Instance)!; - - // Act - var schema = reflector.GetReturnSchema(methodInfo); - - // Assert - Assert.NotNull(schema); - Assert.Equal(JsonSchema.Object, schema[JsonSchema.Type]?.ToString()); - var properties = schema[JsonSchema.Properties]!.AsObject(); - Assert.True(properties.ContainsKey(JsonSchema.Result)); - Assert.Equal(JsonSchema.Boolean, properties[JsonSchema.Result]![JsonSchema.Type]?.ToString()); - } - - [Fact] - public void GetReturnSchema_DoubleMethod_ReturnsNumberSchema() - { - // Arrange - var reflector = new Reflector(); - var methodInfo = GetType().GetMethod(nameof(DoubleMethod), BindingFlags.NonPublic | BindingFlags.Instance)!; - - // Act - var schema = reflector.GetReturnSchema(methodInfo); - - // Assert - Assert.NotNull(schema); - Assert.Equal(JsonSchema.Object, schema[JsonSchema.Type]?.ToString()); - var properties = schema[JsonSchema.Properties]!.AsObject(); - Assert.True(properties.ContainsKey(JsonSchema.Result)); - Assert.Equal(JsonSchema.Number, properties[JsonSchema.Result]![JsonSchema.Type]?.ToString()); + var schema = GetReturnSchemaForMethod(methodName); + AssertPrimitiveReturnSchema(schema!, expectedType, shouldBeRequired: true); } #endregion #region Nullable Primitive Return Type Tests - [Fact] - public void GetReturnSchema_NullableStringMethod_ReturnsStringSchemaWithoutRequired() - { - // Arrange - var reflector = new Reflector(); - var methodInfo = GetType().GetMethod(nameof(NullableStringMethod), BindingFlags.NonPublic | BindingFlags.Instance)!; - - // Act - var schema = reflector.GetReturnSchema(methodInfo); - - // Assert - 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(JsonSchema.Result)); - Assert.Equal(JsonSchema.String, properties[JsonSchema.Result]![JsonSchema.Type]?.ToString()); - - // Verify that "result" is NOT in the required array for nullable return types - if (schema.AsObject().ContainsKey(JsonSchema.Required)) - { - var required = schema[JsonSchema.Required]!.AsArray(); - Assert.DoesNotContain(required, r => r?.ToString() == JsonSchema.Result); - } - } - - [Fact] - public void GetReturnSchema_NullableIntMethod_ReturnsIntegerSchemaWithoutRequired() - { - // Arrange - var reflector = new Reflector(); - var methodInfo = GetType().GetMethod(nameof(NullableIntMethod), BindingFlags.NonPublic | BindingFlags.Instance)!; - - // Act - var schema = reflector.GetReturnSchema(methodInfo); - - // Assert - Assert.NotNull(schema); - Assert.Equal(JsonSchema.Object, schema[JsonSchema.Type]?.ToString()); - var properties = schema[JsonSchema.Properties]!.AsObject(); - Assert.True(properties.ContainsKey(JsonSchema.Result)); - Assert.Equal(JsonSchema.Integer, properties[JsonSchema.Result]![JsonSchema.Type]?.ToString()); - - // Verify that "result" is NOT in the required array for nullable return types - if (schema.AsObject().ContainsKey(JsonSchema.Required)) - { - var required = schema[JsonSchema.Required]!.AsArray(); - Assert.DoesNotContain(required, r => r?.ToString() == JsonSchema.Result); - } - } - - [Fact] - public void GetReturnSchema_NullableBoolMethod_ReturnsBooleanSchemaWithoutRequired() - { - // Arrange - var reflector = new Reflector(); - var methodInfo = GetType().GetMethod(nameof(NullableBoolMethod), BindingFlags.NonPublic | BindingFlags.Instance)!; - - // Act - var schema = reflector.GetReturnSchema(methodInfo); - - // Assert - Assert.NotNull(schema); - Assert.Equal(JsonSchema.Object, schema[JsonSchema.Type]?.ToString()); - var properties = schema[JsonSchema.Properties]!.AsObject(); - Assert.True(properties.ContainsKey(JsonSchema.Result)); - Assert.Equal(JsonSchema.Boolean, properties[JsonSchema.Result]![JsonSchema.Type]?.ToString()); - - // Verify that "result" is NOT in the required array for nullable return types - if (schema.AsObject().ContainsKey(JsonSchema.Required)) - { - var required = schema[JsonSchema.Required]!.AsArray(); - Assert.DoesNotContain(required, r => r?.ToString() == JsonSchema.Result); - } - } - - [Fact] - public void GetReturnSchema_NullableDoubleMethod_ReturnsNumberSchemaWithoutRequired() + [Theory] + [InlineData(nameof(NullableStringMethod), JsonSchema.String)] + [InlineData(nameof(NullableIntMethod), JsonSchema.Integer)] + [InlineData(nameof(NullableBoolMethod), JsonSchema.Boolean)] + [InlineData(nameof(NullableDoubleMethod), JsonSchema.Number)] + [InlineData(nameof(NullableDateTimeMethod), JsonSchema.String)] // DateTime serialized as string + public void GetReturnSchema_NullablePrimitiveMethod_ReturnsSchemaWithoutRequired(string methodName, string expectedType) { - // Arrange - var reflector = new Reflector(); - var methodInfo = GetType().GetMethod(nameof(NullableDoubleMethod), BindingFlags.NonPublic | BindingFlags.Instance)!; - - // Act - var schema = reflector.GetReturnSchema(methodInfo); - - // Assert - Assert.NotNull(schema); - Assert.Equal(JsonSchema.Object, schema[JsonSchema.Type]?.ToString()); - var properties = schema[JsonSchema.Properties]!.AsObject(); - Assert.True(properties.ContainsKey(JsonSchema.Result)); - Assert.Equal(JsonSchema.Number, properties[JsonSchema.Result]![JsonSchema.Type]?.ToString()); - - // Verify that "result" is NOT in the required array for nullable return types - if (schema.AsObject().ContainsKey(JsonSchema.Required)) - { - var required = schema[JsonSchema.Required]!.AsArray(); - Assert.DoesNotContain(required, r => r?.ToString() == JsonSchema.Result); - } - } - - [Fact] - public void GetReturnSchema_NullableDateTimeMethod_ReturnsStringSchemaWithoutRequired() - { - // Arrange - var reflector = new Reflector(); - var methodInfo = GetType().GetMethod(nameof(NullableDateTimeMethod), BindingFlags.NonPublic | BindingFlags.Instance)!; - - // Act - var schema = reflector.GetReturnSchema(methodInfo); - - // Assert - Assert.NotNull(schema); - Assert.Equal(JsonSchema.Object, schema[JsonSchema.Type]?.ToString()); - var properties = schema[JsonSchema.Properties]!.AsObject(); - Assert.True(properties.ContainsKey(JsonSchema.Result)); - // DateTime is typically serialized as string in JSON - Assert.Equal(JsonSchema.String, properties[JsonSchema.Result]![JsonSchema.Type]?.ToString()); - - // Verify that "result" is NOT in the required array for nullable return types - if (schema.AsObject().ContainsKey(JsonSchema.Required)) - { - var required = schema[JsonSchema.Required]!.AsArray(); - Assert.DoesNotContain(required, r => r?.ToString() == JsonSchema.Result); - } + var schema = GetReturnSchemaForMethod(methodName); + AssertPrimitiveReturnSchema(schema!, expectedType, shouldBeRequired: false); } #endregion #region Task Unwrapping Tests - [Fact] - public void GetReturnSchema_TaskString_UnwrapsToStringSchema() + [Theory] + [InlineData(nameof(TaskStringMethod), JsonSchema.String)] + [InlineData(nameof(TaskIntMethod), JsonSchema.Integer)] + [InlineData(nameof(TaskBoolMethod), JsonSchema.Boolean)] + public void GetReturnSchema_TaskPrimitive_UnwrapsCorrectly(string methodName, string expectedType) { - // Arrange - var reflector = new Reflector(); - var methodInfo = GetType().GetMethod(nameof(TaskStringMethod), BindingFlags.NonPublic | BindingFlags.Instance)!; - - // Act - var schema = reflector.GetReturnSchema(methodInfo); - - // Assert - 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(JsonSchema.Result)); - Assert.Equal(JsonSchema.String, properties[JsonSchema.Result]![JsonSchema.Type]?.ToString()); - } - - [Fact] - public void GetReturnSchema_TaskInt_UnwrapsToIntegerSchema() - { - // Arrange - var reflector = new Reflector(); - var methodInfo = GetType().GetMethod(nameof(TaskIntMethod), BindingFlags.NonPublic | BindingFlags.Instance)!; - - // Act - var schema = reflector.GetReturnSchema(methodInfo); - - // Assert - Assert.NotNull(schema); - Assert.Equal(JsonSchema.Object, schema[JsonSchema.Type]?.ToString()); - var properties = schema[JsonSchema.Properties]!.AsObject(); - Assert.True(properties.ContainsKey(JsonSchema.Result)); - Assert.Equal(JsonSchema.Integer, properties[JsonSchema.Result]![JsonSchema.Type]?.ToString()); - } - - [Fact] - public void GetReturnSchema_TaskBool_UnwrapsToBooleanSchema() - { - // Arrange - var reflector = new Reflector(); - var methodInfo = GetType().GetMethod(nameof(TaskBoolMethod), BindingFlags.NonPublic | BindingFlags.Instance)!; - - // Act - var schema = reflector.GetReturnSchema(methodInfo); - - // Assert - Assert.NotNull(schema); - Assert.Equal(JsonSchema.Object, schema[JsonSchema.Type]?.ToString()); - var properties = schema[JsonSchema.Properties]!.AsObject(); - Assert.True(properties.ContainsKey(JsonSchema.Result)); - Assert.Equal(JsonSchema.Boolean, properties[JsonSchema.Result]![JsonSchema.Type]?.ToString()); + var schema = GetReturnSchemaForMethod(methodName); + AssertPrimitiveReturnSchema(schema!, expectedType, shouldBeRequired: true); } [Fact] public void GetReturnSchema_TaskCustomType_UnwrapsToCustomTypeSchema() { - // Arrange - var reflector = new Reflector(); - var methodInfo = GetType().GetMethod(nameof(TaskCustomTypeMethod), BindingFlags.NonPublic | BindingFlags.Instance)!; - - // Act - var schema = reflector.GetReturnSchema(methodInfo); - - // Assert - 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(JsonSchema.Result)); - - // The result property should contain a $ref to the CustomReturnType in $defs - var resultSchema = properties[JsonSchema.Result]!.AsObject(); - Assert.True(resultSchema.ContainsKey(JsonSchema.Ref) || resultSchema.ContainsKey(JsonSchema.Properties)); - - // If it's a ref, verify $defs exists - if (resultSchema.ContainsKey(JsonSchema.Ref)) - { - Assert.True(schema.AsObject().ContainsKey(JsonSchema.Defs)); - } - // If it's not a ref, verify it has the expected properties - else - { - var nestedProperties = resultSchema[JsonSchema.Properties]!.AsObject(); - Assert.True(nestedProperties.ContainsKey("Name")); - Assert.True(nestedProperties.ContainsKey("Value")); - } + var schema = GetReturnSchemaForMethod(nameof(TaskCustomTypeMethod)); + AssertCustomTypeReturnSchema(schema!, new[] { "Name", "Value" }, shouldBeRequired: true); } #endregion #region Task Nullable Unwrapping Tests - [Fact] - public void GetReturnSchema_TaskNullableString_UnwrapsToStringSchemaWithoutRequired() + [Theory] + [InlineData(nameof(TaskNullableStringMethod), JsonSchema.String)] + [InlineData(nameof(TaskNullableIntMethod), JsonSchema.Integer)] + [InlineData(nameof(TaskNullableBoolMethod), JsonSchema.Boolean)] + public void GetReturnSchema_TaskNullablePrimitive_UnwrapsWithoutRequired(string methodName, string expectedType) { - // Arrange - var reflector = new Reflector(); - var methodInfo = GetType().GetMethod(nameof(TaskNullableStringMethod), BindingFlags.NonPublic | BindingFlags.Instance)!; - - // Act - var schema = reflector.GetReturnSchema(methodInfo); - - // Assert - 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(JsonSchema.Result)); - Assert.Equal(JsonSchema.String, properties[JsonSchema.Result]![JsonSchema.Type]?.ToString()); - - // Verify that "result" is NOT in the required array for nullable return types - if (schema.AsObject().ContainsKey(JsonSchema.Required)) - { - var required = schema[JsonSchema.Required]!.AsArray(); - Assert.DoesNotContain(required, r => r?.ToString() == JsonSchema.Result); - } - } - - [Fact] - public void GetReturnSchema_TaskNullableInt_UnwrapsToIntegerSchemaWithoutRequired() - { - // Arrange - var reflector = new Reflector(); - var methodInfo = GetType().GetMethod(nameof(TaskNullableIntMethod), BindingFlags.NonPublic | BindingFlags.Instance)!; - - // Act - var schema = reflector.GetReturnSchema(methodInfo); - - // Assert - Assert.NotNull(schema); - Assert.Equal(JsonSchema.Object, schema[JsonSchema.Type]?.ToString()); - var properties = schema[JsonSchema.Properties]!.AsObject(); - Assert.True(properties.ContainsKey(JsonSchema.Result)); - Assert.Equal(JsonSchema.Integer, properties[JsonSchema.Result]![JsonSchema.Type]?.ToString()); - - // Verify that "result" is NOT in the required array for nullable return types - if (schema.AsObject().ContainsKey(JsonSchema.Required)) - { - var required = schema[JsonSchema.Required]!.AsArray(); - Assert.DoesNotContain(required, r => r?.ToString() == JsonSchema.Result); - } - } - - [Fact] - public void GetReturnSchema_TaskNullableBool_UnwrapsToBooleanSchemaWithoutRequired() - { - // Arrange - var reflector = new Reflector(); - var methodInfo = GetType().GetMethod(nameof(TaskNullableBoolMethod), BindingFlags.NonPublic | BindingFlags.Instance)!; - - // Act - var schema = reflector.GetReturnSchema(methodInfo); - - // Assert - Assert.NotNull(schema); - Assert.Equal(JsonSchema.Object, schema[JsonSchema.Type]?.ToString()); - var properties = schema[JsonSchema.Properties]!.AsObject(); - Assert.True(properties.ContainsKey(JsonSchema.Result)); - Assert.Equal(JsonSchema.Boolean, properties[JsonSchema.Result]![JsonSchema.Type]?.ToString()); - - // Verify that "result" is NOT in the required array for nullable return types - if (schema.AsObject().ContainsKey(JsonSchema.Required)) - { - var required = schema[JsonSchema.Required]!.AsArray(); - Assert.DoesNotContain(required, r => r?.ToString() == JsonSchema.Result); - } + var schema = GetReturnSchemaForMethod(methodName); + AssertPrimitiveReturnSchema(schema!, expectedType, shouldBeRequired: false); } [Fact] public void GetReturnSchema_TaskNullableCustomType_UnwrapsToCustomTypeSchemaWithoutRequired() { - // Arrange - var reflector = new Reflector(); - var methodInfo = GetType().GetMethod(nameof(TaskNullableCustomTypeMethod), BindingFlags.NonPublic | BindingFlags.Instance)!; - - // Act - var schema = reflector.GetReturnSchema(methodInfo); - - // Assert - 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(JsonSchema.Result)); - - // The result property should contain a $ref to the CustomReturnType in $defs - var resultSchema = properties[JsonSchema.Result]!.AsObject(); - Assert.True(resultSchema.ContainsKey(JsonSchema.Ref) || resultSchema.ContainsKey(JsonSchema.Properties)); - - // If it's a ref, verify $defs exists - if (resultSchema.ContainsKey(JsonSchema.Ref)) - { - Assert.True(schema.AsObject().ContainsKey(JsonSchema.Defs)); - } - // If it's not a ref, verify it has the expected properties - else - { - var nestedProperties = resultSchema[JsonSchema.Properties]!.AsObject(); - Assert.True(nestedProperties.ContainsKey("Name")); - Assert.True(nestedProperties.ContainsKey("Value")); - } - - // Verify that "result" is NOT in the required array for nullable return types - if (schema.AsObject().ContainsKey(JsonSchema.Required)) - { - var required = schema[JsonSchema.Required]!.AsArray(); - Assert.DoesNotContain(required, r => r?.ToString() == JsonSchema.Result); - } + var schema = GetReturnSchemaForMethod(nameof(TaskNullableCustomTypeMethod)); + AssertCustomTypeReturnSchema(schema!, new[] { "Name", "Value" }, shouldBeRequired: false); } #endregion #region Nullable Task Unwrapping Tests - [Fact] - public void GetReturnSchema_NullableTaskNullableString_UnwrapsToStringSchemaWithoutRequired() - { - // Arrange - var reflector = new Reflector(); - var methodInfo = GetType().GetMethod(nameof(NullableTaskNullableStringMethod), BindingFlags.NonPublic | BindingFlags.Instance)!; - - // Act - var schema = reflector.GetReturnSchema(methodInfo); - - // Assert - 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(JsonSchema.Result)); - Assert.Equal(JsonSchema.String, properties[JsonSchema.Result]![JsonSchema.Type]?.ToString()); - - // Verify that "result" is NOT in the required array for nullable return types - if (schema.AsObject().ContainsKey(JsonSchema.Required)) - { - var required = schema[JsonSchema.Required]!.AsArray(); - Assert.DoesNotContain(required, r => r?.ToString() == JsonSchema.Result); - } - } - - [Fact] - public void GetReturnSchema_NullableTaskNullableInt_UnwrapsToIntegerSchemaWithoutRequired() + [Theory] + [InlineData(nameof(NullableTaskNullableStringMethod), JsonSchema.String)] + [InlineData(nameof(NullableTaskNullableIntMethod), JsonSchema.Integer)] + public void GetReturnSchema_NullableTaskNullablePrimitive_UnwrapsWithoutRequired(string methodName, string expectedType) { - // Arrange - var reflector = new Reflector(); - var methodInfo = GetType().GetMethod(nameof(NullableTaskNullableIntMethod), BindingFlags.NonPublic | BindingFlags.Instance)!; - - // Act - var schema = reflector.GetReturnSchema(methodInfo); - - // Assert - Assert.NotNull(schema); - Assert.Equal(JsonSchema.Object, schema[JsonSchema.Type]?.ToString()); - var properties = schema[JsonSchema.Properties]!.AsObject(); - Assert.True(properties.ContainsKey(JsonSchema.Result)); - Assert.Equal(JsonSchema.Integer, properties[JsonSchema.Result]![JsonSchema.Type]?.ToString()); - - // Verify that "result" is NOT in the required array for nullable return types - if (schema.AsObject().ContainsKey(JsonSchema.Required)) - { - var required = schema[JsonSchema.Required]!.AsArray(); - Assert.DoesNotContain(required, r => r?.ToString() == JsonSchema.Result); - } + var schema = GetReturnSchemaForMethod(methodName); + AssertPrimitiveReturnSchema(schema!, expectedType, shouldBeRequired: false); } [Fact] public void GetReturnSchema_NullableTaskNullableCustomType_UnwrapsToCustomTypeSchemaWithoutRequired() { - // Arrange - var reflector = new Reflector(); - var methodInfo = GetType().GetMethod(nameof(NullableTaskNullableCustomTypeMethod), BindingFlags.NonPublic | BindingFlags.Instance)!; - - // Act - var schema = reflector.GetReturnSchema(methodInfo); - - // Assert - 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(JsonSchema.Result)); - - // The result property should contain a $ref to the CustomReturnType in $defs - var resultSchema = properties[JsonSchema.Result]!.AsObject(); - Assert.True(resultSchema.ContainsKey(JsonSchema.Ref) || resultSchema.ContainsKey(JsonSchema.Properties)); - - // If it's a ref, verify $defs exists - if (resultSchema.ContainsKey(JsonSchema.Ref)) - { - Assert.True(schema.AsObject().ContainsKey(JsonSchema.Defs)); - } - // If it's not a ref, verify it has the expected properties - else - { - var nestedProperties = resultSchema[JsonSchema.Properties]!.AsObject(); - Assert.True(nestedProperties.ContainsKey("Name")); - Assert.True(nestedProperties.ContainsKey("Value")); - } - - // Verify that "result" is NOT in the required array for nullable return types - if (schema.AsObject().ContainsKey(JsonSchema.Required)) - { - var required = schema[JsonSchema.Required]!.AsArray(); - Assert.DoesNotContain(required, r => r?.ToString() == JsonSchema.Result); - } + var schema = GetReturnSchemaForMethod(nameof(NullableTaskNullableCustomTypeMethod)); + AssertCustomTypeReturnSchema(schema!, new[] { "Name", "Value" }, shouldBeRequired: false); } #endregion #region ValueTask Unwrapping Tests - [Fact] - public void GetReturnSchema_ValueTaskString_UnwrapsToStringSchema() + [Theory] + [InlineData(nameof(ValueTaskStringMethod), JsonSchema.String)] + [InlineData(nameof(ValueTaskIntMethod), JsonSchema.Integer)] + public void GetReturnSchema_ValueTaskPrimitive_UnwrapsCorrectly(string methodName, string expectedType) { - // Arrange - var reflector = new Reflector(); - var methodInfo = GetType().GetMethod(nameof(ValueTaskStringMethod), BindingFlags.NonPublic | BindingFlags.Instance)!; - - // Act - var schema = reflector.GetReturnSchema(methodInfo); - - // Assert - Assert.NotNull(schema); - Assert.Equal(JsonSchema.Object, schema[JsonSchema.Type]?.ToString()); - var properties = schema[JsonSchema.Properties]!.AsObject(); - Assert.True(properties.ContainsKey(JsonSchema.Result)); - Assert.Equal(JsonSchema.String, properties[JsonSchema.Result]![JsonSchema.Type]?.ToString()); - } - - [Fact] - public void GetReturnSchema_ValueTaskInt_UnwrapsToIntegerSchema() - { - // Arrange - var reflector = new Reflector(); - var methodInfo = GetType().GetMethod(nameof(ValueTaskIntMethod), BindingFlags.NonPublic | BindingFlags.Instance)!; - - // Act - var schema = reflector.GetReturnSchema(methodInfo); - - // Assert - Assert.NotNull(schema); - Assert.Equal(JsonSchema.Object, schema[JsonSchema.Type]?.ToString()); - var properties = schema[JsonSchema.Properties]!.AsObject(); - Assert.True(properties.ContainsKey(JsonSchema.Result)); - Assert.Equal(JsonSchema.Integer, properties[JsonSchema.Result]![JsonSchema.Type]?.ToString()); + var schema = GetReturnSchemaForMethod(methodName); + AssertPrimitiveReturnSchema(schema!, expectedType, shouldBeRequired: true); } [Fact] public void GetReturnSchema_ValueTaskCustomType_UnwrapsToCustomTypeSchema() { - // Arrange - var reflector = new Reflector(); - var methodInfo = GetType().GetMethod(nameof(ValueTaskCustomTypeMethod), BindingFlags.NonPublic | BindingFlags.Instance)!; - - // Act - var schema = reflector.GetReturnSchema(methodInfo); - - // Assert - 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(JsonSchema.Result)); - - // The result property should contain a $ref to the CustomReturnType in $defs - var resultSchema = properties[JsonSchema.Result]!.AsObject(); - Assert.True(resultSchema.ContainsKey(JsonSchema.Ref) || resultSchema.ContainsKey(JsonSchema.Properties)); + var schema = GetReturnSchemaForMethod(nameof(ValueTaskCustomTypeMethod)); + AssertCustomTypeReturnSchema(schema!, new[] { "Name", "Value" }, shouldBeRequired: true); } #endregion #region ValueTask Nullable Unwrapping Tests - [Fact] - public void GetReturnSchema_ValueTaskNullableString_UnwrapsToStringSchemaWithoutRequired() + [Theory] + [InlineData(nameof(ValueTaskNullableStringMethod), JsonSchema.String)] + [InlineData(nameof(ValueTaskNullableIntMethod), JsonSchema.Integer)] + public void GetReturnSchema_ValueTaskNullablePrimitive_UnwrapsWithoutRequired(string methodName, string expectedType) { - // Arrange - var reflector = new Reflector(); - var methodInfo = GetType().GetMethod(nameof(ValueTaskNullableStringMethod), BindingFlags.NonPublic | BindingFlags.Instance)!; - - // Act - var schema = reflector.GetReturnSchema(methodInfo); - - // Assert - 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(JsonSchema.Result)); - Assert.Equal(JsonSchema.String, properties[JsonSchema.Result]![JsonSchema.Type]?.ToString()); - - // Verify that "result" is NOT in the required array for nullable return types - if (schema.AsObject().ContainsKey(JsonSchema.Required)) - { - var required = schema[JsonSchema.Required]!.AsArray(); - Assert.DoesNotContain(required, r => r?.ToString() == JsonSchema.Result); - } - } - - [Fact] - public void GetReturnSchema_ValueTaskNullableInt_UnwrapsToIntegerSchemaWithoutRequired() - { - // Arrange - var reflector = new Reflector(); - var methodInfo = GetType().GetMethod(nameof(ValueTaskNullableIntMethod), BindingFlags.NonPublic | BindingFlags.Instance)!; - - // Act - var schema = reflector.GetReturnSchema(methodInfo); - - // Assert - Assert.NotNull(schema); - Assert.Equal(JsonSchema.Object, schema[JsonSchema.Type]?.ToString()); - var properties = schema[JsonSchema.Properties]!.AsObject(); - Assert.True(properties.ContainsKey(JsonSchema.Result)); - Assert.Equal(JsonSchema.Integer, properties[JsonSchema.Result]![JsonSchema.Type]?.ToString()); - - // Verify that "result" is NOT in the required array for nullable return types - if (schema.AsObject().ContainsKey(JsonSchema.Required)) - { - var required = schema[JsonSchema.Required]!.AsArray(); - Assert.DoesNotContain(required, r => r?.ToString() == JsonSchema.Result); - } + var schema = GetReturnSchemaForMethod(methodName); + AssertPrimitiveReturnSchema(schema!, expectedType, shouldBeRequired: false); } [Fact] public void GetReturnSchema_ValueTaskNullableCustomType_UnwrapsToCustomTypeSchemaWithoutRequired() { - // Arrange - var reflector = new Reflector(); - var methodInfo = GetType().GetMethod(nameof(ValueTaskNullableCustomTypeMethod), BindingFlags.NonPublic | BindingFlags.Instance)!; - - // Act - var schema = reflector.GetReturnSchema(methodInfo); - - // Assert - 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(JsonSchema.Result)); - - // The result property should contain a $ref to the CustomReturnType in $defs - var resultSchema = properties[JsonSchema.Result]!.AsObject(); - Assert.True(resultSchema.ContainsKey(JsonSchema.Ref) || resultSchema.ContainsKey(JsonSchema.Properties)); - - // If it's a ref, verify $defs exists - if (resultSchema.ContainsKey(JsonSchema.Ref)) - { - Assert.True(schema.AsObject().ContainsKey(JsonSchema.Defs)); - } - // If it's not a ref, verify it has the expected properties - else - { - var nestedProperties = resultSchema[JsonSchema.Properties]!.AsObject(); - Assert.True(nestedProperties.ContainsKey("Name")); - Assert.True(nestedProperties.ContainsKey("Value")); - } - - // Verify that "result" is NOT in the required array for nullable return types - if (schema.AsObject().ContainsKey(JsonSchema.Required)) - { - var required = schema[JsonSchema.Required]!.AsArray(); - Assert.DoesNotContain(required, r => r?.ToString() == JsonSchema.Result); - } + var schema = GetReturnSchemaForMethod(nameof(ValueTaskNullableCustomTypeMethod)); + AssertCustomTypeReturnSchema(schema!, new[] { "Name", "Value" }, shouldBeRequired: false); } #endregion @@ -853,75 +258,15 @@ public void GetReturnSchema_ValueTaskNullableCustomType_UnwrapsToCustomTypeSchem [Fact] public void GetReturnSchema_CustomType_ReturnsValidObjectSchema() { - // Arrange - var reflector = new Reflector(); - var methodInfo = GetType().GetMethod(nameof(CustomTypeMethod), BindingFlags.NonPublic | BindingFlags.Instance)!; - - // Act - var schema = reflector.GetReturnSchema(methodInfo); - - // Assert - 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(JsonSchema.Result)); - - // The result property should contain the CustomReturnType schema (either inline or ref) - var resultSchema = properties[JsonSchema.Result]!.AsObject(); - - // It could be a $ref or an inline schema - if (resultSchema.ContainsKey(JsonSchema.Ref)) - { - // If it's a ref, $defs should exist with the type definition - Assert.True(schema.AsObject().ContainsKey(JsonSchema.Defs)); - } - else - { - // If it's inline, verify the properties - Assert.True(resultSchema.ContainsKey(JsonSchema.Properties)); - var customTypeProperties = resultSchema[JsonSchema.Properties]!.AsObject(); - Assert.True(customTypeProperties.ContainsKey("Name")); - Assert.True(customTypeProperties.ContainsKey("Value")); - } + var schema = GetReturnSchemaForMethod(nameof(CustomTypeMethod)); + AssertCustomTypeReturnSchema(schema!, new[] { "Name", "Value" }, shouldBeRequired: true); } [Fact] public void GetReturnSchema_ComplexType_ReturnsValidNestedSchema() { - // Arrange - var reflector = new Reflector(); - var methodInfo = GetType().GetMethod(nameof(ComplexTypeMethod), BindingFlags.NonPublic | BindingFlags.Instance)!; - - // Act - var schema = reflector.GetReturnSchema(methodInfo); - - // Assert - 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(JsonSchema.Result)); - - // The result property should contain the ComplexReturnType schema - var resultSchema = properties[JsonSchema.Result]!.AsObject(); - - // It could be a $ref or an inline schema - if (resultSchema.ContainsKey(JsonSchema.Ref)) - { - Assert.True(schema.AsObject().ContainsKey(JsonSchema.Defs)); - } - else - { - Assert.True(resultSchema.ContainsKey(JsonSchema.Properties)); - var complexTypeProperties = resultSchema[JsonSchema.Properties]!.AsObject(); - Assert.True(complexTypeProperties.ContainsKey("StringProperty")); - Assert.True(complexTypeProperties.ContainsKey("IntProperty")); - Assert.True(complexTypeProperties.ContainsKey("NestedObject")); - Assert.True(complexTypeProperties.ContainsKey("StringArray")); - } + var schema = GetReturnSchemaForMethod(nameof(ComplexTypeMethod)); + AssertCustomTypeReturnSchema(schema!, new[] { "StringProperty", "IntProperty", "NestedObject", "StringArray" }, shouldBeRequired: true); } #endregion @@ -931,175 +276,28 @@ public void GetReturnSchema_ComplexType_ReturnsValidNestedSchema() [Fact] public void GetReturnSchema_NullableCustomType_ReturnsValidObjectSchemaWithoutRequired() { - // Arrange - var reflector = new Reflector(); - var methodInfo = GetType().GetMethod(nameof(NullableCustomTypeMethod), BindingFlags.NonPublic | BindingFlags.Instance)!; - - // Act - var schema = reflector.GetReturnSchema(methodInfo); - - // Assert - 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(JsonSchema.Result)); - - // The result property should contain the CustomReturnType schema (either inline or ref) - var resultSchema = properties[JsonSchema.Result]!.AsObject(); - - // It could be a $ref or an inline schema - if (resultSchema.ContainsKey(JsonSchema.Ref)) - { - // If it's a ref, $defs should exist with the type definition - Assert.True(schema.AsObject().ContainsKey(JsonSchema.Defs)); - } - else - { - // If it's inline, verify the properties - Assert.True(resultSchema.ContainsKey(JsonSchema.Properties)); - var customTypeProperties = resultSchema[JsonSchema.Properties]!.AsObject(); - Assert.True(customTypeProperties.ContainsKey("Name")); - Assert.True(customTypeProperties.ContainsKey("Value")); - } - - // Verify that "result" is NOT in the required array for nullable return types - if (schema.AsObject().ContainsKey(JsonSchema.Required)) - { - var required = schema[JsonSchema.Required]!.AsArray(); - Assert.DoesNotContain(required, r => r?.ToString() == JsonSchema.Result); - } + var schema = GetReturnSchemaForMethod(nameof(NullableCustomTypeMethod)); + AssertCustomTypeReturnSchema(schema!, new[] { "Name", "Value" }, shouldBeRequired: false); } [Fact] public void GetReturnSchema_NullableComplexType_ReturnsValidNestedSchemaWithoutRequired() { - // Arrange - var reflector = new Reflector(); - var methodInfo = GetType().GetMethod(nameof(NullableComplexTypeMethod), BindingFlags.NonPublic | BindingFlags.Instance)!; - - // Act - var schema = reflector.GetReturnSchema(methodInfo); - - // Assert - 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(JsonSchema.Result)); - - // The result property should contain the ComplexReturnType schema - var resultSchema = properties[JsonSchema.Result]!.AsObject(); - - // It could be a $ref or an inline schema - if (resultSchema.ContainsKey(JsonSchema.Ref)) - { - Assert.True(schema.AsObject().ContainsKey(JsonSchema.Defs)); - } - else - { - Assert.True(resultSchema.ContainsKey(JsonSchema.Properties)); - var complexTypeProperties = resultSchema[JsonSchema.Properties]!.AsObject(); - Assert.True(complexTypeProperties.ContainsKey("StringProperty")); - Assert.True(complexTypeProperties.ContainsKey("IntProperty")); - Assert.True(complexTypeProperties.ContainsKey("NestedObject")); - Assert.True(complexTypeProperties.ContainsKey("StringArray")); - } - - // Verify that "result" is NOT in the required array for nullable return types - if (schema.AsObject().ContainsKey(JsonSchema.Required)) - { - var required = schema[JsonSchema.Required]!.AsArray(); - Assert.DoesNotContain(required, r => r?.ToString() == JsonSchema.Result); - } + var schema = GetReturnSchemaForMethod(nameof(NullableComplexTypeMethod)); + AssertCustomTypeReturnSchema(schema!, new[] { "StringProperty", "IntProperty", "NestedObject", "StringArray" }, shouldBeRequired: false); } #endregion #region Collection Tests - [Fact] - public void GetReturnSchema_StringArray_ReturnsArraySchema() + [Theory] + [InlineData(nameof(StringArrayMethod), JsonSchema.String)] + [InlineData(nameof(ListIntMethod), JsonSchema.Integer)] + public void GetReturnSchema_CollectionTypes_ReturnsArraySchema(string methodName, string expectedItemType) { - // Arrange - var reflector = new Reflector(); - var methodInfo = GetType().GetMethod(nameof(StringArrayMethod), BindingFlags.NonPublic | BindingFlags.Instance)!; - - // Act - var schema = reflector.GetReturnSchema(methodInfo); - - // Assert - 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(JsonSchema.Result)); - - // The result property should be an array schema (either inline or ref) - var resultNode = properties[JsonSchema.Result]!; - - if (resultNode is JsonObject resultSchema && resultSchema.ContainsKey(JsonSchema.Ref)) - { - // If it's a ref, verify $defs contains the array schema - Assert.True(schema.AsObject().ContainsKey(JsonSchema.Defs)); - } - else if (resultNode is JsonObject resultInlineSchema) - { - // If it's inline, verify it's an array schema - Assert.Equal(JsonSchema.Array, resultInlineSchema[JsonSchema.Type]?.ToString()); - Assert.True(resultInlineSchema.ContainsKey(JsonSchema.Items)); - - var items = resultInlineSchema[JsonSchema.Items]!; - Assert.Equal(JsonSchema.String, items[JsonSchema.Type]?.ToString()); - } - else - { - Assert.Fail("Expected result to be a schema object"); - } - } - - [Fact] - public void GetReturnSchema_ListInt_ReturnsArraySchema() - { - // Arrange - var reflector = new Reflector(); - var methodInfo = GetType().GetMethod(nameof(ListIntMethod), BindingFlags.NonPublic | BindingFlags.Instance)!; - - // Act - var schema = reflector.GetReturnSchema(methodInfo); - - // Assert - 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(JsonSchema.Result)); - - // The result property should be an array schema (either inline or ref) - var resultNode = properties[JsonSchema.Result]!; - - if (resultNode is JsonObject resultSchema && resultSchema.ContainsKey(JsonSchema.Ref)) - { - // If it's a ref, verify $defs contains the array schema - Assert.True(schema.AsObject().ContainsKey(JsonSchema.Defs)); - } - else if (resultNode is JsonObject resultInlineSchema) - { - // If it's inline, verify it's an array schema - Assert.Equal(JsonSchema.Array, resultInlineSchema[JsonSchema.Type]?.ToString()); - Assert.True(resultInlineSchema.ContainsKey(JsonSchema.Items)); - - var items = resultInlineSchema[JsonSchema.Items]!; - Assert.Equal(JsonSchema.Integer, items[JsonSchema.Type]?.ToString()); - } - else - { - Assert.Fail("Expected result to be a schema object"); - } + var schema = GetReturnSchemaForMethod(methodName); + AssertArrayReturnSchema(schema!, expectedItemType, shouldBeRequired: true); } #endregion @@ -1109,49 +307,8 @@ public void GetReturnSchema_ListInt_ReturnsArraySchema() [Fact] public void GetReturnSchema_NullableStringArray_ReturnsArraySchemaWithoutRequired() { - // Arrange - var reflector = new Reflector(); - var methodInfo = GetType().GetMethod(nameof(NullableStringArrayMethod), BindingFlags.NonPublic | BindingFlags.Instance)!; - - // Act - var schema = reflector.GetReturnSchema(methodInfo); - - // Assert - 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(JsonSchema.Result)); - - // The result property should be an array schema (either inline or ref) - var resultNode = properties[JsonSchema.Result]!; - - if (resultNode is JsonObject resultSchema && resultSchema.ContainsKey(JsonSchema.Ref)) - { - // If it's a ref, verify $defs contains the array schema - Assert.True(schema.AsObject().ContainsKey(JsonSchema.Defs)); - } - else if (resultNode is JsonObject resultInlineSchema) - { - // If it's inline, verify it's an array schema - Assert.Equal(JsonSchema.Array, resultInlineSchema[JsonSchema.Type]?.ToString()); - Assert.True(resultInlineSchema.ContainsKey(JsonSchema.Items)); - - var items = resultInlineSchema[JsonSchema.Items]!; - Assert.Equal(JsonSchema.String, items[JsonSchema.Type]?.ToString()); - } - else - { - Assert.Fail("Expected result to be a schema object"); - } - - // Verify that "result" is NOT in the required array for nullable return types - if (schema.AsObject().ContainsKey(JsonSchema.Required)) - { - var required = schema[JsonSchema.Required]!.AsArray(); - Assert.DoesNotContain(required, r => r?.ToString() == JsonSchema.Result); - } + var schema = GetReturnSchemaForMethod(nameof(NullableStringArrayMethod)); + AssertArrayReturnSchema(schema!, JsonSchema.String, shouldBeRequired: false); } #endregion diff --git a/ReflectorNet.Tests/SchemaTests/SchemaTestBase.cs b/ReflectorNet.Tests/SchemaTests/SchemaTestBase.cs index 1dc9d49a..512b3a4c 100644 --- a/ReflectorNet.Tests/SchemaTests/SchemaTestBase.cs +++ b/ReflectorNet.Tests/SchemaTests/SchemaTestBase.cs @@ -95,5 +95,133 @@ protected void TestMethodInputs_PropertyRefs(Reflector? reflector, MethodInfo me return schema; } + + #region Return Schema Helper Methods + + /// + /// Gets the return schema for a method by name + /// + protected JsonNode? GetReturnSchemaForMethod(string methodName, bool justRef = false) + { + var reflector = new Reflector(); + var methodInfo = GetType().GetMethod(methodName, BindingFlags.NonPublic | BindingFlags.Instance)!; + return reflector.GetReturnSchema(methodInfo, justRef); + } + + /// + /// Asserts that a primitive return type has the correct schema structure + /// + protected void AssertPrimitiveReturnSchema(JsonNode schema, string expectedJsonType, bool shouldBeRequired = true) + { + Assert.NotNull(schema); + Assert.Equal(JsonSchema.Object, schema[JsonSchema.Type]?.ToString()); + + var properties = schema[JsonSchema.Properties]!.AsObject(); + Assert.True(properties.ContainsKey(JsonSchema.Result)); + Assert.Equal(expectedJsonType, properties[JsonSchema.Result]![JsonSchema.Type]?.ToString()); + + if (shouldBeRequired) + { + Assert.True(schema.AsObject().ContainsKey(JsonSchema.Required)); + var required = schema[JsonSchema.Required]!.AsArray(); + Assert.Single(required); + Assert.Equal(JsonSchema.Result, required[0]?.ToString()); + } + else + { + AssertResultNotRequired(schema); + } + } + + /// + /// Asserts that "result" is NOT in the required array (for nullable types) + /// + protected void AssertResultNotRequired(JsonNode schema) + { + if (schema.AsObject().ContainsKey(JsonSchema.Required)) + { + var required = schema[JsonSchema.Required]!.AsArray(); + Assert.DoesNotContain(required, r => r?.ToString() == JsonSchema.Result); + } + } + + /// + /// Asserts that a custom type return schema has the correct structure + /// + protected void AssertCustomTypeReturnSchema(JsonNode schema, string[] expectedProperties, bool shouldBeRequired = true) + { + 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(JsonSchema.Result)); + + var resultSchema = properties[JsonSchema.Result]!.AsObject(); + + // Check if it's a ref or inline + if (resultSchema.ContainsKey(JsonSchema.Ref)) + { + Assert.True(schema.AsObject().ContainsKey(JsonSchema.Defs)); + } + else if (resultSchema.ContainsKey(JsonSchema.Properties)) + { + var typeProperties = resultSchema[JsonSchema.Properties]!.AsObject(); + foreach (var expectedProp in expectedProperties) + { + Assert.True(typeProperties.ContainsKey(expectedProp)); + } + } + else + { + // If neither ref nor inline properties, this is unexpected + Assert.True(resultSchema.ContainsKey(JsonSchema.Ref) || resultSchema.ContainsKey(JsonSchema.Properties), + "Result schema should contain either $ref or properties"); + } + + if (!shouldBeRequired) + { + AssertResultNotRequired(schema); + } + } + + /// + /// Asserts that an array return type has the correct schema structure + /// + protected void AssertArrayReturnSchema(JsonNode schema, string expectedItemType, bool shouldBeRequired = true) + { + 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(JsonSchema.Result)); + + var resultNode = properties[JsonSchema.Result]!; + + if (resultNode is JsonObject resultSchema && resultSchema.ContainsKey(JsonSchema.Ref)) + { + Assert.True(schema.AsObject().ContainsKey(JsonSchema.Defs)); + } + else if (resultNode is JsonObject resultInlineSchema) + { + Assert.Equal(JsonSchema.Array, resultInlineSchema[JsonSchema.Type]?.ToString()); + Assert.True(resultInlineSchema.ContainsKey(JsonSchema.Items)); + + var items = resultInlineSchema[JsonSchema.Items]!; + Assert.Equal(expectedItemType, items[JsonSchema.Type]?.ToString()); + } + else + { + Assert.Fail("Expected result to be a schema object"); + } + + if (!shouldBeRequired) + { + AssertResultNotRequired(schema); + } + } + + #endregion } } From 18a0859976da59909a631dad502a6d7d657eb787 Mon Sep 17 00:00:00 2001 From: Ivan Murzak Date: Wed, 15 Oct 2025 23:54:34 -0700 Subject: [PATCH 05/24] fix: update cSpell configuration in settings.json and clean up unused usings in ReturnSchemaTests --- .vscode/settings.json | 3 ++- ReflectorNet.Tests/SchemaTests/ReturnSchemaTests.cs | 3 --- 2 files changed, 2 insertions(+), 4 deletions(-) diff --git a/.vscode/settings.json b/.vscode/settings.json index 8acc1158..77087efc 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -1,6 +1,7 @@ { "cSpell.words": [ "unstringified", - "Unstringify" + "Unstringify", + "Xunit" ] } \ No newline at end of file diff --git a/ReflectorNet.Tests/SchemaTests/ReturnSchemaTests.cs b/ReflectorNet.Tests/SchemaTests/ReturnSchemaTests.cs index 4fba14ca..648bd8a1 100644 --- a/ReflectorNet.Tests/SchemaTests/ReturnSchemaTests.cs +++ b/ReflectorNet.Tests/SchemaTests/ReturnSchemaTests.cs @@ -1,10 +1,7 @@ using System; -using System.Linq; using System.Reflection; -using System.Text.Json.Nodes; using System.Threading.Tasks; using com.IvanMurzak.ReflectorNet.Utils; -using Xunit; using Xunit.Abstractions; namespace com.IvanMurzak.ReflectorNet.Tests.SchemaTests From 8e04b6ee7564d2fa848e2aeafc390d94fe06825c Mon Sep 17 00:00:00 2001 From: Ivan Murzak Date: Thu, 16 Oct 2025 00:00:59 -0700 Subject: [PATCH 06/24] feat: add complex return type tests and assertion method for JSON schema validation --- .../SchemaTests/ReturnSchemaTests.cs | 72 +++++++++++++++++++ .../SchemaTests/SchemaTestBase.cs | 62 ++++++++++++++++ 2 files changed, 134 insertions(+) diff --git a/ReflectorNet.Tests/SchemaTests/ReturnSchemaTests.cs b/ReflectorNet.Tests/SchemaTests/ReturnSchemaTests.cs index 648bd8a1..225a1b62 100644 --- a/ReflectorNet.Tests/SchemaTests/ReturnSchemaTests.cs +++ b/ReflectorNet.Tests/SchemaTests/ReturnSchemaTests.cs @@ -51,6 +51,20 @@ private void VoidMethod() { } private System.Collections.Generic.List ListIntMethod() => new System.Collections.Generic.List(); private System.Collections.Generic.Dictionary DictionaryMethod() => new System.Collections.Generic.Dictionary(); + // List variations + private System.Collections.Generic.List ListComplexTypeMethod() => new System.Collections.Generic.List(); + private System.Collections.Generic.List ListNullableComplexTypeMethod() => new System.Collections.Generic.List(); + private System.Collections.Generic.List? NullableListComplexTypeMethod() => new System.Collections.Generic.List(); + private System.Collections.Generic.List? NullableListNullableComplexTypeMethod() => new System.Collections.Generic.List(); + private Task> TaskListComplexTypeMethod() => Task.FromResult(new System.Collections.Generic.List()); + private Task>? NullableTaskListComplexTypeMethod() => Task.FromResult(new System.Collections.Generic.List()); + private Task?> TaskNullableListComplexTypeMethod() => Task.FromResult?>(new System.Collections.Generic.List()); + private Task> TaskListNullableComplexTypeMethod() => Task.FromResult(new System.Collections.Generic.List()); + private Task?>? NullableTaskNullableListComplexTypeMethod() => Task.FromResult?>(new System.Collections.Generic.List()); + private Task?> TaskNullableListNullableComplexTypeMethod() => Task.FromResult?>(new System.Collections.Generic.List()); + private Task>? NullableTaskListNullableComplexTypeMethod() => Task.FromResult(new System.Collections.Generic.List()); + private Task?>? NullableTaskNullableListNullableComplexTypeMethod() => Task.FromResult?>(new System.Collections.Generic.List()); + // Nullable return types private string? NullableStringMethod() => "test"; private int? NullableIntMethod() => 42; @@ -310,6 +324,64 @@ public void GetReturnSchema_NullableStringArray_ReturnsArraySchemaWithoutRequire #endregion + #region List Tests + + [Theory] + [InlineData(nameof(ListComplexTypeMethod), true)] + [InlineData(nameof(NullableListComplexTypeMethod), false)] + public void GetReturnSchema_ListComplexType_ReturnsArraySchemaWithComplexItems(string methodName, bool shouldBeRequired) + { + var schema = GetReturnSchemaForMethod(methodName); + AssertComplexListReturnSchema(schema!, shouldBeRequired); + } + + [Theory] + [InlineData(nameof(ListNullableComplexTypeMethod), true)] + [InlineData(nameof(NullableListNullableComplexTypeMethod), false)] + public void GetReturnSchema_ListNullableComplexType_ReturnsArraySchemaWithNullableComplexItems(string methodName, bool shouldBeRequired) + { + var schema = GetReturnSchemaForMethod(methodName); + AssertComplexListReturnSchema(schema!, shouldBeRequired, itemsAreNullable: true); + } + + [Theory] + [InlineData(nameof(TaskListComplexTypeMethod), true)] + [InlineData(nameof(NullableTaskListComplexTypeMethod), false)] + public void GetReturnSchema_TaskListComplexType_UnwrapsToArraySchemaWithComplexItems(string methodName, bool shouldBeRequired) + { + var schema = GetReturnSchemaForMethod(methodName); + AssertComplexListReturnSchema(schema!, shouldBeRequired); + } + + [Theory] + [InlineData(nameof(TaskNullableListComplexTypeMethod), false)] + [InlineData(nameof(NullableTaskNullableListComplexTypeMethod), false)] + public void GetReturnSchema_TaskNullableListComplexType_UnwrapsToArraySchemaWithoutRequired(string methodName, bool shouldBeRequired) + { + var schema = GetReturnSchemaForMethod(methodName); + AssertComplexListReturnSchema(schema!, shouldBeRequired); + } + + [Theory] + [InlineData(nameof(TaskListNullableComplexTypeMethod), true)] + [InlineData(nameof(NullableTaskListNullableComplexTypeMethod), false)] + public void GetReturnSchema_TaskListNullableComplexType_UnwrapsToArraySchemaWithNullableItems(string methodName, bool shouldBeRequired) + { + var schema = GetReturnSchemaForMethod(methodName); + AssertComplexListReturnSchema(schema!, shouldBeRequired, itemsAreNullable: true); + } + + [Theory] + [InlineData(nameof(TaskNullableListNullableComplexTypeMethod), false)] + [InlineData(nameof(NullableTaskNullableListNullableComplexTypeMethod), false)] + public void GetReturnSchema_TaskNullableListNullableComplexType_UnwrapsToArraySchemaWithNullableItemsWithoutRequired(string methodName, bool shouldBeRequired) + { + var schema = GetReturnSchemaForMethod(methodName); + AssertComplexListReturnSchema(schema!, shouldBeRequired, itemsAreNullable: true); + } + + #endregion + #region JustRef Parameter Tests [Fact] diff --git a/ReflectorNet.Tests/SchemaTests/SchemaTestBase.cs b/ReflectorNet.Tests/SchemaTests/SchemaTestBase.cs index 512b3a4c..baf25db1 100644 --- a/ReflectorNet.Tests/SchemaTests/SchemaTestBase.cs +++ b/ReflectorNet.Tests/SchemaTests/SchemaTestBase.cs @@ -222,6 +222,68 @@ protected void AssertArrayReturnSchema(JsonNode schema, string expectedItemType, } } + /// + /// Asserts that a List return type has the correct schema structure + /// + protected void AssertComplexListReturnSchema(JsonNode schema, bool shouldBeRequired = true, bool itemsAreNullable = false) + { + 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(JsonSchema.Result)); + + var resultNode = properties[JsonSchema.Result]!; + + if (resultNode is JsonObject resultSchema && resultSchema.ContainsKey(JsonSchema.Ref)) + { + // Result is a reference to a list definition + Assert.True(schema.AsObject().ContainsKey(JsonSchema.Defs)); + } + else if (resultNode is JsonObject resultInlineSchema) + { + // Result is an inline array schema + Assert.Equal(JsonSchema.Array, resultInlineSchema[JsonSchema.Type]?.ToString()); + Assert.True(resultInlineSchema.ContainsKey(JsonSchema.Items)); + + var items = resultInlineSchema[JsonSchema.Items]!; + + // Items should be either a reference to ComplexReturnType or an inline object + if (items is JsonObject itemsSchema) + { + if (itemsSchema.ContainsKey(JsonSchema.Ref)) + { + // Items are a reference to ComplexReturnType + Assert.Contains("ComplexReturnType", itemsSchema[JsonSchema.Ref]?.ToString()); + Assert.True(schema.AsObject().ContainsKey(JsonSchema.Defs)); + } + else if (itemsSchema.ContainsKey(JsonSchema.Type)) + { + // Items are inlined + Assert.Equal(JsonSchema.Object, itemsSchema[JsonSchema.Type]?.ToString()); + } + else + { + Assert.Fail("Items schema should contain either $ref or type"); + } + } + else + { + Assert.Fail("Expected items to be a schema object"); + } + } + else + { + Assert.Fail("Expected result to be a schema object"); + } + + if (!shouldBeRequired) + { + AssertResultNotRequired(schema); + } + } + #endregion } } From 5eebfa055970a1aca905e27052c9e40dd26bf990 Mon Sep 17 00:00:00 2001 From: Ivan Murzak Date: Thu, 16 Oct 2025 00:17:38 -0700 Subject: [PATCH 07/24] feat: add AssertResultRequired method for non-nullable return type schema validation --- .../SchemaTests/SchemaTestBase.cs | 30 +++++++++++++------ 1 file changed, 21 insertions(+), 9 deletions(-) diff --git a/ReflectorNet.Tests/SchemaTests/SchemaTestBase.cs b/ReflectorNet.Tests/SchemaTests/SchemaTestBase.cs index baf25db1..cb6f82c7 100644 --- a/ReflectorNet.Tests/SchemaTests/SchemaTestBase.cs +++ b/ReflectorNet.Tests/SchemaTests/SchemaTestBase.cs @@ -145,6 +145,18 @@ protected void AssertResultNotRequired(JsonNode schema) } } + /// + /// Asserts that "result" IS in the required array (for non-nullable types + /// + protected void AssertResultRequired(JsonNode schema) + { + if (schema.AsObject().ContainsKey(JsonSchema.Required)) + { + var required = schema[JsonSchema.Required]!.AsArray(); + Assert.Contains(required, r => r?.ToString() == JsonSchema.Result); + } + } + /// /// Asserts that a custom type return schema has the correct structure /// @@ -179,10 +191,10 @@ protected void AssertCustomTypeReturnSchema(JsonNode schema, string[] expectedPr "Result schema should contain either $ref or properties"); } - if (!shouldBeRequired) - { + if (shouldBeRequired) + AssertResultRequired(schema); + else AssertResultNotRequired(schema); - } } /// @@ -216,10 +228,10 @@ protected void AssertArrayReturnSchema(JsonNode schema, string expectedItemType, Assert.Fail("Expected result to be a schema object"); } - if (!shouldBeRequired) - { + if (shouldBeRequired) + AssertResultRequired(schema); + else AssertResultNotRequired(schema); - } } /// @@ -278,10 +290,10 @@ protected void AssertComplexListReturnSchema(JsonNode schema, bool shouldBeRequi Assert.Fail("Expected result to be a schema object"); } - if (!shouldBeRequired) - { + if (shouldBeRequired) + AssertResultRequired(schema); + else AssertResultNotRequired(schema); - } } #endregion From 39621f8c142f24e9ff75b9a1b3f8eb0d5ff83fd2 Mon Sep 17 00:00:00 2001 From: Ivan Murzak Date: Thu, 16 Oct 2025 00:20:17 -0700 Subject: [PATCH 08/24] feat: enhance GetReturnSchemaForMethod to log return schema details --- ReflectorNet.Tests/SchemaTests/SchemaTestBase.cs | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/ReflectorNet.Tests/SchemaTests/SchemaTestBase.cs b/ReflectorNet.Tests/SchemaTests/SchemaTestBase.cs index cb6f82c7..2d0feb8d 100644 --- a/ReflectorNet.Tests/SchemaTests/SchemaTestBase.cs +++ b/ReflectorNet.Tests/SchemaTests/SchemaTestBase.cs @@ -105,7 +105,13 @@ protected void TestMethodInputs_PropertyRefs(Reflector? reflector, MethodInfo me { var reflector = new Reflector(); var methodInfo = GetType().GetMethod(methodName, BindingFlags.NonPublic | BindingFlags.Instance)!; - return reflector.GetReturnSchema(methodInfo, justRef); + var schema = reflector.GetReturnSchema(methodInfo, justRef); + + _output.WriteLine($"Return schema for {methodName}:"); + _output.WriteLine(schema?.ToString() ?? "null"); + _output.WriteLine(""); + + return schema; } /// From 73db3b068ebf9664d69fde2dba4020a69ef0f0b7 Mon Sep 17 00:00:00 2001 From: Ivan Murzak Date: Thu, 16 Oct 2025 00:27:27 -0700 Subject: [PATCH 09/24] feat: add Echo method to WrapperClass for value return functionality --- ReflectorNet.Tests.OuterAssembly/Model/NestedClass.cs | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/ReflectorNet.Tests.OuterAssembly/Model/NestedClass.cs b/ReflectorNet.Tests.OuterAssembly/Model/NestedClass.cs index 3f179069..6a65096b 100644 --- a/ReflectorNet.Tests.OuterAssembly/Model/NestedClass.cs +++ b/ReflectorNet.Tests.OuterAssembly/Model/NestedClass.cs @@ -45,5 +45,10 @@ public class WrapperClass [JsonInclude] public T? ValueField; public T? ValueProperty { get; set; } + + public T Echo(T value) + { + return value; + } } } \ No newline at end of file From 6fbb2464296e2776131c9d43ef7cf3efa36fbde8 Mon Sep 17 00:00:00 2001 From: Ivan Murzak Date: Thu, 16 Oct 2025 00:39:30 -0700 Subject: [PATCH 10/24] feat: add Echo and EchoNullable methods to WrapperClass for return type testing --- ReflectorNet.Tests/Model/NestedClass.cs | 12 ++ .../SchemaTests/ReturnSchemaTests.cs | 108 ++++++++++++++++++ 2 files changed, 120 insertions(+) diff --git a/ReflectorNet.Tests/Model/NestedClass.cs b/ReflectorNet.Tests/Model/NestedClass.cs index e6510873..5b4519f1 100644 --- a/ReflectorNet.Tests/Model/NestedClass.cs +++ b/ReflectorNet.Tests/Model/NestedClass.cs @@ -52,5 +52,17 @@ public WrapperClass(T? valueField, T? valueProperty) ValueField = valueField; ValueProperty = valueProperty; } + + /// + /// Echo method that returns the provided value unchanged. + /// Used for testing return type schemas with various generic types. + /// + public T Echo(T value) => value; + + /// + /// Echo method that returns a nullable version of the provided value. + /// Used for testing return type schemas with nullable generic types. + /// + public T? EchoNullable(T? value) => value; } } \ No newline at end of file diff --git a/ReflectorNet.Tests/SchemaTests/ReturnSchemaTests.cs b/ReflectorNet.Tests/SchemaTests/ReturnSchemaTests.cs index 225a1b62..aaccbd46 100644 --- a/ReflectorNet.Tests/SchemaTests/ReturnSchemaTests.cs +++ b/ReflectorNet.Tests/SchemaTests/ReturnSchemaTests.cs @@ -1,6 +1,8 @@ using System; using System.Reflection; +using System.Text.Json.Nodes; using System.Threading.Tasks; +using com.IvanMurzak.ReflectorNet.Tests.Model; using com.IvanMurzak.ReflectorNet.Utils; using Xunit.Abstractions; @@ -450,5 +452,111 @@ public void GetReturnSchema_NullMethodInfo_ThrowsArgumentNullException() } #endregion + + #region WrapperClass Echo Method Tests + + /// + /// Helper to get return schema for a WrapperClass method + /// + private JsonNode? GetWrapperMethodReturnSchema(Type wrapperType, string methodName) + { + var reflector = new Reflector(); + var methodInfo = wrapperType.GetMethod(methodName, BindingFlags.Public | BindingFlags.Instance)!; + var schema = reflector.GetReturnSchema(methodInfo); + + _output.WriteLine($"Return schema for {wrapperType.GetTypeShortName()}.{methodName}:"); + _output.WriteLine(schema?.ToString() ?? "null"); + _output.WriteLine(""); + + return schema; + } + + [Theory] + [InlineData(typeof(string), nameof(WrapperClass.Echo), JsonSchema.String, false)] // string is reference type, T is nullable + [InlineData(typeof(int), nameof(WrapperClass.Echo), JsonSchema.Integer, true)] // int is value type, T is non-nullable + [InlineData(typeof(bool), nameof(WrapperClass.Echo), JsonSchema.Boolean, true)] // bool is value type, T is non-nullable + [InlineData(typeof(double), nameof(WrapperClass.Echo), JsonSchema.Number, true)] // double is value type, T is non-nullable + [InlineData(typeof(string), nameof(WrapperClass.EchoNullable), JsonSchema.String, false)] // string? is nullable reference + [InlineData(typeof(int), nameof(WrapperClass.EchoNullable), JsonSchema.Integer, true)] // int? (Nullable) is itself non-nullable struct + [InlineData(typeof(bool), nameof(WrapperClass.EchoNullable), JsonSchema.Boolean, true)] // bool? (Nullable) is itself non-nullable struct + [InlineData(typeof(double), nameof(WrapperClass.EchoNullable), JsonSchema.Number, true)] // double? (Nullable) is itself non-nullable struct + public void GetReturnSchema_WrapperEchoPrimitive_ReturnsCorrectSchema(Type genericType, string methodName, string expectedType, bool shouldBeRequired) + { + var wrapperType = typeof(WrapperClass<>).MakeGenericType(genericType); + var schema = GetWrapperMethodReturnSchema(wrapperType, methodName); + AssertPrimitiveReturnSchema(schema!, expectedType, shouldBeRequired); + } + + [Theory] + [InlineData(typeof(CustomReturnType), true)] + [InlineData(typeof(ComplexReturnType), true)] + public void GetReturnSchema_WrapperEchoCustomType_ReturnsCorrectSchema(Type genericType, bool shouldBeRequired) + { + var wrapperType = typeof(WrapperClass<>).MakeGenericType(genericType); + var schema = GetWrapperMethodReturnSchema(wrapperType, nameof(WrapperClass.Echo)); + + Assert.NotNull(schema); + Assert.Equal(JsonSchema.Object, schema[JsonSchema.Type]?.ToString()); + + if (shouldBeRequired) + AssertResultRequired(schema); + else + AssertResultNotRequired(schema); + } + + [Theory] + [InlineData(typeof(CustomReturnType), false)] + [InlineData(typeof(ComplexReturnType), false)] + public void GetReturnSchema_WrapperEchoNullableCustomType_ReturnsCorrectSchema(Type genericType, bool shouldBeRequired) + { + var wrapperType = typeof(WrapperClass<>).MakeGenericType(genericType); + var schema = GetWrapperMethodReturnSchema(wrapperType, nameof(WrapperClass.EchoNullable)); + + Assert.NotNull(schema); + Assert.Equal(JsonSchema.Object, schema[JsonSchema.Type]?.ToString()); + + if (shouldBeRequired) + AssertResultRequired(schema); + else + AssertResultNotRequired(schema); + } + + [Theory] + [InlineData(typeof(string[]), JsonSchema.String, true)] + [InlineData(typeof(int[]), JsonSchema.Integer, true)] + public void GetReturnSchema_WrapperEchoArray_ReturnsCorrectSchema(Type genericType, string expectedItemType, bool shouldBeRequired) + { + var wrapperType = typeof(WrapperClass<>).MakeGenericType(genericType); + var schema = GetWrapperMethodReturnSchema(wrapperType, nameof(WrapperClass.Echo)); + AssertArrayReturnSchema(schema!, expectedItemType, shouldBeRequired); + } + + [Theory] + [InlineData(typeof(string[]), JsonSchema.String, false)] + [InlineData(typeof(int[]), JsonSchema.Integer, false)] + public void GetReturnSchema_WrapperEchoNullableArray_ReturnsCorrectSchema(Type genericType, string expectedItemType, bool shouldBeRequired) + { + var wrapperType = typeof(WrapperClass<>).MakeGenericType(genericType); + var schema = GetWrapperMethodReturnSchema(wrapperType, nameof(WrapperClass.EchoNullable)); + AssertArrayReturnSchema(schema!, expectedItemType, shouldBeRequired); + } + + [Fact] + public void GetReturnSchema_WrapperEchoListComplex_ReturnsCorrectSchema() + { + var wrapperType = typeof(WrapperClass>); + var schema = GetWrapperMethodReturnSchema(wrapperType, nameof(WrapperClass.Echo)); + AssertComplexListReturnSchema(schema!, shouldBeRequired: true); + } + + [Fact] + public void GetReturnSchema_WrapperEchoNullableListComplex_ReturnsCorrectSchema() + { + var wrapperType = typeof(WrapperClass>); + var schema = GetWrapperMethodReturnSchema(wrapperType, nameof(WrapperClass.EchoNullable)); + AssertComplexListReturnSchema(schema!, shouldBeRequired: false); + } + + #endregion } } From 2126ad5ece1a50c65b560c46879403042a0cd003 Mon Sep 17 00:00:00 2001 From: Ivan Murzak Date: Thu, 16 Oct 2025 00:41:56 -0700 Subject: [PATCH 11/24] feat: add Address, Company, and Person classes for model representation --- .../Model/Complex_Address.cs | 11 +++++++ .../Model/Complex_Company.cs | 14 ++++++++ .../Model/Complex_Person.cs | 32 +++++++++++++++++++ 3 files changed, 57 insertions(+) create mode 100644 ReflectorNet.Tests.OuterAssembly/Model/Complex_Address.cs create mode 100644 ReflectorNet.Tests.OuterAssembly/Model/Complex_Company.cs create mode 100644 ReflectorNet.Tests.OuterAssembly/Model/Complex_Person.cs diff --git a/ReflectorNet.Tests.OuterAssembly/Model/Complex_Address.cs b/ReflectorNet.Tests.OuterAssembly/Model/Complex_Address.cs new file mode 100644 index 00000000..5cea346b --- /dev/null +++ b/ReflectorNet.Tests.OuterAssembly/Model/Complex_Address.cs @@ -0,0 +1,11 @@ +// Complex class: Address with multiple properties +namespace com.IvanMurzak.ReflectorNet.OuterAssembly.Model +{ + public class Address + { + public string Street { get; set; } = string.Empty; + public string City { get; set; } = string.Empty; + public string? Zip { get; set; } + public string Country { get; set; } = string.Empty; + } +} diff --git a/ReflectorNet.Tests.OuterAssembly/Model/Complex_Company.cs b/ReflectorNet.Tests.OuterAssembly/Model/Complex_Company.cs new file mode 100644 index 00000000..388061fd --- /dev/null +++ b/ReflectorNet.Tests.OuterAssembly/Model/Complex_Company.cs @@ -0,0 +1,14 @@ +// Complex class: Company with nested collections and references +using System.Collections.Generic; + +namespace com.IvanMurzak.ReflectorNet.OuterAssembly.Model +{ + public class Company + { + public string Name { get; set; } = string.Empty; + public Address? Headquarters { get; set; } + public List Employees { get; } = new List(); + public Dictionary> Teams { get; } = new Dictionary>(); + public Dictionary> Directory { get; } = new(); + } +} diff --git a/ReflectorNet.Tests.OuterAssembly/Model/Complex_Person.cs b/ReflectorNet.Tests.OuterAssembly/Model/Complex_Person.cs new file mode 100644 index 00000000..b9c1cc69 --- /dev/null +++ b/ReflectorNet.Tests.OuterAssembly/Model/Complex_Person.cs @@ -0,0 +1,32 @@ +// Complex class: Person with mixed fields/properties and nested types +using System; +using System.Collections.Generic; + +namespace com.IvanMurzak.ReflectorNet.OuterAssembly.Model +{ + public class Person + { + // Fields + public string FirstName = string.Empty; + public string LastName = string.Empty; + private DateTime _birthDate; + + // Properties + public int Age { get; set; } + public Address? Address { get; set; } + public List Tags { get; } = new List(); + public Dictionary Scores { get; } = new Dictionary(); + + // Arrays + public int[] Numbers { get; set; } = Array.Empty(); + public string[][] JaggedAliases { get; set; } = Array.Empty(); + public int[,] Matrix2x2 { get; set; } = new int[2, 2]; + + // Methods using primitives and collections + public void SetBirthDate(DateTime date) => _birthDate = date; + public DateTime GetBirthDate() => _birthDate; + + public void AddTag(string tag) => Tags.Add(tag); + public void SetScore(string key, int value) => Scores[key] = value; + } +} From 5d669d6514aa22875157e788379ec8f9092246e1 Mon Sep 17 00:00:00 2001 From: Ivan Murzak Date: Thu, 16 Oct 2025 00:48:52 -0700 Subject: [PATCH 12/24] feat: add tests for OuterAssembly types in ReturnSchemaTests to validate unwrapping and schema generation --- .../SchemaTests/ReturnSchemaTests.cs | 204 ++++++++++++++++++ 1 file changed, 204 insertions(+) diff --git a/ReflectorNet.Tests/SchemaTests/ReturnSchemaTests.cs b/ReflectorNet.Tests/SchemaTests/ReturnSchemaTests.cs index aaccbd46..d7271ea5 100644 --- a/ReflectorNet.Tests/SchemaTests/ReturnSchemaTests.cs +++ b/ReflectorNet.Tests/SchemaTests/ReturnSchemaTests.cs @@ -5,6 +5,9 @@ using com.IvanMurzak.ReflectorNet.Tests.Model; using com.IvanMurzak.ReflectorNet.Utils; using Xunit.Abstractions; +using OuterPerson = com.IvanMurzak.ReflectorNet.OuterAssembly.Model.Person; +using OuterAddress = com.IvanMurzak.ReflectorNet.OuterAssembly.Model.Address; +using OuterCompany = com.IvanMurzak.ReflectorNet.OuterAssembly.Model.Company; namespace com.IvanMurzak.ReflectorNet.Tests.SchemaTests { @@ -38,15 +41,24 @@ private void VoidMethod() { } private Task TaskIntMethod() => Task.FromResult(42); private Task TaskBoolMethod() => Task.FromResult(true); private Task TaskCustomTypeMethod() => Task.FromResult(new CustomReturnType()); + private Task TaskOuterPersonMethod() => Task.FromResult(new OuterPerson()); + private Task TaskOuterAddressMethod() => Task.FromResult(new OuterAddress()); + private Task TaskOuterCompanyMethod() => Task.FromResult(new OuterCompany()); // ValueTask return types (should be unwrapped) private ValueTask ValueTaskStringMethod() => ValueTask.FromResult("test"); private ValueTask ValueTaskIntMethod() => ValueTask.FromResult(42); private ValueTask ValueTaskCustomTypeMethod() => ValueTask.FromResult(new CustomReturnType()); + private ValueTask ValueTaskOuterPersonMethod() => ValueTask.FromResult(new OuterPerson()); + private ValueTask ValueTaskOuterAddressMethod() => ValueTask.FromResult(new OuterAddress()); + private ValueTask ValueTaskOuterCompanyMethod() => ValueTask.FromResult(new OuterCompany()); // Custom types private CustomReturnType CustomTypeMethod() => new CustomReturnType(); private ComplexReturnType ComplexTypeMethod() => new ComplexReturnType(); + private OuterPerson OuterPersonMethod() => new OuterPerson(); + private OuterAddress OuterAddressMethod() => new OuterAddress(); + private OuterCompany OuterCompanyMethod() => new OuterCompany(); // Collections private string[] StringArrayMethod() => new[] { "test" }; @@ -75,6 +87,9 @@ private void VoidMethod() { } private DateTime? NullableDateTimeMethod() => DateTime.Now; private CustomReturnType? NullableCustomTypeMethod() => new CustomReturnType(); private ComplexReturnType? NullableComplexTypeMethod() => new ComplexReturnType(); + private OuterPerson? NullableOuterPersonMethod() => new OuterPerson(); + private OuterAddress? NullableOuterAddressMethod() => new OuterAddress(); + private OuterCompany? NullableOuterCompanyMethod() => new OuterCompany(); private string[]? NullableStringArrayMethod() => new[] { "test" }; // Task return types (nullable wrapped in Task) @@ -82,16 +97,25 @@ private void VoidMethod() { } private Task TaskNullableIntMethod() => Task.FromResult(42); private Task TaskNullableBoolMethod() => Task.FromResult(true); private Task TaskNullableCustomTypeMethod() => Task.FromResult(new CustomReturnType()); + private Task TaskNullableOuterPersonMethod() => Task.FromResult(new OuterPerson()); + private Task TaskNullableOuterAddressMethod() => Task.FromResult(new OuterAddress()); + private Task TaskNullableOuterCompanyMethod() => Task.FromResult(new OuterCompany()); // ValueTask return types (nullable wrapped in ValueTask) private ValueTask ValueTaskNullableStringMethod() => ValueTask.FromResult("test"); private ValueTask ValueTaskNullableIntMethod() => ValueTask.FromResult(42); private ValueTask ValueTaskNullableCustomTypeMethod() => ValueTask.FromResult(new CustomReturnType()); + private ValueTask ValueTaskNullableOuterPersonMethod() => ValueTask.FromResult(new OuterPerson()); + private ValueTask ValueTaskNullableOuterAddressMethod() => ValueTask.FromResult(new OuterAddress()); + private ValueTask ValueTaskNullableOuterCompanyMethod() => ValueTask.FromResult(new OuterCompany()); // Task? return types (nullable T wrapped in nullable Task) private Task? NullableTaskNullableStringMethod() => Task.FromResult("test"); private Task? NullableTaskNullableIntMethod() => Task.FromResult(42); private Task? NullableTaskNullableCustomTypeMethod() => Task.FromResult(new CustomReturnType()); + private Task? NullableTaskNullableOuterPersonMethod() => Task.FromResult(new OuterPerson()); + private Task? NullableTaskNullableOuterAddressMethod() => Task.FromResult(new OuterAddress()); + private Task? NullableTaskNullableOuterCompanyMethod() => Task.FromResult(new OuterCompany()); // NOTE: ValueTask? is not a valid scenario because ValueTask is a struct, not a class // So it cannot be made nullable in the reference type sense. We removed these tests. @@ -180,6 +204,18 @@ public void GetReturnSchema_TaskCustomType_UnwrapsToCustomTypeSchema() AssertCustomTypeReturnSchema(schema!, new[] { "Name", "Value" }, shouldBeRequired: true); } + [Theory] + [InlineData(nameof(TaskOuterPersonMethod))] + [InlineData(nameof(TaskOuterAddressMethod))] + [InlineData(nameof(TaskOuterCompanyMethod))] + public void GetReturnSchema_TaskOuterAssemblyType_UnwrapsCorrectly(string methodName) + { + var schema = GetReturnSchemaForMethod(methodName); + Assert.NotNull(schema); + Assert.Equal(JsonSchema.Object, schema[JsonSchema.Type]?.ToString()); + AssertResultRequired(schema); + } + #endregion #region Task Nullable Unwrapping Tests @@ -201,6 +237,18 @@ public void GetReturnSchema_TaskNullableCustomType_UnwrapsToCustomTypeSchemaWith AssertCustomTypeReturnSchema(schema!, new[] { "Name", "Value" }, shouldBeRequired: false); } + [Theory] + [InlineData(nameof(TaskNullableOuterPersonMethod))] + [InlineData(nameof(TaskNullableOuterAddressMethod))] + [InlineData(nameof(TaskNullableOuterCompanyMethod))] + public void GetReturnSchema_TaskNullableOuterAssemblyType_UnwrapsWithoutRequired(string methodName) + { + var schema = GetReturnSchemaForMethod(methodName); + Assert.NotNull(schema); + Assert.Equal(JsonSchema.Object, schema[JsonSchema.Type]?.ToString()); + AssertResultNotRequired(schema); + } + #endregion #region Nullable Task Unwrapping Tests @@ -221,6 +269,18 @@ public void GetReturnSchema_NullableTaskNullableCustomType_UnwrapsToCustomTypeSc AssertCustomTypeReturnSchema(schema!, new[] { "Name", "Value" }, shouldBeRequired: false); } + [Theory] + [InlineData(nameof(NullableTaskNullableOuterPersonMethod))] + [InlineData(nameof(NullableTaskNullableOuterAddressMethod))] + [InlineData(nameof(NullableTaskNullableOuterCompanyMethod))] + public void GetReturnSchema_NullableTaskNullableOuterAssemblyType_UnwrapsWithoutRequired(string methodName) + { + var schema = GetReturnSchemaForMethod(methodName); + Assert.NotNull(schema); + Assert.Equal(JsonSchema.Object, schema[JsonSchema.Type]?.ToString()); + AssertResultNotRequired(schema); + } + #endregion #region ValueTask Unwrapping Tests @@ -241,6 +301,18 @@ public void GetReturnSchema_ValueTaskCustomType_UnwrapsToCustomTypeSchema() AssertCustomTypeReturnSchema(schema!, new[] { "Name", "Value" }, shouldBeRequired: true); } + [Theory] + [InlineData(nameof(ValueTaskOuterPersonMethod))] + [InlineData(nameof(ValueTaskOuterAddressMethod))] + [InlineData(nameof(ValueTaskOuterCompanyMethod))] + public void GetReturnSchema_ValueTaskOuterAssemblyType_UnwrapsCorrectly(string methodName) + { + var schema = GetReturnSchemaForMethod(methodName); + Assert.NotNull(schema); + Assert.Equal(JsonSchema.Object, schema[JsonSchema.Type]?.ToString()); + AssertResultRequired(schema); + } + #endregion #region ValueTask Nullable Unwrapping Tests @@ -261,6 +333,18 @@ public void GetReturnSchema_ValueTaskNullableCustomType_UnwrapsToCustomTypeSchem AssertCustomTypeReturnSchema(schema!, new[] { "Name", "Value" }, shouldBeRequired: false); } + [Theory] + [InlineData(nameof(ValueTaskNullableOuterPersonMethod))] + [InlineData(nameof(ValueTaskNullableOuterAddressMethod))] + [InlineData(nameof(ValueTaskNullableOuterCompanyMethod))] + public void GetReturnSchema_ValueTaskNullableOuterAssemblyType_UnwrapsWithoutRequired(string methodName) + { + var schema = GetReturnSchemaForMethod(methodName); + Assert.NotNull(schema); + Assert.Equal(JsonSchema.Object, schema[JsonSchema.Type]?.ToString()); + AssertResultNotRequired(schema); + } + #endregion // NOTE: Nullable ValueTask tests removed because ValueTask is a struct and cannot be made nullable @@ -282,6 +366,33 @@ public void GetReturnSchema_ComplexType_ReturnsValidNestedSchema() AssertCustomTypeReturnSchema(schema!, new[] { "StringProperty", "IntProperty", "NestedObject", "StringArray" }, shouldBeRequired: true); } + [Fact] + public void GetReturnSchema_OuterPerson_ReturnsValidObjectSchema() + { + var schema = GetReturnSchemaForMethod(nameof(OuterPersonMethod)); + Assert.NotNull(schema); + Assert.Equal(JsonSchema.Object, schema[JsonSchema.Type]?.ToString()); + AssertResultRequired(schema); + } + + [Fact] + public void GetReturnSchema_OuterAddress_ReturnsValidObjectSchema() + { + var schema = GetReturnSchemaForMethod(nameof(OuterAddressMethod)); + Assert.NotNull(schema); + Assert.Equal(JsonSchema.Object, schema[JsonSchema.Type]?.ToString()); + AssertResultRequired(schema); + } + + [Fact] + public void GetReturnSchema_OuterCompany_ReturnsValidObjectSchema() + { + var schema = GetReturnSchemaForMethod(nameof(OuterCompanyMethod)); + Assert.NotNull(schema); + Assert.Equal(JsonSchema.Object, schema[JsonSchema.Type]?.ToString()); + AssertResultRequired(schema); + } + #endregion #region Nullable Custom Type Tests @@ -300,6 +411,33 @@ public void GetReturnSchema_NullableComplexType_ReturnsValidNestedSchemaWithoutR AssertCustomTypeReturnSchema(schema!, new[] { "StringProperty", "IntProperty", "NestedObject", "StringArray" }, shouldBeRequired: false); } + [Fact] + public void GetReturnSchema_NullableOuterPerson_ReturnsValidObjectSchemaWithoutRequired() + { + var schema = GetReturnSchemaForMethod(nameof(NullableOuterPersonMethod)); + Assert.NotNull(schema); + Assert.Equal(JsonSchema.Object, schema[JsonSchema.Type]?.ToString()); + AssertResultNotRequired(schema); + } + + [Fact] + public void GetReturnSchema_NullableOuterAddress_ReturnsValidObjectSchemaWithoutRequired() + { + var schema = GetReturnSchemaForMethod(nameof(NullableOuterAddressMethod)); + Assert.NotNull(schema); + Assert.Equal(JsonSchema.Object, schema[JsonSchema.Type]?.ToString()); + AssertResultNotRequired(schema); + } + + [Fact] + public void GetReturnSchema_NullableOuterCompany_ReturnsValidObjectSchemaWithoutRequired() + { + var schema = GetReturnSchemaForMethod(nameof(NullableOuterCompanyMethod)); + Assert.NotNull(schema); + Assert.Equal(JsonSchema.Object, schema[JsonSchema.Type]?.ToString()); + AssertResultNotRequired(schema); + } + #endregion #region Collection Tests @@ -437,6 +575,36 @@ public void GetReturnSchema_PrimitiveType_WithJustRef_ReturnsFullSchema() Assert.False(resultSchema.ContainsKey(JsonSchema.Ref)); } + [Theory] + [InlineData(nameof(OuterPersonMethod), "Person")] + [InlineData(nameof(OuterAddressMethod), "Address")] + [InlineData(nameof(OuterCompanyMethod), "Company")] + public void GetReturnSchema_OuterAssemblyType_WithJustRef_ReturnsRefSchema(string methodName, string expectedTypeName) + { + // Arrange + var reflector = new Reflector(); + var methodInfo = GetType().GetMethod(methodName, BindingFlags.NonPublic | BindingFlags.Instance)!; + + // Act + var schema = reflector.GetReturnSchema(methodInfo, justRef: true); + + // Assert + 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(JsonSchema.Result)); + + // The result property should contain a $ref to the OuterAssembly type + var resultSchema = properties[JsonSchema.Result]!.AsObject(); + Assert.True(resultSchema.ContainsKey(JsonSchema.Ref)); + Assert.Contains(expectedTypeName, resultSchema[JsonSchema.Ref]?.ToString()); + + // $defs should exist with the full type definition + Assert.True(schema.AsObject().ContainsKey(JsonSchema.Defs)); + } + #endregion #region Error Handling Tests @@ -557,6 +725,42 @@ public void GetReturnSchema_WrapperEchoNullableListComplex_ReturnsCorrectSchema( AssertComplexListReturnSchema(schema!, shouldBeRequired: false); } + [Theory] + [InlineData(typeof(OuterPerson), true)] + [InlineData(typeof(OuterAddress), true)] + [InlineData(typeof(OuterCompany), true)] + public void GetReturnSchema_WrapperEchoOuterAssemblyType_ReturnsCorrectSchema(Type genericType, bool shouldBeRequired) + { + var wrapperType = typeof(WrapperClass<>).MakeGenericType(genericType); + var schema = GetWrapperMethodReturnSchema(wrapperType, nameof(WrapperClass.Echo)); + + Assert.NotNull(schema); + Assert.Equal(JsonSchema.Object, schema[JsonSchema.Type]?.ToString()); + + if (shouldBeRequired) + AssertResultRequired(schema); + else + AssertResultNotRequired(schema); + } + + [Theory] + [InlineData(typeof(OuterPerson), false)] + [InlineData(typeof(OuterAddress), false)] + [InlineData(typeof(OuterCompany), false)] + public void GetReturnSchema_WrapperEchoNullableOuterAssemblyType_ReturnsCorrectSchema(Type genericType, bool shouldBeRequired) + { + var wrapperType = typeof(WrapperClass<>).MakeGenericType(genericType); + var schema = GetWrapperMethodReturnSchema(wrapperType, nameof(WrapperClass.EchoNullable)); + + Assert.NotNull(schema); + Assert.Equal(JsonSchema.Object, schema[JsonSchema.Type]?.ToString()); + + if (shouldBeRequired) + AssertResultRequired(schema); + else + AssertResultNotRequired(schema); + } + #endregion } } From 2183e6599dbc5f08a316fcd08a0e6b57cdf64993 Mon Sep 17 00:00:00 2001 From: Ivan Murzak Date: Thu, 16 Oct 2025 01:07:26 -0700 Subject: [PATCH 13/24] feat: add AssertResultDefines method to validate expected types in schema $defs section --- .../SchemaTests/ReturnSchemaTests.cs | 234 ++++++++++++++---- .../SchemaTests/SchemaTestBase.cs | 67 +++++ 2 files changed, 251 insertions(+), 50 deletions(-) diff --git a/ReflectorNet.Tests/SchemaTests/ReturnSchemaTests.cs b/ReflectorNet.Tests/SchemaTests/ReturnSchemaTests.cs index d7271ea5..0ec65459 100644 --- a/ReflectorNet.Tests/SchemaTests/ReturnSchemaTests.cs +++ b/ReflectorNet.Tests/SchemaTests/ReturnSchemaTests.cs @@ -204,16 +204,34 @@ public void GetReturnSchema_TaskCustomType_UnwrapsToCustomTypeSchema() AssertCustomTypeReturnSchema(schema!, new[] { "Name", "Value" }, shouldBeRequired: true); } - [Theory] - [InlineData(nameof(TaskOuterPersonMethod))] - [InlineData(nameof(TaskOuterAddressMethod))] - [InlineData(nameof(TaskOuterCompanyMethod))] - public void GetReturnSchema_TaskOuterAssemblyType_UnwrapsCorrectly(string methodName) + [Fact] + public void GetReturnSchema_TaskOuterPerson_UnwrapsCorrectly() { - var schema = GetReturnSchemaForMethod(methodName); + var schema = GetReturnSchemaForMethod(nameof(TaskOuterPersonMethod)); + Assert.NotNull(schema); + Assert.Equal(JsonSchema.Object, schema[JsonSchema.Type]?.ToString()); + AssertResultRequired(schema); + AssertResultDefines(schema, typeof(OuterPerson), typeof(OuterAddress)); + } + + [Fact] + public void GetReturnSchema_TaskOuterAddress_UnwrapsCorrectly() + { + var schema = GetReturnSchemaForMethod(nameof(TaskOuterAddressMethod)); + Assert.NotNull(schema); + Assert.Equal(JsonSchema.Object, schema[JsonSchema.Type]?.ToString()); + AssertResultRequired(schema); + AssertResultDefines(schema, typeof(OuterAddress)); + } + + [Fact] + public void GetReturnSchema_TaskOuterCompany_UnwrapsCorrectly() + { + var schema = GetReturnSchemaForMethod(nameof(TaskOuterCompanyMethod)); Assert.NotNull(schema); Assert.Equal(JsonSchema.Object, schema[JsonSchema.Type]?.ToString()); AssertResultRequired(schema); + AssertResultDefines(schema, typeof(OuterCompany), typeof(OuterAddress), typeof(OuterPerson)); } #endregion @@ -237,16 +255,34 @@ public void GetReturnSchema_TaskNullableCustomType_UnwrapsToCustomTypeSchemaWith AssertCustomTypeReturnSchema(schema!, new[] { "Name", "Value" }, shouldBeRequired: false); } - [Theory] - [InlineData(nameof(TaskNullableOuterPersonMethod))] - [InlineData(nameof(TaskNullableOuterAddressMethod))] - [InlineData(nameof(TaskNullableOuterCompanyMethod))] - public void GetReturnSchema_TaskNullableOuterAssemblyType_UnwrapsWithoutRequired(string methodName) + [Fact] + public void GetReturnSchema_TaskNullableOuterPerson_UnwrapsWithoutRequired() { - var schema = GetReturnSchemaForMethod(methodName); + var schema = GetReturnSchemaForMethod(nameof(TaskNullableOuterPersonMethod)); + Assert.NotNull(schema); + Assert.Equal(JsonSchema.Object, schema[JsonSchema.Type]?.ToString()); + AssertResultNotRequired(schema); + AssertResultDefines(schema, typeof(OuterPerson), typeof(OuterAddress)); + } + + [Fact] + public void GetReturnSchema_TaskNullableOuterAddress_UnwrapsWithoutRequired() + { + var schema = GetReturnSchemaForMethod(nameof(TaskNullableOuterAddressMethod)); + Assert.NotNull(schema); + Assert.Equal(JsonSchema.Object, schema[JsonSchema.Type]?.ToString()); + AssertResultNotRequired(schema); + AssertResultDefines(schema, typeof(OuterAddress)); + } + + [Fact] + public void GetReturnSchema_TaskNullableOuterCompany_UnwrapsWithoutRequired() + { + var schema = GetReturnSchemaForMethod(nameof(TaskNullableOuterCompanyMethod)); Assert.NotNull(schema); Assert.Equal(JsonSchema.Object, schema[JsonSchema.Type]?.ToString()); AssertResultNotRequired(schema); + AssertResultDefines(schema, typeof(OuterCompany), typeof(OuterAddress), typeof(OuterPerson)); } #endregion @@ -269,16 +305,34 @@ public void GetReturnSchema_NullableTaskNullableCustomType_UnwrapsToCustomTypeSc AssertCustomTypeReturnSchema(schema!, new[] { "Name", "Value" }, shouldBeRequired: false); } - [Theory] - [InlineData(nameof(NullableTaskNullableOuterPersonMethod))] - [InlineData(nameof(NullableTaskNullableOuterAddressMethod))] - [InlineData(nameof(NullableTaskNullableOuterCompanyMethod))] - public void GetReturnSchema_NullableTaskNullableOuterAssemblyType_UnwrapsWithoutRequired(string methodName) + [Fact] + public void GetReturnSchema_NullableTaskNullableOuterPerson_UnwrapsWithoutRequired() { - var schema = GetReturnSchemaForMethod(methodName); + var schema = GetReturnSchemaForMethod(nameof(NullableTaskNullableOuterPersonMethod)); + Assert.NotNull(schema); + Assert.Equal(JsonSchema.Object, schema[JsonSchema.Type]?.ToString()); + AssertResultNotRequired(schema); + AssertResultDefines(schema, typeof(OuterPerson), typeof(OuterAddress)); + } + + [Fact] + public void GetReturnSchema_NullableTaskNullableOuterAddress_UnwrapsWithoutRequired() + { + var schema = GetReturnSchemaForMethod(nameof(NullableTaskNullableOuterAddressMethod)); + Assert.NotNull(schema); + Assert.Equal(JsonSchema.Object, schema[JsonSchema.Type]?.ToString()); + AssertResultNotRequired(schema); + AssertResultDefines(schema, typeof(OuterAddress)); + } + + [Fact] + public void GetReturnSchema_NullableTaskNullableOuterCompany_UnwrapsWithoutRequired() + { + var schema = GetReturnSchemaForMethod(nameof(NullableTaskNullableOuterCompanyMethod)); Assert.NotNull(schema); Assert.Equal(JsonSchema.Object, schema[JsonSchema.Type]?.ToString()); AssertResultNotRequired(schema); + AssertResultDefines(schema, typeof(OuterCompany), typeof(OuterAddress), typeof(OuterPerson)); } #endregion @@ -301,16 +355,34 @@ public void GetReturnSchema_ValueTaskCustomType_UnwrapsToCustomTypeSchema() AssertCustomTypeReturnSchema(schema!, new[] { "Name", "Value" }, shouldBeRequired: true); } - [Theory] - [InlineData(nameof(ValueTaskOuterPersonMethod))] - [InlineData(nameof(ValueTaskOuterAddressMethod))] - [InlineData(nameof(ValueTaskOuterCompanyMethod))] - public void GetReturnSchema_ValueTaskOuterAssemblyType_UnwrapsCorrectly(string methodName) + [Fact] + public void GetReturnSchema_ValueTaskOuterPerson_UnwrapsCorrectly() { - var schema = GetReturnSchemaForMethod(methodName); + var schema = GetReturnSchemaForMethod(nameof(ValueTaskOuterPersonMethod)); + Assert.NotNull(schema); + Assert.Equal(JsonSchema.Object, schema[JsonSchema.Type]?.ToString()); + AssertResultRequired(schema); + AssertResultDefines(schema, typeof(OuterPerson), typeof(OuterAddress)); + } + + [Fact] + public void GetReturnSchema_ValueTaskOuterAddress_UnwrapsCorrectly() + { + var schema = GetReturnSchemaForMethod(nameof(ValueTaskOuterAddressMethod)); + Assert.NotNull(schema); + Assert.Equal(JsonSchema.Object, schema[JsonSchema.Type]?.ToString()); + AssertResultRequired(schema); + AssertResultDefines(schema, typeof(OuterAddress)); + } + + [Fact] + public void GetReturnSchema_ValueTaskOuterCompany_UnwrapsCorrectly() + { + var schema = GetReturnSchemaForMethod(nameof(ValueTaskOuterCompanyMethod)); Assert.NotNull(schema); Assert.Equal(JsonSchema.Object, schema[JsonSchema.Type]?.ToString()); AssertResultRequired(schema); + AssertResultDefines(schema, typeof(OuterCompany), typeof(OuterAddress), typeof(OuterPerson)); } #endregion @@ -333,16 +405,34 @@ public void GetReturnSchema_ValueTaskNullableCustomType_UnwrapsToCustomTypeSchem AssertCustomTypeReturnSchema(schema!, new[] { "Name", "Value" }, shouldBeRequired: false); } - [Theory] - [InlineData(nameof(ValueTaskNullableOuterPersonMethod))] - [InlineData(nameof(ValueTaskNullableOuterAddressMethod))] - [InlineData(nameof(ValueTaskNullableOuterCompanyMethod))] - public void GetReturnSchema_ValueTaskNullableOuterAssemblyType_UnwrapsWithoutRequired(string methodName) + [Fact] + public void GetReturnSchema_ValueTaskNullableOuterPerson_UnwrapsWithoutRequired() { - var schema = GetReturnSchemaForMethod(methodName); + var schema = GetReturnSchemaForMethod(nameof(ValueTaskNullableOuterPersonMethod)); + Assert.NotNull(schema); + Assert.Equal(JsonSchema.Object, schema[JsonSchema.Type]?.ToString()); + AssertResultNotRequired(schema); + AssertResultDefines(schema, typeof(OuterPerson), typeof(OuterAddress)); + } + + [Fact] + public void GetReturnSchema_ValueTaskNullableOuterAddress_UnwrapsWithoutRequired() + { + var schema = GetReturnSchemaForMethod(nameof(ValueTaskNullableOuterAddressMethod)); + Assert.NotNull(schema); + Assert.Equal(JsonSchema.Object, schema[JsonSchema.Type]?.ToString()); + AssertResultNotRequired(schema); + AssertResultDefines(schema, typeof(OuterAddress)); + } + + [Fact] + public void GetReturnSchema_ValueTaskNullableOuterCompany_UnwrapsWithoutRequired() + { + var schema = GetReturnSchemaForMethod(nameof(ValueTaskNullableOuterCompanyMethod)); Assert.NotNull(schema); Assert.Equal(JsonSchema.Object, schema[JsonSchema.Type]?.ToString()); AssertResultNotRequired(schema); + AssertResultDefines(schema, typeof(OuterCompany), typeof(OuterAddress), typeof(OuterPerson)); } #endregion @@ -364,6 +454,7 @@ public void GetReturnSchema_ComplexType_ReturnsValidNestedSchema() { var schema = GetReturnSchemaForMethod(nameof(ComplexTypeMethod)); AssertCustomTypeReturnSchema(schema!, new[] { "StringProperty", "IntProperty", "NestedObject", "StringArray" }, shouldBeRequired: true); + AssertResultDefines(schema!, typeof(ComplexReturnType), typeof(CustomReturnType)); } [Fact] @@ -373,6 +464,7 @@ public void GetReturnSchema_OuterPerson_ReturnsValidObjectSchema() Assert.NotNull(schema); Assert.Equal(JsonSchema.Object, schema[JsonSchema.Type]?.ToString()); AssertResultRequired(schema); + AssertResultDefines(schema, typeof(OuterPerson), typeof(OuterAddress)); } [Fact] @@ -382,6 +474,7 @@ public void GetReturnSchema_OuterAddress_ReturnsValidObjectSchema() Assert.NotNull(schema); Assert.Equal(JsonSchema.Object, schema[JsonSchema.Type]?.ToString()); AssertResultRequired(schema); + AssertResultDefines(schema, typeof(OuterAddress)); } [Fact] @@ -391,6 +484,7 @@ public void GetReturnSchema_OuterCompany_ReturnsValidObjectSchema() Assert.NotNull(schema); Assert.Equal(JsonSchema.Object, schema[JsonSchema.Type]?.ToString()); AssertResultRequired(schema); + AssertResultDefines(schema, typeof(OuterCompany), typeof(OuterAddress), typeof(OuterPerson)); } #endregion @@ -409,6 +503,7 @@ public void GetReturnSchema_NullableComplexType_ReturnsValidNestedSchemaWithoutR { var schema = GetReturnSchemaForMethod(nameof(NullableComplexTypeMethod)); AssertCustomTypeReturnSchema(schema!, new[] { "StringProperty", "IntProperty", "NestedObject", "StringArray" }, shouldBeRequired: false); + AssertResultDefines(schema!, typeof(ComplexReturnType), typeof(CustomReturnType)); } [Fact] @@ -418,6 +513,7 @@ public void GetReturnSchema_NullableOuterPerson_ReturnsValidObjectSchemaWithoutR Assert.NotNull(schema); Assert.Equal(JsonSchema.Object, schema[JsonSchema.Type]?.ToString()); AssertResultNotRequired(schema); + AssertResultDefines(schema, typeof(OuterPerson), typeof(OuterAddress)); } [Fact] @@ -427,6 +523,7 @@ public void GetReturnSchema_NullableOuterAddress_ReturnsValidObjectSchemaWithout Assert.NotNull(schema); Assert.Equal(JsonSchema.Object, schema[JsonSchema.Type]?.ToString()); AssertResultNotRequired(schema); + AssertResultDefines(schema, typeof(OuterAddress)); } [Fact] @@ -436,6 +533,7 @@ public void GetReturnSchema_NullableOuterCompany_ReturnsValidObjectSchemaWithout Assert.NotNull(schema); Assert.Equal(JsonSchema.Object, schema[JsonSchema.Type]?.ToString()); AssertResultNotRequired(schema); + AssertResultDefines(schema, typeof(OuterCompany), typeof(OuterAddress), typeof(OuterPerson)); } #endregion @@ -725,40 +823,76 @@ public void GetReturnSchema_WrapperEchoNullableListComplex_ReturnsCorrectSchema( AssertComplexListReturnSchema(schema!, shouldBeRequired: false); } - [Theory] - [InlineData(typeof(OuterPerson), true)] - [InlineData(typeof(OuterAddress), true)] - [InlineData(typeof(OuterCompany), true)] - public void GetReturnSchema_WrapperEchoOuterAssemblyType_ReturnsCorrectSchema(Type genericType, bool shouldBeRequired) + [Fact] + public void GetReturnSchema_WrapperEchoOuterPerson_ReturnsCorrectSchema() { - var wrapperType = typeof(WrapperClass<>).MakeGenericType(genericType); + var wrapperType = typeof(WrapperClass<>).MakeGenericType(typeof(OuterPerson)); var schema = GetWrapperMethodReturnSchema(wrapperType, nameof(WrapperClass.Echo)); Assert.NotNull(schema); Assert.Equal(JsonSchema.Object, schema[JsonSchema.Type]?.ToString()); + AssertResultRequired(schema); + AssertResultDefines(schema, typeof(OuterPerson), typeof(OuterAddress)); + } - if (shouldBeRequired) - AssertResultRequired(schema); - else - AssertResultNotRequired(schema); + [Fact] + public void GetReturnSchema_WrapperEchoOuterAddress_ReturnsCorrectSchema() + { + var wrapperType = typeof(WrapperClass<>).MakeGenericType(typeof(OuterAddress)); + var schema = GetWrapperMethodReturnSchema(wrapperType, nameof(WrapperClass.Echo)); + + Assert.NotNull(schema); + Assert.Equal(JsonSchema.Object, schema[JsonSchema.Type]?.ToString()); + AssertResultRequired(schema); + AssertResultDefines(schema, typeof(OuterAddress)); } - [Theory] - [InlineData(typeof(OuterPerson), false)] - [InlineData(typeof(OuterAddress), false)] - [InlineData(typeof(OuterCompany), false)] - public void GetReturnSchema_WrapperEchoNullableOuterAssemblyType_ReturnsCorrectSchema(Type genericType, bool shouldBeRequired) + [Fact] + public void GetReturnSchema_WrapperEchoOuterCompany_ReturnsCorrectSchema() { - var wrapperType = typeof(WrapperClass<>).MakeGenericType(genericType); + var wrapperType = typeof(WrapperClass<>).MakeGenericType(typeof(OuterCompany)); + var schema = GetWrapperMethodReturnSchema(wrapperType, nameof(WrapperClass.Echo)); + + Assert.NotNull(schema); + Assert.Equal(JsonSchema.Object, schema[JsonSchema.Type]?.ToString()); + AssertResultRequired(schema); + AssertResultDefines(schema, typeof(OuterCompany), typeof(OuterAddress), typeof(OuterPerson)); + } + + [Fact] + public void GetReturnSchema_WrapperEchoNullableOuterPerson_ReturnsCorrectSchema() + { + var wrapperType = typeof(WrapperClass<>).MakeGenericType(typeof(OuterPerson)); var schema = GetWrapperMethodReturnSchema(wrapperType, nameof(WrapperClass.EchoNullable)); Assert.NotNull(schema); Assert.Equal(JsonSchema.Object, schema[JsonSchema.Type]?.ToString()); + AssertResultNotRequired(schema); + AssertResultDefines(schema, typeof(OuterPerson), typeof(OuterAddress)); + } - if (shouldBeRequired) - AssertResultRequired(schema); - else - AssertResultNotRequired(schema); + [Fact] + public void GetReturnSchema_WrapperEchoNullableOuterAddress_ReturnsCorrectSchema() + { + var wrapperType = typeof(WrapperClass<>).MakeGenericType(typeof(OuterAddress)); + var schema = GetWrapperMethodReturnSchema(wrapperType, nameof(WrapperClass.EchoNullable)); + + Assert.NotNull(schema); + Assert.Equal(JsonSchema.Object, schema[JsonSchema.Type]?.ToString()); + AssertResultNotRequired(schema); + AssertResultDefines(schema, typeof(OuterAddress)); + } + + [Fact] + public void GetReturnSchema_WrapperEchoNullableOuterCompany_ReturnsCorrectSchema() + { + var wrapperType = typeof(WrapperClass<>).MakeGenericType(typeof(OuterCompany)); + var schema = GetWrapperMethodReturnSchema(wrapperType, nameof(WrapperClass.EchoNullable)); + + Assert.NotNull(schema); + Assert.Equal(JsonSchema.Object, schema[JsonSchema.Type]?.ToString()); + AssertResultNotRequired(schema); + AssertResultDefines(schema, typeof(OuterCompany), typeof(OuterAddress), typeof(OuterPerson)); } #endregion diff --git a/ReflectorNet.Tests/SchemaTests/SchemaTestBase.cs b/ReflectorNet.Tests/SchemaTests/SchemaTestBase.cs index 2d0feb8d..f306ba2a 100644 --- a/ReflectorNet.Tests/SchemaTests/SchemaTestBase.cs +++ b/ReflectorNet.Tests/SchemaTests/SchemaTestBase.cs @@ -1,4 +1,5 @@ using System; +using System.Collections.Generic; using System.Linq; using System.Reflection; using System.Text.Json.Nodes; @@ -163,6 +164,72 @@ protected void AssertResultRequired(JsonNode schema) } } + /// + /// Asserts that all expected types are defined in the $defs section of the schema OR referenced within the schema. + /// This method recursively checks all $ref values in the schema to ensure nested types are properly referenced. + /// + protected void AssertResultDefines(JsonNode schema, params Type[] expectedTypes) + { + Assert.NotNull(schema); + Assert.True(schema.AsObject().ContainsKey(JsonSchema.Defs), "Schema should contain $defs section"); + + var defines = schema[JsonSchema.Defs]!.AsObject(); + Assert.NotNull(defines); + + // Collect all $ref values throughout the schema (includes nested references) + var allReferences = new HashSet(); + CollectAllReferences(schema, allReferences); + + foreach (var expectedType in expectedTypes) + { + var expectedTypeId = expectedType.GetTypeId(); + + // Check if the type is either: + // 1. Directly defined in $defs (exact match) + var isDirectlyDefined = defines.ContainsKey(expectedTypeId); + + // 2. Referenced somewhere in the schema (exact match in $ref) + var expectedRef = $"{JsonSchema.RefValue}{expectedTypeId}"; + var isReferenced = allReferences.Contains(expectedRef); + + // 3. Referenced as part of a generic type (e.g., List, Dictionary) + var isReferencedInGeneric = allReferences.Any(r => r.Contains(expectedTypeId)); + + Assert.True(isDirectlyDefined || isReferenced || isReferencedInGeneric, + $"Expected type '{expectedType.GetTypeShortName()}' with ID '{expectedTypeId}' should be either defined in $defs or referenced in schema. " + + $"Expected $ref: '{expectedRef}'. " + + $"Defined types: {string.Join(", ", defines.Select(d => d.Key))}. " + + $"Referenced types: {string.Join(", ", allReferences)}"); + } + } + + /// + /// Helper method to recursively collect all $ref values in a JSON schema + /// + private void CollectAllReferences(JsonNode? node, HashSet references) + { + if (node == null) return; + + if (node is JsonObject obj) + { + foreach (var kvp in obj) + { + if (kvp.Key == JsonSchema.Ref && kvp.Value != null) + { + references.Add(kvp.Value.ToString()); + } + CollectAllReferences(kvp.Value, references); + } + } + else if (node is JsonArray arr) + { + foreach (var item in arr) + { + CollectAllReferences(item, references); + } + } + } + /// /// Asserts that a custom type return schema has the correct structure /// From d0a5925269a83b3d6d53f029d69942082043c1d3 Mon Sep 17 00:00:00 2001 From: Ivan Murzak Date: Thu, 16 Oct 2025 01:37:03 -0700 Subject: [PATCH 14/24] fix: update schema validation logic to require both definition and reference for expected types --- .../SchemaTests/ReturnSchemaTests.cs | 16 ++++++++-------- ReflectorNet.Tests/SchemaTests/SchemaTestBase.cs | 7 ++----- 2 files changed, 10 insertions(+), 13 deletions(-) diff --git a/ReflectorNet.Tests/SchemaTests/ReturnSchemaTests.cs b/ReflectorNet.Tests/SchemaTests/ReturnSchemaTests.cs index 0ec65459..48cf8465 100644 --- a/ReflectorNet.Tests/SchemaTests/ReturnSchemaTests.cs +++ b/ReflectorNet.Tests/SchemaTests/ReturnSchemaTests.cs @@ -811,7 +811,7 @@ public void GetReturnSchema_WrapperEchoNullableArray_ReturnsCorrectSchema(Type g public void GetReturnSchema_WrapperEchoListComplex_ReturnsCorrectSchema() { var wrapperType = typeof(WrapperClass>); - var schema = GetWrapperMethodReturnSchema(wrapperType, nameof(WrapperClass.Echo)); + var schema = GetWrapperMethodReturnSchema(wrapperType, nameof(WrapperClass>.Echo)); AssertComplexListReturnSchema(schema!, shouldBeRequired: true); } @@ -819,7 +819,7 @@ public void GetReturnSchema_WrapperEchoListComplex_ReturnsCorrectSchema() public void GetReturnSchema_WrapperEchoNullableListComplex_ReturnsCorrectSchema() { var wrapperType = typeof(WrapperClass>); - var schema = GetWrapperMethodReturnSchema(wrapperType, nameof(WrapperClass.EchoNullable)); + var schema = GetWrapperMethodReturnSchema(wrapperType, nameof(WrapperClass>.EchoNullable)); AssertComplexListReturnSchema(schema!, shouldBeRequired: false); } @@ -827,7 +827,7 @@ public void GetReturnSchema_WrapperEchoNullableListComplex_ReturnsCorrectSchema( public void GetReturnSchema_WrapperEchoOuterPerson_ReturnsCorrectSchema() { var wrapperType = typeof(WrapperClass<>).MakeGenericType(typeof(OuterPerson)); - var schema = GetWrapperMethodReturnSchema(wrapperType, nameof(WrapperClass.Echo)); + var schema = GetWrapperMethodReturnSchema(wrapperType, nameof(WrapperClass.Echo)); Assert.NotNull(schema); Assert.Equal(JsonSchema.Object, schema[JsonSchema.Type]?.ToString()); @@ -839,7 +839,7 @@ public void GetReturnSchema_WrapperEchoOuterPerson_ReturnsCorrectSchema() public void GetReturnSchema_WrapperEchoOuterAddress_ReturnsCorrectSchema() { var wrapperType = typeof(WrapperClass<>).MakeGenericType(typeof(OuterAddress)); - var schema = GetWrapperMethodReturnSchema(wrapperType, nameof(WrapperClass.Echo)); + var schema = GetWrapperMethodReturnSchema(wrapperType, nameof(WrapperClass.Echo)); Assert.NotNull(schema); Assert.Equal(JsonSchema.Object, schema[JsonSchema.Type]?.ToString()); @@ -851,7 +851,7 @@ public void GetReturnSchema_WrapperEchoOuterAddress_ReturnsCorrectSchema() public void GetReturnSchema_WrapperEchoOuterCompany_ReturnsCorrectSchema() { var wrapperType = typeof(WrapperClass<>).MakeGenericType(typeof(OuterCompany)); - var schema = GetWrapperMethodReturnSchema(wrapperType, nameof(WrapperClass.Echo)); + var schema = GetWrapperMethodReturnSchema(wrapperType, nameof(WrapperClass.Echo)); Assert.NotNull(schema); Assert.Equal(JsonSchema.Object, schema[JsonSchema.Type]?.ToString()); @@ -863,7 +863,7 @@ public void GetReturnSchema_WrapperEchoOuterCompany_ReturnsCorrectSchema() public void GetReturnSchema_WrapperEchoNullableOuterPerson_ReturnsCorrectSchema() { var wrapperType = typeof(WrapperClass<>).MakeGenericType(typeof(OuterPerson)); - var schema = GetWrapperMethodReturnSchema(wrapperType, nameof(WrapperClass.EchoNullable)); + var schema = GetWrapperMethodReturnSchema(wrapperType, nameof(WrapperClass.EchoNullable)); Assert.NotNull(schema); Assert.Equal(JsonSchema.Object, schema[JsonSchema.Type]?.ToString()); @@ -875,7 +875,7 @@ public void GetReturnSchema_WrapperEchoNullableOuterPerson_ReturnsCorrectSchema( public void GetReturnSchema_WrapperEchoNullableOuterAddress_ReturnsCorrectSchema() { var wrapperType = typeof(WrapperClass<>).MakeGenericType(typeof(OuterAddress)); - var schema = GetWrapperMethodReturnSchema(wrapperType, nameof(WrapperClass.EchoNullable)); + var schema = GetWrapperMethodReturnSchema(wrapperType, nameof(WrapperClass.EchoNullable)); Assert.NotNull(schema); Assert.Equal(JsonSchema.Object, schema[JsonSchema.Type]?.ToString()); @@ -887,7 +887,7 @@ public void GetReturnSchema_WrapperEchoNullableOuterAddress_ReturnsCorrectSchema public void GetReturnSchema_WrapperEchoNullableOuterCompany_ReturnsCorrectSchema() { var wrapperType = typeof(WrapperClass<>).MakeGenericType(typeof(OuterCompany)); - var schema = GetWrapperMethodReturnSchema(wrapperType, nameof(WrapperClass.EchoNullable)); + var schema = GetWrapperMethodReturnSchema(wrapperType, nameof(WrapperClass.EchoNullable)); Assert.NotNull(schema); Assert.Equal(JsonSchema.Object, schema[JsonSchema.Type]?.ToString()); diff --git a/ReflectorNet.Tests/SchemaTests/SchemaTestBase.cs b/ReflectorNet.Tests/SchemaTests/SchemaTestBase.cs index f306ba2a..bb11b546 100644 --- a/ReflectorNet.Tests/SchemaTests/SchemaTestBase.cs +++ b/ReflectorNet.Tests/SchemaTests/SchemaTestBase.cs @@ -192,11 +192,8 @@ protected void AssertResultDefines(JsonNode schema, params Type[] expectedTypes) var expectedRef = $"{JsonSchema.RefValue}{expectedTypeId}"; var isReferenced = allReferences.Contains(expectedRef); - // 3. Referenced as part of a generic type (e.g., List, Dictionary) - var isReferencedInGeneric = allReferences.Any(r => r.Contains(expectedTypeId)); - - Assert.True(isDirectlyDefined || isReferenced || isReferencedInGeneric, - $"Expected type '{expectedType.GetTypeShortName()}' with ID '{expectedTypeId}' should be either defined in $defs or referenced in schema. " + + Assert.True(isDirectlyDefined && isReferenced, + $"Expected type '{expectedType.GetTypeShortName()}' with ID '{expectedTypeId}' should be both defined in $defs and referenced in schema. " + $"Expected $ref: '{expectedRef}'. " + $"Defined types: {string.Join(", ", defines.Select(d => d.Key))}. " + $"Referenced types: {string.Join(", ", allReferences)}"); From b6b1557fd11f489c915e8340223d27df4096dc88 Mon Sep 17 00:00:00 2001 From: Ivan Murzak Date: Thu, 16 Oct 2025 01:47:22 -0700 Subject: [PATCH 15/24] feat: enhance schema generation by recursively collecting nested non-primitive types --- ReflectorNet/src/Utils/Json/JsonSchema.cs | 85 ++++++++++++++++++----- 1 file changed, 67 insertions(+), 18 deletions(-) diff --git a/ReflectorNet/src/Utils/Json/JsonSchema.cs b/ReflectorNet/src/Utils/Json/JsonSchema.cs index 05d62358..8e33dce5 100644 --- a/ReflectorNet/src/Utils/Json/JsonSchema.cs +++ b/ReflectorNet/src/Utils/Json/JsonSchema.cs @@ -6,6 +6,7 @@ */ using System; +using System.Collections.Generic; using System.ComponentModel; using System.Reflection; using System.Text.Json.Nodes; @@ -190,33 +191,81 @@ public JsonNode GetSchema(Reflector reflector, Type type, bool justRef = false) else { schema = GetSchema(reflector, type, justRef: true); - var typeId = type.GetTypeId(); - defines ??= new JsonObject(); - if (!defines.ContainsKey(typeId)) + // Recursively collect all nested types + CollectNestedTypes(reflector, type, defines); + } + + return (schema, defines); + } + + /// + /// Recursively collects all nested non-primitive types from a given type and adds them to the definitions. + /// This method traverses through properties, fields, generic arguments, and collection item types + /// to find all types that need to be included in the $defs section. + /// + /// The Reflector instance used for type analysis. + /// The type to analyze for nested types. + /// The JsonObject to accumulate type definitions. + /// Set of already visited types to prevent infinite recursion. + private void CollectNestedTypes(Reflector reflector, Type type, JsonObject defines, HashSet? visitedTypes = null) + { + visitedTypes ??= new HashSet(); + + // Avoid infinite recursion + if (visitedTypes.Contains(type)) + return; + + visitedTypes.Add(type); + + // Skip primitive types + if (TypeUtils.IsPrimitive(type)) + return; + + var typeId = type.GetTypeId(); + + // Add the type definition if not already present + if (!defines.ContainsKey(typeId)) + { + var fullSchema = GetSchema(reflector, type, justRef: false); + defines[typeId] = fullSchema; + } + + // Handle generic type arguments (e.g., List, Dictionary) + foreach (var genericArgument in TypeUtils.GetGenericTypes(type)) + { + CollectNestedTypes(reflector, genericArgument, defines, visitedTypes); + } + + // Handle collection item types (e.g., T[], List, IEnumerable) + if (TypeUtils.IsIEnumerable(type)) + { + var itemType = TypeUtils.GetEnumerableItemType(type); + if (itemType != null) { - var fullSchema = GetSchema(reflector, type, justRef: false); - defines[typeId] = fullSchema; + CollectNestedTypes(reflector, itemType, defines, visitedTypes); } + } - // Add generic type parameters recursively if any - foreach (var genericArgument in TypeUtils.GetGenericTypes(type)) + // Handle properties and fields to find nested types + var properties = reflector.GetSerializableProperties(type); + if (properties != null) + { + foreach (var prop in properties) { - if (TypeUtils.IsPrimitive(genericArgument)) - continue; - - var genericTypeId = genericArgument.GetTypeId(); - if (defines.ContainsKey(genericTypeId)) - continue; - - var genericSchema = GetSchema(reflector, genericArgument, justRef: false); - if (genericSchema != null) - defines[genericTypeId] = genericSchema; + CollectNestedTypes(reflector, prop.PropertyType, defines, visitedTypes); } } - return (schema, defines); + var fields = reflector.GetSerializableFields(type); + if (fields != null) + { + foreach (var field in fields) + { + CollectNestedTypes(reflector, field.FieldType, defines, visitedTypes); + } + } } /// From 5c8684f9c1b855de8721c79460a978bb5d944688 Mon Sep 17 00:00:00 2001 From: Ivan Murzak Date: Thu, 16 Oct 2025 19:06:44 -0700 Subject: [PATCH 16/24] Refactor schema type ID handling and enhance tests - Updated method parameter type ID retrieval from GetTypeId to GetSchemaTypeId in SchemaTestBase. - Enhanced AssertResultDefines to filter expected types, ensuring only non-primitive and non-enum types are included in $defs. - Added comprehensive tests for GetSchemaTypeId covering various collection types, nullable types, and custom classes in TestSchemaTypeId. - Introduced JsonObjectBuilder for building JSON schema objects with methods for adding properties, definitions, and handling required fields. - Updated TypeUtils to include GetSchemaTypeId method for consistent schema type ID generation. - Modified converters (MethodDataConverter, SerializedMemberConverter, SerializedMemberListConverter) to use GetSchemaTypeId for static ID retrieval. - Adjusted JsonSchema to utilize GetSchemaTypeId for reference handling and type definitions. - Commented out a test in TestType to avoid issues with current assembly type resolution. --- .../Model/NestedClass.cs | 18 +- .../JsonConverterTests/JsonConverterTests.cs | 3 +- .../JsonConvertors/ObjectRefConverter.cs | 2 +- ...ializedMemberCustomDescriptionConverter.cs | 2 +- ReflectorNet.Tests/Model/NestedClass.cs | 68 --- .../ReflectorTests/DeserializationTests.cs | 2 +- .../ReflectorTests/SerializationTests.cs | 1 + .../SerializeDeserializationTests.cs | 1 + .../ReflectorTests/SerializePopulateTests.cs | 1 + .../SchemaTests/ReturnSchemaTests.cs | 264 ++++++----- .../SchemaTests/SchemaTestBase.cs | 26 +- .../SchemaTests/TestSchemaTypeId.cs | 428 ++++++++++++++++++ ReflectorNet.Tests/SchemaTests/TestType.cs | 12 +- ReflectorNet.Tests/Utils/JsonObjectBuilder.cs | 190 ++++++++ ReflectorNet.Tests/Utils/TestUtils.Types.cs | 2 +- .../src/Convertor/Json/MethodDataConverter.cs | 4 +- .../Json/SerializedMemberConverter.cs | 2 +- .../Json/SerializedMemberListConverter.cs | 2 +- ReflectorNet/src/Extension/ExtensionsType.cs | 1 + ReflectorNet/src/Utils/Json/JsonSchema.cs | 4 +- ReflectorNet/src/Utils/TypeUtils.Name.cs | 64 ++- 21 files changed, 871 insertions(+), 226 deletions(-) delete mode 100644 ReflectorNet.Tests/Model/NestedClass.cs create mode 100644 ReflectorNet.Tests/SchemaTests/TestSchemaTypeId.cs create mode 100644 ReflectorNet.Tests/Utils/JsonObjectBuilder.cs diff --git a/ReflectorNet.Tests.OuterAssembly/Model/NestedClass.cs b/ReflectorNet.Tests.OuterAssembly/Model/NestedClass.cs index 6a65096b..44661feb 100644 --- a/ReflectorNet.Tests.OuterAssembly/Model/NestedClass.cs +++ b/ReflectorNet.Tests.OuterAssembly/Model/NestedClass.cs @@ -46,9 +46,23 @@ public class WrapperClass public T? ValueField; public T? ValueProperty { get; set; } - public T Echo(T value) + public WrapperClass() { } + public WrapperClass(T? valueField, T? valueProperty) { - return value; + ValueField = valueField; + ValueProperty = valueProperty; } + + /// + /// Echo method that returns the provided value unchanged. + /// Used for testing return type schemas with various generic types. + /// + public T Echo(T value) => value; + + /// + /// Echo method that returns a nullable version of the provided value. + /// Used for testing return type schemas with nullable generic types. + /// + public T? EchoNullable(T? value) => value; } } \ No newline at end of file diff --git a/ReflectorNet.Tests/JsonConverterTests/JsonConverterTests.cs b/ReflectorNet.Tests/JsonConverterTests/JsonConverterTests.cs index e423837b..765c077b 100644 --- a/ReflectorNet.Tests/JsonConverterTests/JsonConverterTests.cs +++ b/ReflectorNet.Tests/JsonConverterTests/JsonConverterTests.cs @@ -1,4 +1,5 @@ -using com.IvanMurzak.ReflectorNet.Tests.Model; +using com.IvanMurzak.ReflectorNet.OuterAssembly.Model; +using com.IvanMurzak.ReflectorNet.Tests.Model; using Xunit.Abstractions; namespace com.IvanMurzak.ReflectorNet.Tests.Utils diff --git a/ReflectorNet.Tests/Model/JsonConvertors/ObjectRefConverter.cs b/ReflectorNet.Tests/Model/JsonConvertors/ObjectRefConverter.cs index b3e3df28..915a749e 100644 --- a/ReflectorNet.Tests/Model/JsonConvertors/ObjectRefConverter.cs +++ b/ReflectorNet.Tests/Model/JsonConvertors/ObjectRefConverter.cs @@ -9,7 +9,7 @@ namespace com.IvanMurzak.ReflectorNet.Tests.Model { public class ObjectRefConverter : JsonConverter, IJsonSchemaConverter { - public string Id => typeof(ObjectRef).GetTypeId(); + public string Id => typeof(ObjectRef).GetSchemaTypeId(); public JsonNode GetScheme() => new JsonObject { [JsonSchema.Type] = JsonSchema.Object, diff --git a/ReflectorNet.Tests/Model/JsonConvertors/SerializedMemberCustomDescriptionConverter.cs b/ReflectorNet.Tests/Model/JsonConvertors/SerializedMemberCustomDescriptionConverter.cs index 568bfe60..5f51e4e2 100644 --- a/ReflectorNet.Tests/Model/JsonConvertors/SerializedMemberCustomDescriptionConverter.cs +++ b/ReflectorNet.Tests/Model/JsonConvertors/SerializedMemberCustomDescriptionConverter.cs @@ -12,7 +12,7 @@ public class SerializedMemberCustomDescriptionConverter : JsonConverter TypeUtils.GetTypeId(); + public static string StaticId => TypeUtils.GetSchemaTypeId(); public static JsonNode Schema => new JsonObject { [JsonSchema.Type] = JsonSchema.Object, diff --git a/ReflectorNet.Tests/Model/NestedClass.cs b/ReflectorNet.Tests/Model/NestedClass.cs deleted file mode 100644 index 5b4519f1..00000000 --- a/ReflectorNet.Tests/Model/NestedClass.cs +++ /dev/null @@ -1,68 +0,0 @@ - -using System.Text.Json.Serialization; - -namespace com.IvanMurzak.ReflectorNet.Tests.Model -{ - // Non static class with nested classes and static members - public class ParentClass - { - public class NestedClass - { - public static string NestedStaticField = "I am static field"; - public static string NestedStaticProperty { get; set; } = "I am static property"; - - [JsonInclude] - public string NestedField = "I am field"; - public string NestedProperty { get; set; } = "I am property"; - } - public static class NestedStaticClass - { - public static string NestedStaticField = "I am static field"; - public static string NestedStaticProperty { get; set; } = "I am static property"; - } - } - // Static class with nested classes and static members - public static class StaticParentClass - { - public class NestedClass - { - public static string NestedStaticField = "I am static field"; - public static string NestedStaticProperty { get; set; } = "I am static property"; - - [JsonInclude] - public string NestedField = "I am field"; - public string NestedProperty { get; set; } = "I am property"; - } - public static class NestedStaticClass - { - public static string NestedStaticField = "I am static field"; - public static string NestedStaticProperty { get; set; } = "I am static property"; - } - } - // Wrapper class - public class WrapperClass - { - [JsonInclude] - public T? ValueField; - public T? ValueProperty { get; set; } - - public WrapperClass() { } - public WrapperClass(T? valueField, T? valueProperty) - { - ValueField = valueField; - ValueProperty = valueProperty; - } - - /// - /// Echo method that returns the provided value unchanged. - /// Used for testing return type schemas with various generic types. - /// - public T Echo(T value) => value; - - /// - /// Echo method that returns a nullable version of the provided value. - /// Used for testing return type schemas with nullable generic types. - /// - public T? EchoNullable(T? value) => value; - } -} \ No newline at end of file diff --git a/ReflectorNet.Tests/ReflectorTests/DeserializationTests.cs b/ReflectorNet.Tests/ReflectorTests/DeserializationTests.cs index ea147c43..701fbc95 100644 --- a/ReflectorNet.Tests/ReflectorTests/DeserializationTests.cs +++ b/ReflectorNet.Tests/ReflectorTests/DeserializationTests.cs @@ -1,10 +1,10 @@ using System.Text; using com.IvanMurzak.ReflectorNet.Utils; -using com.IvanMurzak.ReflectorNet.Tests.Model; using Xunit.Abstractions; using com.IvanMurzak.ReflectorNet.Model; using System.Collections.Generic; using System; +using com.IvanMurzak.ReflectorNet.OuterAssembly.Model; namespace com.IvanMurzak.ReflectorNet.Tests.Utils { diff --git a/ReflectorNet.Tests/ReflectorTests/SerializationTests.cs b/ReflectorNet.Tests/ReflectorTests/SerializationTests.cs index 5f0f5e84..bc91bf51 100644 --- a/ReflectorNet.Tests/ReflectorTests/SerializationTests.cs +++ b/ReflectorNet.Tests/ReflectorTests/SerializationTests.cs @@ -2,6 +2,7 @@ using com.IvanMurzak.ReflectorNet.Utils; using com.IvanMurzak.ReflectorNet.Tests.Model; using Xunit.Abstractions; +using com.IvanMurzak.ReflectorNet.OuterAssembly.Model; namespace com.IvanMurzak.ReflectorNet.Tests.Utils { diff --git a/ReflectorNet.Tests/ReflectorTests/SerializeDeserializationTests.cs b/ReflectorNet.Tests/ReflectorTests/SerializeDeserializationTests.cs index 4abda6c5..7bdfc0b8 100644 --- a/ReflectorNet.Tests/ReflectorTests/SerializeDeserializationTests.cs +++ b/ReflectorNet.Tests/ReflectorTests/SerializeDeserializationTests.cs @@ -2,6 +2,7 @@ using com.IvanMurzak.ReflectorNet.Tests.Model; using Xunit.Abstractions; using System; +using com.IvanMurzak.ReflectorNet.OuterAssembly.Model; namespace com.IvanMurzak.ReflectorNet.Tests.Utils { diff --git a/ReflectorNet.Tests/ReflectorTests/SerializePopulateTests.cs b/ReflectorNet.Tests/ReflectorTests/SerializePopulateTests.cs index 1345a7bc..b7138bbf 100644 --- a/ReflectorNet.Tests/ReflectorTests/SerializePopulateTests.cs +++ b/ReflectorNet.Tests/ReflectorTests/SerializePopulateTests.cs @@ -2,6 +2,7 @@ using com.IvanMurzak.ReflectorNet.Tests.Model; using Xunit.Abstractions; using System; +using com.IvanMurzak.ReflectorNet.OuterAssembly.Model; namespace com.IvanMurzak.ReflectorNet.Tests.Utils { diff --git a/ReflectorNet.Tests/SchemaTests/ReturnSchemaTests.cs b/ReflectorNet.Tests/SchemaTests/ReturnSchemaTests.cs index 48cf8465..d60a1aee 100644 --- a/ReflectorNet.Tests/SchemaTests/ReturnSchemaTests.cs +++ b/ReflectorNet.Tests/SchemaTests/ReturnSchemaTests.cs @@ -2,12 +2,10 @@ using System.Reflection; using System.Text.Json.Nodes; using System.Threading.Tasks; -using com.IvanMurzak.ReflectorNet.Tests.Model; using com.IvanMurzak.ReflectorNet.Utils; using Xunit.Abstractions; -using OuterPerson = com.IvanMurzak.ReflectorNet.OuterAssembly.Model.Person; -using OuterAddress = com.IvanMurzak.ReflectorNet.OuterAssembly.Model.Address; -using OuterCompany = com.IvanMurzak.ReflectorNet.OuterAssembly.Model.Company; +using com.IvanMurzak.ReflectorNet.OuterAssembly.Model; +using System.Collections.Generic; namespace com.IvanMurzak.ReflectorNet.Tests.SchemaTests { @@ -41,43 +39,43 @@ private void VoidMethod() { } private Task TaskIntMethod() => Task.FromResult(42); private Task TaskBoolMethod() => Task.FromResult(true); private Task TaskCustomTypeMethod() => Task.FromResult(new CustomReturnType()); - private Task TaskOuterPersonMethod() => Task.FromResult(new OuterPerson()); - private Task TaskOuterAddressMethod() => Task.FromResult(new OuterAddress()); - private Task TaskOuterCompanyMethod() => Task.FromResult(new OuterCompany()); + private Task TaskPersonMethod() => Task.FromResult(new Person()); + private Task
TaskAddressMethod() => Task.FromResult(new Address()); + private Task TaskCompanyMethod() => Task.FromResult(new Company()); // ValueTask return types (should be unwrapped) private ValueTask ValueTaskStringMethod() => ValueTask.FromResult("test"); private ValueTask ValueTaskIntMethod() => ValueTask.FromResult(42); private ValueTask ValueTaskCustomTypeMethod() => ValueTask.FromResult(new CustomReturnType()); - private ValueTask ValueTaskOuterPersonMethod() => ValueTask.FromResult(new OuterPerson()); - private ValueTask ValueTaskOuterAddressMethod() => ValueTask.FromResult(new OuterAddress()); - private ValueTask ValueTaskOuterCompanyMethod() => ValueTask.FromResult(new OuterCompany()); + private ValueTask ValueTaskPersonMethod() => ValueTask.FromResult(new Person()); + private ValueTask
ValueTaskAddressMethod() => ValueTask.FromResult(new Address()); + private ValueTask ValueTaskCompanyMethod() => ValueTask.FromResult(new Company()); // Custom types private CustomReturnType CustomTypeMethod() => new CustomReturnType(); private ComplexReturnType ComplexTypeMethod() => new ComplexReturnType(); - private OuterPerson OuterPersonMethod() => new OuterPerson(); - private OuterAddress OuterAddressMethod() => new OuterAddress(); - private OuterCompany OuterCompanyMethod() => new OuterCompany(); + private Person PersonMethod() => new Person(); + private Address AddressMethod() => new Address(); + private Company CompanyMethod() => new Company(); // Collections private string[] StringArrayMethod() => new[] { "test" }; - private System.Collections.Generic.List ListIntMethod() => new System.Collections.Generic.List(); - private System.Collections.Generic.Dictionary DictionaryMethod() => new System.Collections.Generic.Dictionary(); + private List ListIntMethod() => new List(); + private Dictionary DictionaryMethod() => new Dictionary(); // List variations - private System.Collections.Generic.List ListComplexTypeMethod() => new System.Collections.Generic.List(); - private System.Collections.Generic.List ListNullableComplexTypeMethod() => new System.Collections.Generic.List(); - private System.Collections.Generic.List? NullableListComplexTypeMethod() => new System.Collections.Generic.List(); - private System.Collections.Generic.List? NullableListNullableComplexTypeMethod() => new System.Collections.Generic.List(); - private Task> TaskListComplexTypeMethod() => Task.FromResult(new System.Collections.Generic.List()); - private Task>? NullableTaskListComplexTypeMethod() => Task.FromResult(new System.Collections.Generic.List()); - private Task?> TaskNullableListComplexTypeMethod() => Task.FromResult?>(new System.Collections.Generic.List()); - private Task> TaskListNullableComplexTypeMethod() => Task.FromResult(new System.Collections.Generic.List()); - private Task?>? NullableTaskNullableListComplexTypeMethod() => Task.FromResult?>(new System.Collections.Generic.List()); - private Task?> TaskNullableListNullableComplexTypeMethod() => Task.FromResult?>(new System.Collections.Generic.List()); - private Task>? NullableTaskListNullableComplexTypeMethod() => Task.FromResult(new System.Collections.Generic.List()); - private Task?>? NullableTaskNullableListNullableComplexTypeMethod() => Task.FromResult?>(new System.Collections.Generic.List()); + private List ListComplexTypeMethod() => new List(); + private List ListNullableComplexTypeMethod() => new List(); + private List? NullableListComplexTypeMethod() => new List(); + private List? NullableListNullableComplexTypeMethod() => new List(); + private Task> TaskListComplexTypeMethod() => Task.FromResult(new List()); + private Task>? NullableTaskListComplexTypeMethod() => Task.FromResult(new List()); + private Task?> TaskNullableListComplexTypeMethod() => Task.FromResult?>(new List()); + private Task> TaskListNullableComplexTypeMethod() => Task.FromResult(new List()); + private Task?>? NullableTaskNullableListComplexTypeMethod() => Task.FromResult?>(new List()); + private Task?> TaskNullableListNullableComplexTypeMethod() => Task.FromResult?>(new List()); + private Task>? NullableTaskListNullableComplexTypeMethod() => Task.FromResult(new List()); + private Task?>? NullableTaskNullableListNullableComplexTypeMethod() => Task.FromResult?>(new List()); // Nullable return types private string? NullableStringMethod() => "test"; @@ -87,9 +85,9 @@ private void VoidMethod() { } private DateTime? NullableDateTimeMethod() => DateTime.Now; private CustomReturnType? NullableCustomTypeMethod() => new CustomReturnType(); private ComplexReturnType? NullableComplexTypeMethod() => new ComplexReturnType(); - private OuterPerson? NullableOuterPersonMethod() => new OuterPerson(); - private OuterAddress? NullableOuterAddressMethod() => new OuterAddress(); - private OuterCompany? NullableOuterCompanyMethod() => new OuterCompany(); + private Person? NullablePersonMethod() => new Person(); + private Address? NullableAddressMethod() => new Address(); + private Company? NullableCompanyMethod() => new Company(); private string[]? NullableStringArrayMethod() => new[] { "test" }; // Task return types (nullable wrapped in Task) @@ -97,25 +95,25 @@ private void VoidMethod() { } private Task TaskNullableIntMethod() => Task.FromResult(42); private Task TaskNullableBoolMethod() => Task.FromResult(true); private Task TaskNullableCustomTypeMethod() => Task.FromResult(new CustomReturnType()); - private Task TaskNullableOuterPersonMethod() => Task.FromResult(new OuterPerson()); - private Task TaskNullableOuterAddressMethod() => Task.FromResult(new OuterAddress()); - private Task TaskNullableOuterCompanyMethod() => Task.FromResult(new OuterCompany()); + private Task TaskNullablePersonMethod() => Task.FromResult(new Person()); + private Task TaskNullableAddressMethod() => Task.FromResult(new Address()); + private Task TaskNullableCompanyMethod() => Task.FromResult(new Company()); // ValueTask return types (nullable wrapped in ValueTask) private ValueTask ValueTaskNullableStringMethod() => ValueTask.FromResult("test"); private ValueTask ValueTaskNullableIntMethod() => ValueTask.FromResult(42); private ValueTask ValueTaskNullableCustomTypeMethod() => ValueTask.FromResult(new CustomReturnType()); - private ValueTask ValueTaskNullableOuterPersonMethod() => ValueTask.FromResult(new OuterPerson()); - private ValueTask ValueTaskNullableOuterAddressMethod() => ValueTask.FromResult(new OuterAddress()); - private ValueTask ValueTaskNullableOuterCompanyMethod() => ValueTask.FromResult(new OuterCompany()); + private ValueTask ValueTaskNullablePersonMethod() => ValueTask.FromResult(new Person()); + private ValueTask ValueTaskNullableAddressMethod() => ValueTask.FromResult(new Address()); + private ValueTask ValueTaskNullableCompanyMethod() => ValueTask.FromResult(new Company()); // Task? return types (nullable T wrapped in nullable Task) private Task? NullableTaskNullableStringMethod() => Task.FromResult("test"); private Task? NullableTaskNullableIntMethod() => Task.FromResult(42); private Task? NullableTaskNullableCustomTypeMethod() => Task.FromResult(new CustomReturnType()); - private Task? NullableTaskNullableOuterPersonMethod() => Task.FromResult(new OuterPerson()); - private Task? NullableTaskNullableOuterAddressMethod() => Task.FromResult(new OuterAddress()); - private Task? NullableTaskNullableOuterCompanyMethod() => Task.FromResult(new OuterCompany()); + private Task? NullableTaskNullablePersonMethod() => Task.FromResult(new Person()); + private Task? NullableTaskNullableAddressMethod() => Task.FromResult(new Address()); + private Task? NullableTaskNullableCompanyMethod() => Task.FromResult(new Company()); // NOTE: ValueTask? is not a valid scenario because ValueTask is a struct, not a class // So it cannot be made nullable in the reference type sense. We removed these tests. @@ -205,33 +203,33 @@ public void GetReturnSchema_TaskCustomType_UnwrapsToCustomTypeSchema() } [Fact] - public void GetReturnSchema_TaskOuterPerson_UnwrapsCorrectly() + public void GetReturnSchema_TaskPerson_UnwrapsCorrectly() { - var schema = GetReturnSchemaForMethod(nameof(TaskOuterPersonMethod)); + var schema = GetReturnSchemaForMethod(nameof(TaskPersonMethod)); Assert.NotNull(schema); Assert.Equal(JsonSchema.Object, schema[JsonSchema.Type]?.ToString()); AssertResultRequired(schema); - AssertResultDefines(schema, typeof(OuterPerson), typeof(OuterAddress)); + AssertResultDefines(schema, typeof(Person), typeof(Address)); } [Fact] - public void GetReturnSchema_TaskOuterAddress_UnwrapsCorrectly() + public void GetReturnSchema_TaskAddress_UnwrapsCorrectly() { - var schema = GetReturnSchemaForMethod(nameof(TaskOuterAddressMethod)); + var schema = GetReturnSchemaForMethod(nameof(TaskAddressMethod)); Assert.NotNull(schema); Assert.Equal(JsonSchema.Object, schema[JsonSchema.Type]?.ToString()); AssertResultRequired(schema); - AssertResultDefines(schema, typeof(OuterAddress)); + AssertResultDefines(schema, typeof(Address)); } [Fact] - public void GetReturnSchema_TaskOuterCompany_UnwrapsCorrectly() + public void GetReturnSchema_TaskCompany_UnwrapsCorrectly() { - var schema = GetReturnSchemaForMethod(nameof(TaskOuterCompanyMethod)); + var schema = GetReturnSchemaForMethod(nameof(TaskCompanyMethod)); Assert.NotNull(schema); Assert.Equal(JsonSchema.Object, schema[JsonSchema.Type]?.ToString()); AssertResultRequired(schema); - AssertResultDefines(schema, typeof(OuterCompany), typeof(OuterAddress), typeof(OuterPerson)); + AssertResultDefines(schema, typeof(Company), typeof(Address), typeof(Person)); } #endregion @@ -256,33 +254,33 @@ public void GetReturnSchema_TaskNullableCustomType_UnwrapsToCustomTypeSchemaWith } [Fact] - public void GetReturnSchema_TaskNullableOuterPerson_UnwrapsWithoutRequired() + public void GetReturnSchema_TaskNullablePerson_UnwrapsWithoutRequired() { - var schema = GetReturnSchemaForMethod(nameof(TaskNullableOuterPersonMethod)); + var schema = GetReturnSchemaForMethod(nameof(TaskNullablePersonMethod)); Assert.NotNull(schema); Assert.Equal(JsonSchema.Object, schema[JsonSchema.Type]?.ToString()); AssertResultNotRequired(schema); - AssertResultDefines(schema, typeof(OuterPerson), typeof(OuterAddress)); + AssertResultDefines(schema, typeof(Person), typeof(Address)); } [Fact] - public void GetReturnSchema_TaskNullableOuterAddress_UnwrapsWithoutRequired() + public void GetReturnSchema_TaskNullableAddress_UnwrapsWithoutRequired() { - var schema = GetReturnSchemaForMethod(nameof(TaskNullableOuterAddressMethod)); + var schema = GetReturnSchemaForMethod(nameof(TaskNullableAddressMethod)); Assert.NotNull(schema); Assert.Equal(JsonSchema.Object, schema[JsonSchema.Type]?.ToString()); AssertResultNotRequired(schema); - AssertResultDefines(schema, typeof(OuterAddress)); + AssertResultDefines(schema, typeof(Address)); } [Fact] - public void GetReturnSchema_TaskNullableOuterCompany_UnwrapsWithoutRequired() + public void GetReturnSchema_TaskNullableCompany_UnwrapsWithoutRequired() { - var schema = GetReturnSchemaForMethod(nameof(TaskNullableOuterCompanyMethod)); + var schema = GetReturnSchemaForMethod(nameof(TaskNullableCompanyMethod)); Assert.NotNull(schema); Assert.Equal(JsonSchema.Object, schema[JsonSchema.Type]?.ToString()); AssertResultNotRequired(schema); - AssertResultDefines(schema, typeof(OuterCompany), typeof(OuterAddress), typeof(OuterPerson)); + AssertResultDefines(schema, typeof(Company), typeof(Address), typeof(Person)); } #endregion @@ -306,33 +304,33 @@ public void GetReturnSchema_NullableTaskNullableCustomType_UnwrapsToCustomTypeSc } [Fact] - public void GetReturnSchema_NullableTaskNullableOuterPerson_UnwrapsWithoutRequired() + public void GetReturnSchema_NullableTaskNullablePerson_UnwrapsWithoutRequired() { - var schema = GetReturnSchemaForMethod(nameof(NullableTaskNullableOuterPersonMethod)); + var schema = GetReturnSchemaForMethod(nameof(NullableTaskNullablePersonMethod)); Assert.NotNull(schema); Assert.Equal(JsonSchema.Object, schema[JsonSchema.Type]?.ToString()); AssertResultNotRequired(schema); - AssertResultDefines(schema, typeof(OuterPerson), typeof(OuterAddress)); + AssertResultDefines(schema, typeof(Person), typeof(Address)); } [Fact] - public void GetReturnSchema_NullableTaskNullableOuterAddress_UnwrapsWithoutRequired() + public void GetReturnSchema_NullableTaskNullableAddress_UnwrapsWithoutRequired() { - var schema = GetReturnSchemaForMethod(nameof(NullableTaskNullableOuterAddressMethod)); + var schema = GetReturnSchemaForMethod(nameof(NullableTaskNullableAddressMethod)); Assert.NotNull(schema); Assert.Equal(JsonSchema.Object, schema[JsonSchema.Type]?.ToString()); AssertResultNotRequired(schema); - AssertResultDefines(schema, typeof(OuterAddress)); + AssertResultDefines(schema, typeof(Address)); } [Fact] - public void GetReturnSchema_NullableTaskNullableOuterCompany_UnwrapsWithoutRequired() + public void GetReturnSchema_NullableTaskNullableCompany_UnwrapsWithoutRequired() { - var schema = GetReturnSchemaForMethod(nameof(NullableTaskNullableOuterCompanyMethod)); + var schema = GetReturnSchemaForMethod(nameof(NullableTaskNullableCompanyMethod)); Assert.NotNull(schema); Assert.Equal(JsonSchema.Object, schema[JsonSchema.Type]?.ToString()); AssertResultNotRequired(schema); - AssertResultDefines(schema, typeof(OuterCompany), typeof(OuterAddress), typeof(OuterPerson)); + AssertResultDefines(schema, typeof(Company), typeof(Address), typeof(Person)); } #endregion @@ -356,33 +354,33 @@ public void GetReturnSchema_ValueTaskCustomType_UnwrapsToCustomTypeSchema() } [Fact] - public void GetReturnSchema_ValueTaskOuterPerson_UnwrapsCorrectly() + public void GetReturnSchema_ValueTaskPerson_UnwrapsCorrectly() { - var schema = GetReturnSchemaForMethod(nameof(ValueTaskOuterPersonMethod)); + var schema = GetReturnSchemaForMethod(nameof(ValueTaskPersonMethod)); Assert.NotNull(schema); Assert.Equal(JsonSchema.Object, schema[JsonSchema.Type]?.ToString()); AssertResultRequired(schema); - AssertResultDefines(schema, typeof(OuterPerson), typeof(OuterAddress)); + AssertResultDefines(schema, typeof(Person), typeof(Address)); } [Fact] - public void GetReturnSchema_ValueTaskOuterAddress_UnwrapsCorrectly() + public void GetReturnSchema_ValueTaskAddress_UnwrapsCorrectly() { - var schema = GetReturnSchemaForMethod(nameof(ValueTaskOuterAddressMethod)); + var schema = GetReturnSchemaForMethod(nameof(ValueTaskAddressMethod)); Assert.NotNull(schema); Assert.Equal(JsonSchema.Object, schema[JsonSchema.Type]?.ToString()); AssertResultRequired(schema); - AssertResultDefines(schema, typeof(OuterAddress)); + AssertResultDefines(schema, typeof(Address)); } [Fact] - public void GetReturnSchema_ValueTaskOuterCompany_UnwrapsCorrectly() + public void GetReturnSchema_ValueTaskCompany_UnwrapsCorrectly() { - var schema = GetReturnSchemaForMethod(nameof(ValueTaskOuterCompanyMethod)); + var schema = GetReturnSchemaForMethod(nameof(ValueTaskCompanyMethod)); Assert.NotNull(schema); Assert.Equal(JsonSchema.Object, schema[JsonSchema.Type]?.ToString()); AssertResultRequired(schema); - AssertResultDefines(schema, typeof(OuterCompany), typeof(OuterAddress), typeof(OuterPerson)); + AssertResultDefines(schema, typeof(Company), typeof(Address), typeof(Person)); } #endregion @@ -406,33 +404,33 @@ public void GetReturnSchema_ValueTaskNullableCustomType_UnwrapsToCustomTypeSchem } [Fact] - public void GetReturnSchema_ValueTaskNullableOuterPerson_UnwrapsWithoutRequired() + public void GetReturnSchema_ValueTaskNullablePerson_UnwrapsWithoutRequired() { - var schema = GetReturnSchemaForMethod(nameof(ValueTaskNullableOuterPersonMethod)); + var schema = GetReturnSchemaForMethod(nameof(ValueTaskNullablePersonMethod)); Assert.NotNull(schema); Assert.Equal(JsonSchema.Object, schema[JsonSchema.Type]?.ToString()); AssertResultNotRequired(schema); - AssertResultDefines(schema, typeof(OuterPerson), typeof(OuterAddress)); + AssertResultDefines(schema, typeof(Person), typeof(Address)); } [Fact] - public void GetReturnSchema_ValueTaskNullableOuterAddress_UnwrapsWithoutRequired() + public void GetReturnSchema_ValueTaskNullableAddress_UnwrapsWithoutRequired() { - var schema = GetReturnSchemaForMethod(nameof(ValueTaskNullableOuterAddressMethod)); + var schema = GetReturnSchemaForMethod(nameof(ValueTaskNullableAddressMethod)); Assert.NotNull(schema); Assert.Equal(JsonSchema.Object, schema[JsonSchema.Type]?.ToString()); AssertResultNotRequired(schema); - AssertResultDefines(schema, typeof(OuterAddress)); + AssertResultDefines(schema, typeof(Address)); } [Fact] - public void GetReturnSchema_ValueTaskNullableOuterCompany_UnwrapsWithoutRequired() + public void GetReturnSchema_ValueTaskNullableCompany_UnwrapsWithoutRequired() { - var schema = GetReturnSchemaForMethod(nameof(ValueTaskNullableOuterCompanyMethod)); + var schema = GetReturnSchemaForMethod(nameof(ValueTaskNullableCompanyMethod)); Assert.NotNull(schema); Assert.Equal(JsonSchema.Object, schema[JsonSchema.Type]?.ToString()); AssertResultNotRequired(schema); - AssertResultDefines(schema, typeof(OuterCompany), typeof(OuterAddress), typeof(OuterPerson)); + AssertResultDefines(schema, typeof(Company), typeof(Address), typeof(Person)); } #endregion @@ -458,33 +456,33 @@ public void GetReturnSchema_ComplexType_ReturnsValidNestedSchema() } [Fact] - public void GetReturnSchema_OuterPerson_ReturnsValidObjectSchema() + public void GetReturnSchema_Person_ReturnsValidObjectSchema() { - var schema = GetReturnSchemaForMethod(nameof(OuterPersonMethod)); + var schema = GetReturnSchemaForMethod(nameof(PersonMethod)); Assert.NotNull(schema); Assert.Equal(JsonSchema.Object, schema[JsonSchema.Type]?.ToString()); AssertResultRequired(schema); - AssertResultDefines(schema, typeof(OuterPerson), typeof(OuterAddress)); + AssertResultDefines(schema, typeof(Person), typeof(Address)); } [Fact] - public void GetReturnSchema_OuterAddress_ReturnsValidObjectSchema() + public void GetReturnSchema_Address_ReturnsValidObjectSchema() { - var schema = GetReturnSchemaForMethod(nameof(OuterAddressMethod)); + var schema = GetReturnSchemaForMethod(nameof(AddressMethod)); Assert.NotNull(schema); Assert.Equal(JsonSchema.Object, schema[JsonSchema.Type]?.ToString()); AssertResultRequired(schema); - AssertResultDefines(schema, typeof(OuterAddress)); + AssertResultDefines(schema, typeof(Address)); } [Fact] - public void GetReturnSchema_OuterCompany_ReturnsValidObjectSchema() + public void GetReturnSchema_Company_ReturnsValidObjectSchema() { - var schema = GetReturnSchemaForMethod(nameof(OuterCompanyMethod)); + var schema = GetReturnSchemaForMethod(nameof(CompanyMethod)); Assert.NotNull(schema); Assert.Equal(JsonSchema.Object, schema[JsonSchema.Type]?.ToString()); AssertResultRequired(schema); - AssertResultDefines(schema, typeof(OuterCompany), typeof(OuterAddress), typeof(OuterPerson)); + AssertResultDefines(schema, typeof(Company), typeof(Address), typeof(Person)); } #endregion @@ -507,33 +505,33 @@ public void GetReturnSchema_NullableComplexType_ReturnsValidNestedSchemaWithoutR } [Fact] - public void GetReturnSchema_NullableOuterPerson_ReturnsValidObjectSchemaWithoutRequired() + public void GetReturnSchema_NullablePerson_ReturnsValidObjectSchemaWithoutRequired() { - var schema = GetReturnSchemaForMethod(nameof(NullableOuterPersonMethod)); + var schema = GetReturnSchemaForMethod(nameof(NullablePersonMethod)); Assert.NotNull(schema); Assert.Equal(JsonSchema.Object, schema[JsonSchema.Type]?.ToString()); AssertResultNotRequired(schema); - AssertResultDefines(schema, typeof(OuterPerson), typeof(OuterAddress)); + AssertResultDefines(schema, typeof(Person), typeof(Address)); } [Fact] - public void GetReturnSchema_NullableOuterAddress_ReturnsValidObjectSchemaWithoutRequired() + public void GetReturnSchema_NullableAddress_ReturnsValidObjectSchemaWithoutRequired() { - var schema = GetReturnSchemaForMethod(nameof(NullableOuterAddressMethod)); + var schema = GetReturnSchemaForMethod(nameof(NullableAddressMethod)); Assert.NotNull(schema); Assert.Equal(JsonSchema.Object, schema[JsonSchema.Type]?.ToString()); AssertResultNotRequired(schema); - AssertResultDefines(schema, typeof(OuterAddress)); + AssertResultDefines(schema, typeof(Address)); } [Fact] - public void GetReturnSchema_NullableOuterCompany_ReturnsValidObjectSchemaWithoutRequired() + public void GetReturnSchema_NullableCompany_ReturnsValidObjectSchemaWithoutRequired() { - var schema = GetReturnSchemaForMethod(nameof(NullableOuterCompanyMethod)); + var schema = GetReturnSchemaForMethod(nameof(NullableCompanyMethod)); Assert.NotNull(schema); Assert.Equal(JsonSchema.Object, schema[JsonSchema.Type]?.ToString()); AssertResultNotRequired(schema); - AssertResultDefines(schema, typeof(OuterCompany), typeof(OuterAddress), typeof(OuterPerson)); + AssertResultDefines(schema, typeof(Company), typeof(Address), typeof(Person)); } #endregion @@ -674,9 +672,9 @@ public void GetReturnSchema_PrimitiveType_WithJustRef_ReturnsFullSchema() } [Theory] - [InlineData(nameof(OuterPersonMethod), "Person")] - [InlineData(nameof(OuterAddressMethod), "Address")] - [InlineData(nameof(OuterCompanyMethod), "Company")] + [InlineData(nameof(PersonMethod), "Person")] + [InlineData(nameof(AddressMethod), "Address")] + [InlineData(nameof(CompanyMethod), "Company")] public void GetReturnSchema_OuterAssemblyType_WithJustRef_ReturnsRefSchema(string methodName, string expectedTypeName) { // Arrange @@ -810,89 +808,89 @@ public void GetReturnSchema_WrapperEchoNullableArray_ReturnsCorrectSchema(Type g [Fact] public void GetReturnSchema_WrapperEchoListComplex_ReturnsCorrectSchema() { - var wrapperType = typeof(WrapperClass>); - var schema = GetWrapperMethodReturnSchema(wrapperType, nameof(WrapperClass>.Echo)); + var wrapperType = typeof(WrapperClass>); + var schema = GetWrapperMethodReturnSchema(wrapperType, nameof(WrapperClass>.Echo)); AssertComplexListReturnSchema(schema!, shouldBeRequired: true); } [Fact] public void GetReturnSchema_WrapperEchoNullableListComplex_ReturnsCorrectSchema() { - var wrapperType = typeof(WrapperClass>); - var schema = GetWrapperMethodReturnSchema(wrapperType, nameof(WrapperClass>.EchoNullable)); + var wrapperType = typeof(WrapperClass>); + var schema = GetWrapperMethodReturnSchema(wrapperType, nameof(WrapperClass>.EchoNullable)); AssertComplexListReturnSchema(schema!, shouldBeRequired: false); } [Fact] - public void GetReturnSchema_WrapperEchoOuterPerson_ReturnsCorrectSchema() + public void GetReturnSchema_WrapperEchoPerson_ReturnsCorrectSchema() { - var wrapperType = typeof(WrapperClass<>).MakeGenericType(typeof(OuterPerson)); - var schema = GetWrapperMethodReturnSchema(wrapperType, nameof(WrapperClass.Echo)); + var wrapperType = typeof(WrapperClass<>).MakeGenericType(typeof(Person)); + var schema = GetWrapperMethodReturnSchema(wrapperType, nameof(WrapperClass.Echo)); Assert.NotNull(schema); Assert.Equal(JsonSchema.Object, schema[JsonSchema.Type]?.ToString()); AssertResultRequired(schema); - AssertResultDefines(schema, typeof(OuterPerson), typeof(OuterAddress)); + AssertResultDefines(schema, typeof(Person), typeof(Address)); } [Fact] - public void GetReturnSchema_WrapperEchoOuterAddress_ReturnsCorrectSchema() + public void GetReturnSchema_WrapperEchoAddress_ReturnsCorrectSchema() { - var wrapperType = typeof(WrapperClass<>).MakeGenericType(typeof(OuterAddress)); - var schema = GetWrapperMethodReturnSchema(wrapperType, nameof(WrapperClass.Echo)); + var wrapperType = typeof(WrapperClass<>).MakeGenericType(typeof(Address)); + var schema = GetWrapperMethodReturnSchema(wrapperType, nameof(WrapperClass
.Echo)); Assert.NotNull(schema); Assert.Equal(JsonSchema.Object, schema[JsonSchema.Type]?.ToString()); AssertResultRequired(schema); - AssertResultDefines(schema, typeof(OuterAddress)); + AssertResultDefines(schema, typeof(Address)); } [Fact] - public void GetReturnSchema_WrapperEchoOuterCompany_ReturnsCorrectSchema() + public void GetReturnSchema_WrapperEchoCompany_ReturnsCorrectSchema() { - var wrapperType = typeof(WrapperClass<>).MakeGenericType(typeof(OuterCompany)); - var schema = GetWrapperMethodReturnSchema(wrapperType, nameof(WrapperClass.Echo)); + var wrapperType = typeof(WrapperClass<>).MakeGenericType(typeof(Company)); + var schema = GetWrapperMethodReturnSchema(wrapperType, nameof(WrapperClass.Echo)); Assert.NotNull(schema); Assert.Equal(JsonSchema.Object, schema[JsonSchema.Type]?.ToString()); AssertResultRequired(schema); - AssertResultDefines(schema, typeof(OuterCompany), typeof(OuterAddress), typeof(OuterPerson)); + AssertResultDefines(schema, typeof(Company), typeof(Address), typeof(Person)); } [Fact] - public void GetReturnSchema_WrapperEchoNullableOuterPerson_ReturnsCorrectSchema() + public void GetReturnSchema_WrapperEchoNullablePerson_ReturnsCorrectSchema() { - var wrapperType = typeof(WrapperClass<>).MakeGenericType(typeof(OuterPerson)); - var schema = GetWrapperMethodReturnSchema(wrapperType, nameof(WrapperClass.EchoNullable)); + var wrapperType = typeof(WrapperClass<>).MakeGenericType(typeof(Person)); + var schema = GetWrapperMethodReturnSchema(wrapperType, nameof(WrapperClass.EchoNullable)); Assert.NotNull(schema); Assert.Equal(JsonSchema.Object, schema[JsonSchema.Type]?.ToString()); AssertResultNotRequired(schema); - AssertResultDefines(schema, typeof(OuterPerson), typeof(OuterAddress)); + AssertResultDefines(schema, typeof(Person), typeof(Address)); } [Fact] - public void GetReturnSchema_WrapperEchoNullableOuterAddress_ReturnsCorrectSchema() + public void GetReturnSchema_WrapperEchoNullableAddress_ReturnsCorrectSchema() { - var wrapperType = typeof(WrapperClass<>).MakeGenericType(typeof(OuterAddress)); - var schema = GetWrapperMethodReturnSchema(wrapperType, nameof(WrapperClass.EchoNullable)); + var wrapperType = typeof(WrapperClass<>).MakeGenericType(typeof(Address)); + var schema = GetWrapperMethodReturnSchema(wrapperType, nameof(WrapperClass
.EchoNullable)); Assert.NotNull(schema); Assert.Equal(JsonSchema.Object, schema[JsonSchema.Type]?.ToString()); AssertResultNotRequired(schema); - AssertResultDefines(schema, typeof(OuterAddress)); + AssertResultDefines(schema, typeof(Address)); } [Fact] - public void GetReturnSchema_WrapperEchoNullableOuterCompany_ReturnsCorrectSchema() + public void GetReturnSchema_WrapperEchoNullableCompany_ReturnsCorrectSchema() { - var wrapperType = typeof(WrapperClass<>).MakeGenericType(typeof(OuterCompany)); - var schema = GetWrapperMethodReturnSchema(wrapperType, nameof(WrapperClass.EchoNullable)); + var wrapperType = typeof(WrapperClass<>).MakeGenericType(typeof(Company)); + var schema = GetWrapperMethodReturnSchema(wrapperType, nameof(WrapperClass.EchoNullable)); Assert.NotNull(schema); Assert.Equal(JsonSchema.Object, schema[JsonSchema.Type]?.ToString()); AssertResultNotRequired(schema); - AssertResultDefines(schema, typeof(OuterCompany), typeof(OuterAddress), typeof(OuterPerson)); + AssertResultDefines(schema, typeof(Company), typeof(Address), typeof(Person)); } #endregion diff --git a/ReflectorNet.Tests/SchemaTests/SchemaTestBase.cs b/ReflectorNet.Tests/SchemaTests/SchemaTestBase.cs index bb11b546..8d998564 100644 --- a/ReflectorNet.Tests/SchemaTests/SchemaTestBase.cs +++ b/ReflectorNet.Tests/SchemaTests/SchemaTestBase.cs @@ -54,7 +54,7 @@ protected void TestMethodInputs_PropertyRefs(Reflector? reflector, MethodInfo me var methodParameter = methodInfo.GetParameters().FirstOrDefault(p => p.Name == parameterName); Assert.NotNull(methodParameter); - var typeId = methodParameter.ParameterType.GetTypeId(); + var typeId = methodParameter.ParameterType.GetSchemaTypeId(); var refString = $"{JsonSchema.RefValue}{typeId}"; var targetDefine = defines[typeId]; @@ -89,7 +89,7 @@ protected void TestMethodInputs_PropertyRefs(Reflector? reflector, MethodInfo me foreach (var expectedType in expectedTypes) { - var typeId = expectedType.GetTypeId(); + var typeId = expectedType.GetSchemaTypeId(); var targetDefine = defines[typeId]; Assert.NotNull(targetDefine); } @@ -167,6 +167,8 @@ protected void AssertResultRequired(JsonNode schema) /// /// Asserts that all expected types are defined in the $defs section of the schema OR referenced within the schema. /// This method recursively checks all $ref values in the schema to ensure nested types are properly referenced. + /// Only non-primitive and non-enum types should be included in $defs, as primitives and enums are inlined. + /// Verifies that at minimum the expected types are present (additional types may be included by the schema generator). /// protected void AssertResultDefines(JsonNode schema, params Type[] expectedTypes) { @@ -180,9 +182,13 @@ protected void AssertResultDefines(JsonNode schema, params Type[] expectedTypes) var allReferences = new HashSet(); CollectAllReferences(schema, allReferences); - foreach (var expectedType in expectedTypes) + // Filter expected types to only include non-primitive, non-enum types + var nonPrimitiveTypes = expectedTypes.Where(t => !TypeUtils.IsPrimitive(t) && !t.IsEnum).ToArray(); + + // Verify all expected types are present in $defs and referenced + foreach (var expectedType in nonPrimitiveTypes) { - var expectedTypeId = expectedType.GetTypeId(); + var expectedTypeId = expectedType.GetSchemaTypeId(); // Check if the type is either: // 1. Directly defined in $defs (exact match) @@ -198,6 +204,18 @@ protected void AssertResultDefines(JsonNode schema, params Type[] expectedTypes) $"Defined types: {string.Join(", ", defines.Select(d => d.Key))}. " + $"Referenced types: {string.Join(", ", allReferences)}"); } + + // Verify that if any of the expected types appear in $defs, they are not primitive or enum + foreach (var expectedType in expectedTypes) + { + var expectedTypeId = expectedType.GetSchemaTypeId(); + if (defines.ContainsKey(expectedTypeId)) + { + Assert.False(TypeUtils.IsPrimitive(expectedType) || expectedType.IsEnum, + $"Type '{expectedType.GetTypeShortName()}' with ID '{expectedTypeId}' is primitive or enum and should not be in $defs. " + + $"Primitives and enums should be inlined in the schema."); + } + } } /// diff --git a/ReflectorNet.Tests/SchemaTests/TestSchemaTypeId.cs b/ReflectorNet.Tests/SchemaTests/TestSchemaTypeId.cs new file mode 100644 index 00000000..a718637a --- /dev/null +++ b/ReflectorNet.Tests/SchemaTests/TestSchemaTypeId.cs @@ -0,0 +1,428 @@ +using System.Collections.Generic; +using com.IvanMurzak.ReflectorNet.Utils; +using Xunit.Abstractions; + +namespace com.IvanMurzak.ReflectorNet.Tests.SchemaTests +{ + public class TestSchemaTypeId : BaseTest + { + public TestSchemaTypeId(ITestOutputHelper output) : base(output) { } + + [Fact] + public void GetSchemaTypeId_SimpleArray_ShouldAppendArray() + { + // Arrange + var type = typeof(int[]); + + // Act + var result = type.GetSchemaTypeId(); + + // Assert + Assert.Equal($"System.Int32{TypeUtils.ArraySuffix}", result); + _output.WriteLine($"int[] -> {result}"); + } + + [Fact] + public void GetSchemaTypeId_NestedArray_ShouldAppendMultipleArrays() + { + // Arrange + var type = typeof(int[][]); + + // Act + var result = type.GetSchemaTypeId(); + + // Assert + Assert.Equal($"System.Int32{TypeUtils.ArraySuffix}{TypeUtils.ArraySuffix}", result); + _output.WriteLine($"int[][] -> {result}"); + } + + [Fact] + public void GetSchemaTypeId_TripleNestedArray_ShouldAppendThreeArrays() + { + // Arrange + var type = typeof(int[][][]); + + // Act + var result = type.GetSchemaTypeId(); + + // Assert + Assert.Equal($"System.Int32{TypeUtils.ArraySuffix}{TypeUtils.ArraySuffix}{TypeUtils.ArraySuffix}", result); + _output.WriteLine($"int[][][] -> {result}"); + } + + [Fact] + public void GetSchemaTypeId_StringArray_ShouldWorkForAnyType() + { + // Arrange + var type = typeof(string[]); + + // Act + var result = type.GetSchemaTypeId(); + + // Assert + Assert.Equal($"System.String{TypeUtils.ArraySuffix}", result); + _output.WriteLine($"string[] -> {result}"); + } + + [Fact] + public void GetSchemaTypeId_ListOfInt_ShouldNormalizeToArray() + { + // Arrange + var listType = typeof(List); + var arrayType = typeof(int[]); + + // Act + var listResult = listType.GetSchemaTypeId(); + var arrayResult = arrayType.GetSchemaTypeId(); + + // Assert + Assert.Equal(arrayResult, listResult); + Assert.Equal($"System.Int32{TypeUtils.ArraySuffix}", listResult); + _output.WriteLine($"List -> {listResult}"); + _output.WriteLine($"int[] -> {arrayResult}"); + _output.WriteLine($"Both should be equal: {listResult == arrayResult}"); + } + + [Fact] + public void GetSchemaTypeId_ListOfString_ShouldMatchStringArray() + { + // Arrange + var listType = typeof(List); + var arrayType = typeof(string[]); + + // Act + var listResult = listType.GetSchemaTypeId(); + var arrayResult = arrayType.GetSchemaTypeId(); + + // Assert + Assert.Equal(arrayResult, listResult); + Assert.Equal($"System.String{TypeUtils.ArraySuffix}", listResult); + _output.WriteLine($"List -> {listResult}"); + _output.WriteLine($"string[] -> {arrayResult}"); + } + + [Fact] + public void GetSchemaTypeId_ListOfIntArray_ShouldNormalizeToDoubleArray() + { + // Arrange + var type = typeof(List); + + // Act + var result = type.GetSchemaTypeId(); + + // Assert + Assert.Equal($"System.Int32{TypeUtils.ArraySuffix}{TypeUtils.ArraySuffix}", result); + _output.WriteLine($"List -> {result}"); + } + + [Fact] + public void GetSchemaTypeId_ListOfListOfInt_ShouldNormalizeToDoubleArray() + { + // Arrange + var type = typeof(List>); + + // Act + var result = type.GetSchemaTypeId(); + + // Assert + Assert.Equal($"System.Int32{TypeUtils.ArraySuffix}{TypeUtils.ArraySuffix}", result); + _output.WriteLine($"List> -> {result}"); + } + + [Fact] + public void GetSchemaTypeId_ListOfListOfInt_ShouldMatchIntDoubleArray() + { + // Arrange + var listType = typeof(List>); + var arrayType = typeof(int[][]); + + // Act + var listResult = listType.GetSchemaTypeId(); + var arrayResult = arrayType.GetSchemaTypeId(); + + // Assert + Assert.Equal(arrayResult, listResult); + Assert.Equal($"System.Int32{TypeUtils.ArraySuffix}{TypeUtils.ArraySuffix}", listResult); + _output.WriteLine($"List> -> {listResult}"); + _output.WriteLine($"int[][] -> {arrayResult}"); + } + + [Fact] + public void GetSchemaTypeId_ListOfDoubleNestedArray_ShouldAppendThreeArrays() + { + // Arrange + var type = typeof(List); + + // Act + var result = type.GetSchemaTypeId(); + + // Assert + Assert.Equal($"System.Int32{TypeUtils.ArraySuffix}{TypeUtils.ArraySuffix}{TypeUtils.ArraySuffix}", result); + _output.WriteLine($"List -> {result}"); + } + + [Fact] + public void GetSchemaTypeId_ListOfStringArray_ShouldWorkForAnyType() + { + // Arrange + var type = typeof(List); + + // Act + var result = type.GetSchemaTypeId(); + + // Assert + Assert.Equal($"System.String{TypeUtils.ArraySuffix}{TypeUtils.ArraySuffix}", result); + _output.WriteLine($"List -> {result}"); + } + + [Fact] + public void GetSchemaTypeId_ComplexNestedCollections_ShouldHandleDeepNesting() + { + // Arrange + var type = typeof(List[]>); + + // Act + var result = type.GetSchemaTypeId(); + + // Assert + Assert.Equal($"System.String{TypeUtils.ArraySuffix}{TypeUtils.ArraySuffix}{TypeUtils.ArraySuffix}{TypeUtils.ArraySuffix}", result); + _output.WriteLine($"List[]> -> {result}"); + } + + [Fact] + public void GetSchemaTypeId_NonCollectionType_ShouldReturnFullName() + { + // Arrange + var type = typeof(string); + + // Act + var result = type.GetSchemaTypeId(); + + // Assert + Assert.Equal("System.String", result); + _output.WriteLine($"string -> {result}"); + } + + [Fact] + public void GetSchemaTypeId_NullableType_ShouldHandleUnderlyingType() + { + // Arrange + var type = typeof(int?); + + // Act + var result = type.GetSchemaTypeId(); + + // Assert + Assert.Equal("System.Int32", result); + _output.WriteLine($"int? -> {result}"); + } + + [Fact] + public void GetSchemaTypeId_NullableArrayType_ShouldHandleUnderlyingArrayType() + { + // Arrange + var type = typeof(int?[]); + + // Act + var result = type.GetSchemaTypeId(); + + // Assert + Assert.Equal($"System.Int32{TypeUtils.ArraySuffix}", result); + _output.WriteLine($"int?[] -> {result}"); + } + + [Fact] + public void GetSchemaTypeId_IEnumerableOfInt_ShouldReturnGenericFormat() + { + // Arrange + var type = typeof(IEnumerable); + + // Act + var result = type.GetSchemaTypeId(); + + // Assert + Assert.Equal("System.Collections.Generic.IEnumerable", result); + _output.WriteLine($"IEnumerable -> {result}"); + } + + [Fact] + public void GetSchemaTypeId_ICollectionOfString_ShouldReturnGenericFormat() + { + // Arrange + var type = typeof(ICollection); + + // Act + var result = type.GetSchemaTypeId(); + + // Assert + Assert.Equal("System.Collections.Generic.ICollection", result); + _output.WriteLine($"ICollection -> {result}"); + } + + [Fact] + public void GetSchemaTypeId_IListOfInt_ShouldReturnGenericFormat() + { + // Arrange + var type = typeof(IList); + + // Act + var result = type.GetSchemaTypeId(); + + // Assert + Assert.Equal("System.Collections.Generic.IList", result); + _output.WriteLine($"IList -> {result}"); + } + + [Fact] + public void GetSchemaTypeId_HashSetOfInt_ShouldNormalizeToArray() + { + // Arrange + var type = typeof(HashSet); + + // Act + var result = type.GetSchemaTypeId(); + + // Assert + Assert.Equal($"System.Int32{TypeUtils.ArraySuffix}", result); + _output.WriteLine($"HashSet -> {result}"); + } + + [Fact] + public void GetSchemaTypeId_SortedSetOfString_ShouldNormalizeToArray() + { + // Arrange + var type = typeof(SortedSet); + + // Act + var result = type.GetSchemaTypeId(); + + // Assert + Assert.Equal($"System.String{TypeUtils.ArraySuffix}", result); + _output.WriteLine($"SortedSet -> {result}"); + } + + [Fact] + public void GetSchemaTypeId_LinkedListOfInt_ShouldNormalizeToArray() + { + // Arrange + var type = typeof(LinkedList); + + // Act + var result = type.GetSchemaTypeId(); + + // Assert + Assert.Equal($"System.Int32{TypeUtils.ArraySuffix}", result); + _output.WriteLine($"LinkedList -> {result}"); + } + + [Fact] + public void GetSchemaTypeId_QueueOfString_ShouldNormalizeToArray() + { + // Arrange + var type = typeof(Queue); + + // Act + var result = type.GetSchemaTypeId(); + + // Assert + Assert.Equal($"System.String{TypeUtils.ArraySuffix}", result); + _output.WriteLine($"Queue -> {result}"); + } + + [Fact] + public void GetSchemaTypeId_StackOfInt_ShouldNormalizeToArray() + { + // Arrange + var type = typeof(Stack); + + // Act + var result = type.GetSchemaTypeId(); + + // Assert + Assert.Equal($"System.Int32{TypeUtils.ArraySuffix}", result); + _output.WriteLine($"Stack -> {result}"); + } + + [Fact] + public void GetSchemaTypeId_NestedGenericCollections_AllShouldNormalize() + { + // Arrange + var listOfHashSet = typeof(List>); + var queueOfStack = typeof(Queue>); + var arrayOfLinkedList = typeof(LinkedList[]); + + // Act + var result1 = listOfHashSet.GetSchemaTypeId(); + var result2 = queueOfStack.GetSchemaTypeId(); + var result3 = arrayOfLinkedList.GetSchemaTypeId(); + + // Assert + Assert.Equal($"System.Int32{TypeUtils.ArraySuffix}{TypeUtils.ArraySuffix}", result1); + Assert.Equal($"System.String{TypeUtils.ArraySuffix}{TypeUtils.ArraySuffix}", result2); + Assert.Equal($"System.Double{TypeUtils.ArraySuffix}{TypeUtils.ArraySuffix}", result3); + _output.WriteLine($"List> -> {result1}"); + _output.WriteLine($"Queue> -> {result2}"); + _output.WriteLine($"LinkedList[] -> {result3}"); + } + + [Fact] + public void GetSchemaTypeId_CustomClass_ShouldReturnFullName() + { + // Arrange + var type = typeof(TestType); + + // Act + var result = type.GetSchemaTypeId(); + + // Assert + Assert.Equal("com.IvanMurzak.ReflectorNet.Tests.SchemaTests.TestType", result); + _output.WriteLine($"TestType -> {result}"); + } + + [Fact] + public void GetSchemaTypeId_ListOfCustomClass_ShouldNormalizeToArray() + { + // Arrange + var type = typeof(List); + + // Act + var result = type.GetSchemaTypeId(); + + // Assert + Assert.Equal($"com.IvanMurzak.ReflectorNet.Tests.SchemaTests.TestType{TypeUtils.ArraySuffix}", result); + _output.WriteLine($"List -> {result}"); + } + + [Fact] + public void GetSchemaTypeId_ArrayOfCustomClass_ShouldAppendArray() + { + // Arrange + var type = typeof(TestType[]); + + // Act + var result = type.GetSchemaTypeId(); + + // Assert + Assert.Equal($"com.IvanMurzak.ReflectorNet.Tests.SchemaTests.TestType{TypeUtils.ArraySuffix}", result); + _output.WriteLine($"TestType[] -> {result}"); + } + + [Fact] + public void GetSchemaTypeId_ListAndArrayOfCustomClass_ShouldMatch() + { + // Arrange + var listType = typeof(List); + var arrayType = typeof(TestType[]); + + // Act + var listResult = listType.GetSchemaTypeId(); + var arrayResult = arrayType.GetSchemaTypeId(); + + // Assert + Assert.Equal(arrayResult, listResult); + Assert.Equal($"com.IvanMurzak.ReflectorNet.Tests.SchemaTests.TestType{TypeUtils.ArraySuffix}", listResult); + _output.WriteLine($"List -> {listResult}"); + _output.WriteLine($"TestType[] -> {arrayResult}"); + } + } +} diff --git a/ReflectorNet.Tests/SchemaTests/TestType.cs b/ReflectorNet.Tests/SchemaTests/TestType.cs index 3bf00a36..34be46ea 100644 --- a/ReflectorNet.Tests/SchemaTests/TestType.cs +++ b/ReflectorNet.Tests/SchemaTests/TestType.cs @@ -66,12 +66,12 @@ void TestTypeName(Type type) Assert.Equal(type, TypeUtils.GetType(typeName)); } - [Fact] - public void GetTypeName_CurrentAssembly() - { - TestTypeName(typeof(Model.ParentClass)); - TestTypeName(typeof(Model.ParentClass.NestedClass)); - } + // [Fact] + // public void GetTypeName_CurrentAssembly() + // { + // TestTypeName(typeof(Model.ParentClass)); + // TestTypeName(typeof(Model.ParentClass.NestedClass)); + // } [Fact] public void GetTypeName_OuterAssembly() diff --git a/ReflectorNet.Tests/Utils/JsonObjectBuilder.cs b/ReflectorNet.Tests/Utils/JsonObjectBuilder.cs new file mode 100644 index 00000000..a6403457 --- /dev/null +++ b/ReflectorNet.Tests/Utils/JsonObjectBuilder.cs @@ -0,0 +1,190 @@ +using System; +using System.Text.Json; +using System.Text.Json.Nodes; +using com.IvanMurzak.ReflectorNet.Utils; + +namespace com.IvanMurzak.ReflectorNet.Tests.Model +{ + internal class JsonObjectBuilder + { + public JsonObject? Result { get; private set; } + + public JsonObjectBuilder() + { + Result = new JsonObject(); + } + + public JsonObjectBuilder SetNull() + { + Result = null; + return this; + } + + public JsonObjectBuilder SetTypeObject() + { + if (Result == null) + throw new InvalidOperationException("Cannot set type on a null schema."); + + Result[JsonSchema.Type] = JsonSchema.Object; + return this; + } + + public JsonObjectBuilder SetTypeArray() + { + if (Result == null) + throw new InvalidOperationException("Cannot set type on a null schema."); + + Result[JsonSchema.Type] = JsonSchema.Array; + return this; + } + + public JsonObjectBuilder AddRequired(string name) + { + if (Result == null) + throw new InvalidOperationException("Cannot add required property to a null schema."); + + if (Result[JsonSchema.Required] == null) + Result[JsonSchema.Required] = new JsonArray(); + + Result[JsonSchema.Required]!.AsArray().Add(name); + + return this; + } + + public JsonObjectBuilder AddSimpleProperty(string name, string type, bool required = false, string? description = null) + { + if (Result == null) + throw new InvalidOperationException("Cannot add property to a null schema."); + + if (Result[JsonSchema.Properties] == null) + Result[JsonSchema.Properties] = new JsonObject(); + + Result[JsonSchema.Properties]![name] = new JsonObject + { + [JsonSchema.Type] = type + }; + + if (required) + AddRequired(name); + + if (description != null) + Result[JsonSchema.Properties]![name]![JsonSchema.Description] = description; + + return this; + } + + public JsonObjectBuilder AddJsonElementProperty(string name, JsonObject? value, bool required = false, string? description = null) + { + if (Result == null) + throw new InvalidOperationException("Cannot add property to a null schema."); + + if (Result[JsonSchema.Properties] == null) + Result[JsonSchema.Properties] = new JsonObject(); + + Result[JsonSchema.Properties]![name] = value; + + if (required) + AddRequired(name); + + if (description != null) + Result[JsonSchema.Properties]![name]![JsonSchema.Description] = description; + + return this; + } + + public JsonObjectBuilder AddArrayProperty(string name, string itemType, bool required = false, string? description = null) + { + if (Result == null) + throw new InvalidOperationException("Cannot add property to a null schema."); + + if (Result[JsonSchema.Properties] == null) + Result[JsonSchema.Properties] = new JsonObject(); + + Result[JsonSchema.Properties]![name] = new JsonObject + { + [JsonSchema.Type] = JsonSchema.Array, + [JsonSchema.Items] = new JsonObject + { + [JsonSchema.Type] = itemType + } + }; + + if (required) + AddRequired(name); + + if (description != null) + Result[JsonSchema.Properties]![name]![JsonSchema.Description] = description; + + return this; + } + + public JsonObjectBuilder AddRefPropertyAndDefinition(string name, bool required, JsonObject? definition, string? description = null) where T : notnull + { + return AddRefProperty(name, TypeUtils.GetSchemaTypeId(), required, description) + .AddDefinition(TypeUtils.GetSchemaTypeId(), definition); + } + + public JsonObjectBuilder AddRefProperty(string name, bool required = false, string? description = null) where T : notnull + { + return AddRefProperty(name, TypeUtils.GetSchemaTypeId(), required, description); + } + + public JsonObjectBuilder AddRefProperty(string name, string typeId, bool required = false, string? description = null) + { + if (Result == null) + throw new InvalidOperationException("Cannot add property to a null schema."); + + if (Result[JsonSchema.Properties] == null) + Result[JsonSchema.Properties] = new JsonObject(); + + Result[JsonSchema.Properties]![name] = new JsonObject + { + [JsonSchema.Ref] = JsonSchema.RefValue + typeId + }; + + if (required) + AddRequired(name); + + if (description != null) + Result[JsonSchema.Properties]![name]![JsonSchema.Description] = description; + + return this; + } + + public JsonObjectBuilder AddDefinition(string name, JsonObject? definition) + { + if (Result == null) + throw new InvalidOperationException("Cannot add definition to a null schema."); + + if (Result[JsonSchema.Defs] == null) + Result[JsonSchema.Defs] = new JsonObject(); + + Result[JsonSchema.Defs]![name] = definition; + + return this; + } + + public JsonObjectBuilder AddArrayDefinition(string name, string itemType) + { + var arrayDefinition = new JsonObject + { + [JsonSchema.Type] = JsonSchema.Array, + [JsonSchema.Items] = new JsonObject + { + [JsonSchema.Type] = itemType + } + }; + + return AddDefinition(name, arrayDefinition); + } + + public JsonObject? BuildJsonObject() + { + return Result; + } + public JsonElement? BuildJsonElement() + { + return BuildJsonObject()?.ToJsonElement(); + } + } +} \ No newline at end of file diff --git a/ReflectorNet.Tests/Utils/TestUtils.Types.cs b/ReflectorNet.Tests/Utils/TestUtils.Types.cs index 9829584d..0bc93acc 100644 --- a/ReflectorNet.Tests/Utils/TestUtils.Types.cs +++ b/ReflectorNet.Tests/Utils/TestUtils.Types.cs @@ -1,9 +1,9 @@ using System; using System.Collections.Generic; -using System.Globalization; using System.Linq; using System.Reflection; using com.IvanMurzak.ReflectorNet.Model; +using com.IvanMurzak.ReflectorNet.OuterAssembly.Model; namespace com.IvanMurzak.ReflectorNet.Tests.Model { diff --git a/ReflectorNet/src/Convertor/Json/MethodDataConverter.cs b/ReflectorNet/src/Convertor/Json/MethodDataConverter.cs index e594d5c7..3dd9246f 100644 --- a/ReflectorNet/src/Convertor/Json/MethodDataConverter.cs +++ b/ReflectorNet/src/Convertor/Json/MethodDataConverter.cs @@ -20,7 +20,7 @@ namespace com.IvanMurzak.ReflectorNet.Json public class MethodDataConverter : JsonConverter, IJsonSchemaConverter { - public static string StaticId => TypeUtils.GetTypeId(); + public static string StaticId => TypeUtils.GetSchemaTypeId(); public static JsonNode Schema => new JsonObject { @@ -101,7 +101,7 @@ public class MethodDataConverter : JsonConverter, IJsonSchemaConvert [JsonSchema.Type] = JsonSchema.Array, [JsonSchema.Items] = new JsonObject { - [JsonSchema.Ref] = JsonSchema.RefValue + TypeUtils.GetTypeId() + [JsonSchema.Ref] = JsonSchema.RefValue + TypeUtils.GetSchemaTypeId() }, [JsonSchema.Description] = TypeUtils.GetDescription( typeof(MethodData) diff --git a/ReflectorNet/src/Convertor/Json/SerializedMemberConverter.cs b/ReflectorNet/src/Convertor/Json/SerializedMemberConverter.cs index 1b84c0a5..817b99ba 100644 --- a/ReflectorNet/src/Convertor/Json/SerializedMemberConverter.cs +++ b/ReflectorNet/src/Convertor/Json/SerializedMemberConverter.cs @@ -17,7 +17,7 @@ namespace com.IvanMurzak.ReflectorNet.Json { public class SerializedMemberConverter : JsonConverter, IJsonSchemaConverter { - public static string StaticId => TypeUtils.GetTypeId(); + public static string StaticId => TypeUtils.GetSchemaTypeId(); public static JsonNode Schema => new JsonObject { [JsonSchema.Type] = JsonSchema.Object, diff --git a/ReflectorNet/src/Convertor/Json/SerializedMemberListConverter.cs b/ReflectorNet/src/Convertor/Json/SerializedMemberListConverter.cs index 2bc8c291..7c7cf4c7 100644 --- a/ReflectorNet/src/Convertor/Json/SerializedMemberListConverter.cs +++ b/ReflectorNet/src/Convertor/Json/SerializedMemberListConverter.cs @@ -16,7 +16,7 @@ namespace com.IvanMurzak.ReflectorNet.Json { public class SerializedMemberListConverter : JsonConverter, IJsonSchemaConverter { - public static string StaticId => TypeUtils.GetTypeId(); + public static string StaticId => TypeUtils.GetSchemaTypeId(); public static JsonNode Schema => new JsonObject { [JsonSchema.Type] = JsonSchema.Array, diff --git a/ReflectorNet/src/Extension/ExtensionsType.cs b/ReflectorNet/src/Extension/ExtensionsType.cs index 9bbaf029..118546ea 100644 --- a/ReflectorNet/src/Extension/ExtensionsType.cs +++ b/ReflectorNet/src/Extension/ExtensionsType.cs @@ -17,6 +17,7 @@ public static class ExtensionsType public static string GetTypeShortName(this Type? type) => TypeUtils.GetTypeShortName(type); public static string GetTypeName(this Type? type, bool pretty = false) => TypeUtils.GetTypeName(type, pretty); public static string GetTypeId(this Type type) => TypeUtils.GetTypeId(type); + public static string GetSchemaTypeId(this Type type) => TypeUtils.GetSchemaTypeId(type); public static bool IsMatch(this Type? type, string? typeName) => TypeUtils.IsNameMatch(type, typeName); } } \ No newline at end of file diff --git a/ReflectorNet/src/Utils/Json/JsonSchema.cs b/ReflectorNet/src/Utils/Json/JsonSchema.cs index 8e33dce5..caef409f 100644 --- a/ReflectorNet/src/Utils/Json/JsonSchema.cs +++ b/ReflectorNet/src/Utils/Json/JsonSchema.cs @@ -120,7 +120,7 @@ public JsonNode GetSchema(Reflector reflector, Type type, bool justRef = false) // If justRef is true and the type is not primitive, we return a reference schema schema = new JsonObject { - [Ref] = RefValue + type.GetTypeId() + [Ref] = RefValue + type.GetSchemaTypeId() }; // Get description from the type if available @@ -223,7 +223,7 @@ private void CollectNestedTypes(Reflector reflector, Type type, JsonObject defin if (TypeUtils.IsPrimitive(type)) return; - var typeId = type.GetTypeId(); + var typeId = type.GetSchemaTypeId(); // Add the type definition if not already present if (!defines.ContainsKey(typeId)) diff --git a/ReflectorNet/src/Utils/TypeUtils.Name.cs b/ReflectorNet/src/Utils/TypeUtils.Name.cs index da3d2bd3..9020c411 100644 --- a/ReflectorNet/src/Utils/TypeUtils.Name.cs +++ b/ReflectorNet/src/Utils/TypeUtils.Name.cs @@ -1,4 +1,5 @@ using System; +using System.Collections.Generic; using System.Linq; namespace com.IvanMurzak.ReflectorNet.Utils @@ -32,7 +33,7 @@ public static string GetTypeId(Type type) // Special case: string is technically IEnumerable but shouldn't be treated as an array if (type == typeof(string)) - return type.GetTypeName(pretty: true); + return GetTypeName(type, pretty: true); // If type is a generic type, use its full name with generic arguments if (type.IsGenericType) @@ -58,8 +59,67 @@ public static string GetTypeId(Type type) return $"{GetTypeId(elementType)}{ArraySuffix}"; } - return type.GetTypeName(pretty: true); + return GetTypeName(type, pretty: true); } + + public static string GetSchemaTypeId() => GetSchemaTypeId(typeof(T)); + public static string GetSchemaTypeId(Type type) + { + if (type == null) + throw new ArgumentNullException(nameof(type)); + + // Handle nullable types + type = Nullable.GetUnderlyingType(type) ?? type; + + // Special case: string is technically IEnumerable but shouldn't be treated as an array + if (type == typeof(string)) + return GetTypeName(type, pretty: true); + + // If type is a generic type, use its full name with generic arguments + if (type.IsGenericType) + { + var genericTypes = type.GetGenericArguments(); + if (genericTypes.Length == 1) + { + if (typeof(System.Collections.IList).IsAssignableFrom(type) || + typeof(ISet<>).MakeGenericType(genericTypes).IsAssignableFrom(type) || + typeof(LinkedList<>).MakeGenericType(genericTypes).IsAssignableFrom(type) || + typeof(Queue<>).MakeGenericType(genericTypes).IsAssignableFrom(type) || + typeof(Stack<>).MakeGenericType(genericTypes).IsAssignableFrom(type) || + typeof(List<>).MakeGenericType(genericTypes).IsAssignableFrom(type) || + typeof(SortedSet<>).MakeGenericType(genericTypes).IsAssignableFrom(type)) + { + var itemType = genericTypes[0]; + if (itemType == null) + throw new InvalidOperationException($"Array type '{type}' has no item type."); + return $"{GetSchemaTypeId(itemType)}{ArraySuffix}"; + } + } + + var genericTypeName = type.GetGenericTypeDefinition().GetTypeName(pretty: true); + if (StringUtils.IsNullOrEmpty(genericTypeName)) + throw new InvalidOperationException($"Generic type '{type}' does not have a full name."); + + var tickIndex = genericTypeName.IndexOf('`'); + if (tickIndex > 0) + genericTypeName = genericTypeName.Substring(0, tickIndex); + + // Recursively get the type ID for each generic argument + var genericArgs = genericTypes.Select(GetSchemaTypeId); + return $"{genericTypeName}<{string.Join(",", genericArgs)}>"; + } + + if (type.IsArray) + { + var elementType = type.GetElementType(); + if (elementType == null) + throw new InvalidOperationException($"Array type '{type}' has no element type."); + return $"{GetSchemaTypeId(elementType)}{ArraySuffix}"; + } + + return GetTypeName(type, pretty: true); + } + public static bool IsNameMatch(Type? type, string? typeName) { if (type == null || string.IsNullOrEmpty(typeName)) From 4ac71b9516186bc774e52326c65d3442565a33d4 Mon Sep 17 00:00:00 2001 From: Ivan Murzak Date: Fri, 17 Oct 2025 01:57:26 -0700 Subject: [PATCH 17/24] feat: add AssertAllRefsDefined method to validate $ref references in schema $defs section --- .../SchemaTests/ReturnSchemaTests.cs | 60 +++++ .../SchemaTests/SchemaTestBase.cs | 38 ++++ ReflectorNet/src/Utils/Json/JsonSchema.cs | 215 ++++++++++-------- 3 files changed, 213 insertions(+), 100 deletions(-) diff --git a/ReflectorNet.Tests/SchemaTests/ReturnSchemaTests.cs b/ReflectorNet.Tests/SchemaTests/ReturnSchemaTests.cs index d60a1aee..6d438a2a 100644 --- a/ReflectorNet.Tests/SchemaTests/ReturnSchemaTests.cs +++ b/ReflectorNet.Tests/SchemaTests/ReturnSchemaTests.cs @@ -163,6 +163,7 @@ public void GetReturnSchema_PrimitiveMethod_ReturnsCorrectSchema(string methodNa { var schema = GetReturnSchemaForMethod(methodName); AssertPrimitiveReturnSchema(schema!, expectedType, shouldBeRequired: true); + AssertAllRefsDefined(schema!); } #endregion @@ -179,6 +180,7 @@ public void GetReturnSchema_NullablePrimitiveMethod_ReturnsSchemaWithoutRequired { var schema = GetReturnSchemaForMethod(methodName); AssertPrimitiveReturnSchema(schema!, expectedType, shouldBeRequired: false); + AssertAllRefsDefined(schema!); } #endregion @@ -193,6 +195,7 @@ public void GetReturnSchema_TaskPrimitive_UnwrapsCorrectly(string methodName, st { var schema = GetReturnSchemaForMethod(methodName); AssertPrimitiveReturnSchema(schema!, expectedType, shouldBeRequired: true); + AssertAllRefsDefined(schema!); } [Fact] @@ -200,6 +203,7 @@ public void GetReturnSchema_TaskCustomType_UnwrapsToCustomTypeSchema() { var schema = GetReturnSchemaForMethod(nameof(TaskCustomTypeMethod)); AssertCustomTypeReturnSchema(schema!, new[] { "Name", "Value" }, shouldBeRequired: true); + AssertAllRefsDefined(schema!); } [Fact] @@ -210,6 +214,7 @@ public void GetReturnSchema_TaskPerson_UnwrapsCorrectly() Assert.Equal(JsonSchema.Object, schema[JsonSchema.Type]?.ToString()); AssertResultRequired(schema); AssertResultDefines(schema, typeof(Person), typeof(Address)); + AssertAllRefsDefined(schema); } [Fact] @@ -220,6 +225,7 @@ public void GetReturnSchema_TaskAddress_UnwrapsCorrectly() Assert.Equal(JsonSchema.Object, schema[JsonSchema.Type]?.ToString()); AssertResultRequired(schema); AssertResultDefines(schema, typeof(Address)); + AssertAllRefsDefined(schema); } [Fact] @@ -230,6 +236,7 @@ public void GetReturnSchema_TaskCompany_UnwrapsCorrectly() Assert.Equal(JsonSchema.Object, schema[JsonSchema.Type]?.ToString()); AssertResultRequired(schema); AssertResultDefines(schema, typeof(Company), typeof(Address), typeof(Person)); + AssertAllRefsDefined(schema); } #endregion @@ -244,6 +251,7 @@ public void GetReturnSchema_TaskNullablePrimitive_UnwrapsWithoutRequired(string { var schema = GetReturnSchemaForMethod(methodName); AssertPrimitiveReturnSchema(schema!, expectedType, shouldBeRequired: false); + AssertAllRefsDefined(schema!); } [Fact] @@ -251,6 +259,7 @@ public void GetReturnSchema_TaskNullableCustomType_UnwrapsToCustomTypeSchemaWith { var schema = GetReturnSchemaForMethod(nameof(TaskNullableCustomTypeMethod)); AssertCustomTypeReturnSchema(schema!, new[] { "Name", "Value" }, shouldBeRequired: false); + AssertAllRefsDefined(schema!); } [Fact] @@ -261,6 +270,7 @@ public void GetReturnSchema_TaskNullablePerson_UnwrapsWithoutRequired() Assert.Equal(JsonSchema.Object, schema[JsonSchema.Type]?.ToString()); AssertResultNotRequired(schema); AssertResultDefines(schema, typeof(Person), typeof(Address)); + AssertAllRefsDefined(schema); } [Fact] @@ -271,6 +281,7 @@ public void GetReturnSchema_TaskNullableAddress_UnwrapsWithoutRequired() Assert.Equal(JsonSchema.Object, schema[JsonSchema.Type]?.ToString()); AssertResultNotRequired(schema); AssertResultDefines(schema, typeof(Address)); + AssertAllRefsDefined(schema); } [Fact] @@ -281,6 +292,7 @@ public void GetReturnSchema_TaskNullableCompany_UnwrapsWithoutRequired() Assert.Equal(JsonSchema.Object, schema[JsonSchema.Type]?.ToString()); AssertResultNotRequired(schema); AssertResultDefines(schema, typeof(Company), typeof(Address), typeof(Person)); + AssertAllRefsDefined(schema); } #endregion @@ -294,6 +306,7 @@ public void GetReturnSchema_NullableTaskNullablePrimitive_UnwrapsWithoutRequired { var schema = GetReturnSchemaForMethod(methodName); AssertPrimitiveReturnSchema(schema!, expectedType, shouldBeRequired: false); + AssertAllRefsDefined(schema!); } [Fact] @@ -301,6 +314,7 @@ public void GetReturnSchema_NullableTaskNullableCustomType_UnwrapsToCustomTypeSc { var schema = GetReturnSchemaForMethod(nameof(NullableTaskNullableCustomTypeMethod)); AssertCustomTypeReturnSchema(schema!, new[] { "Name", "Value" }, shouldBeRequired: false); + AssertAllRefsDefined(schema!); } [Fact] @@ -311,6 +325,7 @@ public void GetReturnSchema_NullableTaskNullablePerson_UnwrapsWithoutRequired() Assert.Equal(JsonSchema.Object, schema[JsonSchema.Type]?.ToString()); AssertResultNotRequired(schema); AssertResultDefines(schema, typeof(Person), typeof(Address)); + AssertAllRefsDefined(schema); } [Fact] @@ -321,6 +336,7 @@ public void GetReturnSchema_NullableTaskNullableAddress_UnwrapsWithoutRequired() Assert.Equal(JsonSchema.Object, schema[JsonSchema.Type]?.ToString()); AssertResultNotRequired(schema); AssertResultDefines(schema, typeof(Address)); + AssertAllRefsDefined(schema); } [Fact] @@ -331,6 +347,7 @@ public void GetReturnSchema_NullableTaskNullableCompany_UnwrapsWithoutRequired() Assert.Equal(JsonSchema.Object, schema[JsonSchema.Type]?.ToString()); AssertResultNotRequired(schema); AssertResultDefines(schema, typeof(Company), typeof(Address), typeof(Person)); + AssertAllRefsDefined(schema); } #endregion @@ -344,6 +361,7 @@ public void GetReturnSchema_ValueTaskPrimitive_UnwrapsCorrectly(string methodNam { var schema = GetReturnSchemaForMethod(methodName); AssertPrimitiveReturnSchema(schema!, expectedType, shouldBeRequired: true); + AssertAllRefsDefined(schema!); } [Fact] @@ -351,6 +369,7 @@ public void GetReturnSchema_ValueTaskCustomType_UnwrapsToCustomTypeSchema() { var schema = GetReturnSchemaForMethod(nameof(ValueTaskCustomTypeMethod)); AssertCustomTypeReturnSchema(schema!, new[] { "Name", "Value" }, shouldBeRequired: true); + AssertAllRefsDefined(schema!); } [Fact] @@ -361,6 +380,7 @@ public void GetReturnSchema_ValueTaskPerson_UnwrapsCorrectly() Assert.Equal(JsonSchema.Object, schema[JsonSchema.Type]?.ToString()); AssertResultRequired(schema); AssertResultDefines(schema, typeof(Person), typeof(Address)); + AssertAllRefsDefined(schema); } [Fact] @@ -371,6 +391,7 @@ public void GetReturnSchema_ValueTaskAddress_UnwrapsCorrectly() Assert.Equal(JsonSchema.Object, schema[JsonSchema.Type]?.ToString()); AssertResultRequired(schema); AssertResultDefines(schema, typeof(Address)); + AssertAllRefsDefined(schema); } [Fact] @@ -381,6 +402,7 @@ public void GetReturnSchema_ValueTaskCompany_UnwrapsCorrectly() Assert.Equal(JsonSchema.Object, schema[JsonSchema.Type]?.ToString()); AssertResultRequired(schema); AssertResultDefines(schema, typeof(Company), typeof(Address), typeof(Person)); + AssertAllRefsDefined(schema); } #endregion @@ -401,6 +423,7 @@ public void GetReturnSchema_ValueTaskNullableCustomType_UnwrapsToCustomTypeSchem { var schema = GetReturnSchemaForMethod(nameof(ValueTaskNullableCustomTypeMethod)); AssertCustomTypeReturnSchema(schema!, new[] { "Name", "Value" }, shouldBeRequired: false); + AssertAllRefsDefined(schema!); } [Fact] @@ -411,6 +434,7 @@ public void GetReturnSchema_ValueTaskNullablePerson_UnwrapsWithoutRequired() Assert.Equal(JsonSchema.Object, schema[JsonSchema.Type]?.ToString()); AssertResultNotRequired(schema); AssertResultDefines(schema, typeof(Person), typeof(Address)); + AssertAllRefsDefined(schema); } [Fact] @@ -421,6 +445,7 @@ public void GetReturnSchema_ValueTaskNullableAddress_UnwrapsWithoutRequired() Assert.Equal(JsonSchema.Object, schema[JsonSchema.Type]?.ToString()); AssertResultNotRequired(schema); AssertResultDefines(schema, typeof(Address)); + AssertAllRefsDefined(schema); } [Fact] @@ -431,6 +456,7 @@ public void GetReturnSchema_ValueTaskNullableCompany_UnwrapsWithoutRequired() Assert.Equal(JsonSchema.Object, schema[JsonSchema.Type]?.ToString()); AssertResultNotRequired(schema); AssertResultDefines(schema, typeof(Company), typeof(Address), typeof(Person)); + AssertAllRefsDefined(schema); } #endregion @@ -445,6 +471,7 @@ public void GetReturnSchema_CustomType_ReturnsValidObjectSchema() { var schema = GetReturnSchemaForMethod(nameof(CustomTypeMethod)); AssertCustomTypeReturnSchema(schema!, new[] { "Name", "Value" }, shouldBeRequired: true); + AssertAllRefsDefined(schema!); } [Fact] @@ -453,6 +480,7 @@ public void GetReturnSchema_ComplexType_ReturnsValidNestedSchema() var schema = GetReturnSchemaForMethod(nameof(ComplexTypeMethod)); AssertCustomTypeReturnSchema(schema!, new[] { "StringProperty", "IntProperty", "NestedObject", "StringArray" }, shouldBeRequired: true); AssertResultDefines(schema!, typeof(ComplexReturnType), typeof(CustomReturnType)); + AssertAllRefsDefined(schema!); } [Fact] @@ -463,6 +491,7 @@ public void GetReturnSchema_Person_ReturnsValidObjectSchema() Assert.Equal(JsonSchema.Object, schema[JsonSchema.Type]?.ToString()); AssertResultRequired(schema); AssertResultDefines(schema, typeof(Person), typeof(Address)); + AssertAllRefsDefined(schema); } [Fact] @@ -473,6 +502,7 @@ public void GetReturnSchema_Address_ReturnsValidObjectSchema() Assert.Equal(JsonSchema.Object, schema[JsonSchema.Type]?.ToString()); AssertResultRequired(schema); AssertResultDefines(schema, typeof(Address)); + AssertAllRefsDefined(schema); } [Fact] @@ -483,6 +513,7 @@ public void GetReturnSchema_Company_ReturnsValidObjectSchema() Assert.Equal(JsonSchema.Object, schema[JsonSchema.Type]?.ToString()); AssertResultRequired(schema); AssertResultDefines(schema, typeof(Company), typeof(Address), typeof(Person)); + AssertAllRefsDefined(schema); } #endregion @@ -494,6 +525,7 @@ public void GetReturnSchema_NullableCustomType_ReturnsValidObjectSchemaWithoutRe { var schema = GetReturnSchemaForMethod(nameof(NullableCustomTypeMethod)); AssertCustomTypeReturnSchema(schema!, new[] { "Name", "Value" }, shouldBeRequired: false); + AssertAllRefsDefined(schema!); } [Fact] @@ -502,6 +534,7 @@ public void GetReturnSchema_NullableComplexType_ReturnsValidNestedSchemaWithoutR var schema = GetReturnSchemaForMethod(nameof(NullableComplexTypeMethod)); AssertCustomTypeReturnSchema(schema!, new[] { "StringProperty", "IntProperty", "NestedObject", "StringArray" }, shouldBeRequired: false); AssertResultDefines(schema!, typeof(ComplexReturnType), typeof(CustomReturnType)); + AssertAllRefsDefined(schema!); } [Fact] @@ -512,6 +545,7 @@ public void GetReturnSchema_NullablePerson_ReturnsValidObjectSchemaWithoutRequir Assert.Equal(JsonSchema.Object, schema[JsonSchema.Type]?.ToString()); AssertResultNotRequired(schema); AssertResultDefines(schema, typeof(Person), typeof(Address)); + AssertAllRefsDefined(schema); } [Fact] @@ -522,6 +556,7 @@ public void GetReturnSchema_NullableAddress_ReturnsValidObjectSchemaWithoutRequi Assert.Equal(JsonSchema.Object, schema[JsonSchema.Type]?.ToString()); AssertResultNotRequired(schema); AssertResultDefines(schema, typeof(Address)); + AssertAllRefsDefined(schema); } [Fact] @@ -532,6 +567,7 @@ public void GetReturnSchema_NullableCompany_ReturnsValidObjectSchemaWithoutRequi Assert.Equal(JsonSchema.Object, schema[JsonSchema.Type]?.ToString()); AssertResultNotRequired(schema); AssertResultDefines(schema, typeof(Company), typeof(Address), typeof(Person)); + AssertAllRefsDefined(schema); } #endregion @@ -545,6 +581,7 @@ public void GetReturnSchema_CollectionTypes_ReturnsArraySchema(string methodName { var schema = GetReturnSchemaForMethod(methodName); AssertArrayReturnSchema(schema!, expectedItemType, shouldBeRequired: true); + AssertAllRefsDefined(schema!); } #endregion @@ -556,6 +593,7 @@ public void GetReturnSchema_NullableStringArray_ReturnsArraySchemaWithoutRequire { var schema = GetReturnSchemaForMethod(nameof(NullableStringArrayMethod)); AssertArrayReturnSchema(schema!, JsonSchema.String, shouldBeRequired: false); + AssertAllRefsDefined(schema!); } #endregion @@ -569,6 +607,7 @@ public void GetReturnSchema_ListComplexType_ReturnsArraySchemaWithComplexItems(s { var schema = GetReturnSchemaForMethod(methodName); AssertComplexListReturnSchema(schema!, shouldBeRequired); + AssertAllRefsDefined(schema!); } [Theory] @@ -578,6 +617,7 @@ public void GetReturnSchema_ListNullableComplexType_ReturnsArraySchemaWithNullab { var schema = GetReturnSchemaForMethod(methodName); AssertComplexListReturnSchema(schema!, shouldBeRequired, itemsAreNullable: true); + AssertAllRefsDefined(schema!); } [Theory] @@ -587,6 +627,7 @@ public void GetReturnSchema_TaskListComplexType_UnwrapsToArraySchemaWithComplexI { var schema = GetReturnSchemaForMethod(methodName); AssertComplexListReturnSchema(schema!, shouldBeRequired); + AssertAllRefsDefined(schema!); } [Theory] @@ -596,6 +637,7 @@ public void GetReturnSchema_TaskNullableListComplexType_UnwrapsToArraySchemaWith { var schema = GetReturnSchemaForMethod(methodName); AssertComplexListReturnSchema(schema!, shouldBeRequired); + AssertAllRefsDefined(schema!); } [Theory] @@ -605,6 +647,7 @@ public void GetReturnSchema_TaskListNullableComplexType_UnwrapsToArraySchemaWith { var schema = GetReturnSchemaForMethod(methodName); AssertComplexListReturnSchema(schema!, shouldBeRequired, itemsAreNullable: true); + AssertAllRefsDefined(schema!); } [Theory] @@ -614,6 +657,7 @@ public void GetReturnSchema_TaskNullableListNullableComplexType_UnwrapsToArraySc { var schema = GetReturnSchemaForMethod(methodName); AssertComplexListReturnSchema(schema!, shouldBeRequired, itemsAreNullable: true); + AssertAllRefsDefined(schema!); } #endregion @@ -645,6 +689,7 @@ public void GetReturnSchema_CustomType_WithJustRef_ReturnsRefSchema() // $defs should exist with the full type definition Assert.True(schema.AsObject().ContainsKey(JsonSchema.Defs)); + AssertAllRefsDefined(schema); } [Fact] @@ -669,6 +714,7 @@ public void GetReturnSchema_PrimitiveType_WithJustRef_ReturnsFullSchema() var resultSchema = properties[JsonSchema.Result]!.AsObject(); Assert.Equal(JsonSchema.String, resultSchema[JsonSchema.Type]?.ToString()); Assert.False(resultSchema.ContainsKey(JsonSchema.Ref)); + AssertAllRefsDefined(schema); } [Theory] @@ -699,6 +745,7 @@ public void GetReturnSchema_OuterAssemblyType_WithJustRef_ReturnsRefSchema(strin // $defs should exist with the full type definition Assert.True(schema.AsObject().ContainsKey(JsonSchema.Defs)); + AssertAllRefsDefined(schema); } #endregion @@ -749,6 +796,7 @@ public void GetReturnSchema_WrapperEchoPrimitive_ReturnsCorrectSchema(Type gener var wrapperType = typeof(WrapperClass<>).MakeGenericType(genericType); var schema = GetWrapperMethodReturnSchema(wrapperType, methodName); AssertPrimitiveReturnSchema(schema!, expectedType, shouldBeRequired); + AssertAllRefsDefined(schema!); } [Theory] @@ -766,6 +814,7 @@ public void GetReturnSchema_WrapperEchoCustomType_ReturnsCorrectSchema(Type gene AssertResultRequired(schema); else AssertResultNotRequired(schema); + AssertAllRefsDefined(schema); } [Theory] @@ -783,6 +832,7 @@ public void GetReturnSchema_WrapperEchoNullableCustomType_ReturnsCorrectSchema(T AssertResultRequired(schema); else AssertResultNotRequired(schema); + AssertAllRefsDefined(schema); } [Theory] @@ -793,6 +843,7 @@ public void GetReturnSchema_WrapperEchoArray_ReturnsCorrectSchema(Type genericTy var wrapperType = typeof(WrapperClass<>).MakeGenericType(genericType); var schema = GetWrapperMethodReturnSchema(wrapperType, nameof(WrapperClass.Echo)); AssertArrayReturnSchema(schema!, expectedItemType, shouldBeRequired); + AssertAllRefsDefined(schema!); } [Theory] @@ -803,6 +854,7 @@ public void GetReturnSchema_WrapperEchoNullableArray_ReturnsCorrectSchema(Type g var wrapperType = typeof(WrapperClass<>).MakeGenericType(genericType); var schema = GetWrapperMethodReturnSchema(wrapperType, nameof(WrapperClass.EchoNullable)); AssertArrayReturnSchema(schema!, expectedItemType, shouldBeRequired); + AssertAllRefsDefined(schema!); } [Fact] @@ -811,6 +863,7 @@ public void GetReturnSchema_WrapperEchoListComplex_ReturnsCorrectSchema() var wrapperType = typeof(WrapperClass>); var schema = GetWrapperMethodReturnSchema(wrapperType, nameof(WrapperClass>.Echo)); AssertComplexListReturnSchema(schema!, shouldBeRequired: true); + AssertAllRefsDefined(schema!); } [Fact] @@ -819,6 +872,7 @@ public void GetReturnSchema_WrapperEchoNullableListComplex_ReturnsCorrectSchema( var wrapperType = typeof(WrapperClass>); var schema = GetWrapperMethodReturnSchema(wrapperType, nameof(WrapperClass>.EchoNullable)); AssertComplexListReturnSchema(schema!, shouldBeRequired: false); + AssertAllRefsDefined(schema!); } [Fact] @@ -831,6 +885,7 @@ public void GetReturnSchema_WrapperEchoPerson_ReturnsCorrectSchema() Assert.Equal(JsonSchema.Object, schema[JsonSchema.Type]?.ToString()); AssertResultRequired(schema); AssertResultDefines(schema, typeof(Person), typeof(Address)); + AssertAllRefsDefined(schema); } [Fact] @@ -843,6 +898,7 @@ public void GetReturnSchema_WrapperEchoAddress_ReturnsCorrectSchema() Assert.Equal(JsonSchema.Object, schema[JsonSchema.Type]?.ToString()); AssertResultRequired(schema); AssertResultDefines(schema, typeof(Address)); + AssertAllRefsDefined(schema); } [Fact] @@ -855,6 +911,7 @@ public void GetReturnSchema_WrapperEchoCompany_ReturnsCorrectSchema() Assert.Equal(JsonSchema.Object, schema[JsonSchema.Type]?.ToString()); AssertResultRequired(schema); AssertResultDefines(schema, typeof(Company), typeof(Address), typeof(Person)); + AssertAllRefsDefined(schema); } [Fact] @@ -867,6 +924,7 @@ public void GetReturnSchema_WrapperEchoNullablePerson_ReturnsCorrectSchema() Assert.Equal(JsonSchema.Object, schema[JsonSchema.Type]?.ToString()); AssertResultNotRequired(schema); AssertResultDefines(schema, typeof(Person), typeof(Address)); + AssertAllRefsDefined(schema); } [Fact] @@ -879,6 +937,7 @@ public void GetReturnSchema_WrapperEchoNullableAddress_ReturnsCorrectSchema() Assert.Equal(JsonSchema.Object, schema[JsonSchema.Type]?.ToString()); AssertResultNotRequired(schema); AssertResultDefines(schema, typeof(Address)); + AssertAllRefsDefined(schema); } [Fact] @@ -891,6 +950,7 @@ public void GetReturnSchema_WrapperEchoNullableCompany_ReturnsCorrectSchema() Assert.Equal(JsonSchema.Object, schema[JsonSchema.Type]?.ToString()); AssertResultNotRequired(schema); AssertResultDefines(schema, typeof(Company), typeof(Address), typeof(Person)); + AssertAllRefsDefined(schema); } #endregion diff --git a/ReflectorNet.Tests/SchemaTests/SchemaTestBase.cs b/ReflectorNet.Tests/SchemaTests/SchemaTestBase.cs index 8d998564..59af7817 100644 --- a/ReflectorNet.Tests/SchemaTests/SchemaTestBase.cs +++ b/ReflectorNet.Tests/SchemaTests/SchemaTestBase.cs @@ -245,6 +245,44 @@ private void CollectAllReferences(JsonNode? node, HashSet references) } } + /// + /// Asserts that all $ref references found in the schema are defined in the $defs section. + /// This method recursively scans the entire schema for all $ref values and verifies that + /// each referenced type has a corresponding definition in $defs. + /// + protected void AssertAllRefsDefined(JsonNode schema) + { + Assert.NotNull(schema); + + // Collect all $ref values in the schema + var allReferences = new HashSet(); + CollectAllReferences(schema, allReferences); + + // If there are no references, we're done + if (allReferences.Count == 0) + { + return; + } + + // Schema must have $defs if there are references + Assert.True(schema.AsObject().ContainsKey(JsonSchema.Defs), + $"Schema contains {allReferences.Count} $ref reference(s) but no $defs section. References: {string.Join(", ", allReferences)}"); + + var defines = schema[JsonSchema.Defs]!.AsObject(); + Assert.NotNull(defines); + + // Check each reference to ensure it's defined + foreach (var reference in allReferences) + { + // Extract the type ID from the reference (e.g., "#/$defs/TypeId" -> "TypeId") + var typeId = reference.Replace(JsonSchema.RefValue, string.Empty); + + Assert.True(defines.ContainsKey(typeId), + $"Reference '{reference}' (type ID: '{typeId}') is not defined in $defs. " + + $"Available definitions: {string.Join(", ", defines.Select(d => d.Key))}"); + } + } + /// /// Asserts that a custom type return schema has the correct structure /// diff --git a/ReflectorNet/src/Utils/Json/JsonSchema.cs b/ReflectorNet/src/Utils/Json/JsonSchema.cs index caef409f..210b825e 100644 --- a/ReflectorNet/src/Utils/Json/JsonSchema.cs +++ b/ReflectorNet/src/Utils/Json/JsonSchema.cs @@ -8,6 +8,7 @@ using System; using System.Collections.Generic; using System.ComponentModel; +using System.Linq; using System.Reflection; using System.Text.Json.Nodes; using System.Threading.Tasks; @@ -169,36 +170,7 @@ public JsonNode GetSchema(Reflector reflector, Type type, bool justRef = false) } return schema; } - /// - /// Generates a JSON Schema with proper $defs handling for complex types. - /// This method handles type deduplication by placing complex type definitions in the $defs section - /// and using $ref references to avoid schema repetition. - /// - /// The Reflector instance used for type analysis and schema generation. - /// The Type for which to generate the schema. - /// Whether to use compact references for complex types. Default is false. - /// Optional JsonObject to accumulate type definitions. If null, a new object is created. - /// A tuple containing the generated schema and the definitions object. - private (JsonNode schema, JsonObject? defines) GenerateSchemaWithDefs(Reflector reflector, Type type, bool justRef = false, JsonObject? defines = null) - { - var isPrimitive = TypeUtils.IsPrimitive(type); - JsonNode schema; - if (isPrimitive) - { - schema = GetSchema(reflector, type, justRef: justRef); - } - else - { - schema = GetSchema(reflector, type, justRef: true); - defines ??= new JsonObject(); - - // Recursively collect all nested types - CollectNestedTypes(reflector, type, defines); - } - - return (schema, defines); - } /// /// Recursively collects all nested non-primitive types from a given type and adds them to the definitions. @@ -315,44 +287,14 @@ public JsonNode GetArgumentsSchema(Reflector reflector, MethodInfo method, bool if (parameters.Length == 0) return new JsonObject { [Type] = Object }; - var properties = new JsonObject(); - var defines = new JsonObject(); - var required = new JsonArray(); + var types = parameters + .Select(p => ( + type: p.ParameterType, + name: p.Name, + description: TypeUtils.GetDescription(p), + required: !p.HasDefaultValue)); - // Create a schema object manually - var schema = new JsonObject - { - // [SchemaDraft] = JsonValue.Create(SchemaDraftValue), - [Type] = Object, - [Properties] = properties, - [Required] = required - }; - - foreach (var parameter in parameters) - { - var (parameterSchema, updatedDefines) = GenerateSchemaWithDefs(reflector, parameter.ParameterType, justRef, defines); - defines = updatedDefines; - - if (parameterSchema == null) - continue; - - properties[parameter.Name!] = parameterSchema; - - if (parameterSchema is JsonObject parameterSchemaObject) - { - var propertyDescription = TypeUtils.GetDescription(parameter); - if (!string.IsNullOrEmpty(propertyDescription)) - parameterSchemaObject[Description] = JsonValue.Create(propertyDescription); - } - - // Check if the parameter has a default value - if (!parameter.HasDefaultValue) - required.Add(parameter.Name!); - } - - if (defines != null && defines.Count > 0) - schema[Defs] = defines; - return schema; + return GenerateSchema(reflector, types, justRef); } /// @@ -449,57 +391,130 @@ public JsonNode GetArgumentsSchema(Reflector reflector, MethodInfo method, bool unwrappedType = nullableUnderlyingType; } - // Check for nullable reference types using NullabilityInfoContext - if (!isNullable && (unwrappedType.IsClass || unwrappedType.IsInterface || unwrappedType.IsArray)) + // // Check for nullable reference types using NullabilityInfoContext + // if (!isNullable && (unwrappedType.IsClass || unwrappedType.IsInterface || unwrappedType.IsArray)) + // { + // try + // { + // #if NET5_0_OR_GREATER + // var nullabilityContext = new System.Reflection.NullabilityInfoContext(); + // var nullabilityInfo = nullabilityContext.Create(methodInfo.ReturnParameter); + + // // For Task or ValueTask, check the generic argument's nullability + // if (returnType.IsGenericType) + // { + // var genericDef = returnType.GetGenericTypeDefinition(); + // if (genericDef == typeof(Task<>) || genericDef == typeof(ValueTask<>)) + // { + // isNullable = isNullable || (nullabilityInfo.GenericTypeArguments.Length > 0 && + // nullabilityInfo.GenericTypeArguments[0].ReadState == System.Reflection.NullabilityState.Nullable); + // } + // else + // { + // isNullable = nullabilityInfo.ReadState == System.Reflection.NullabilityState.Nullable; + // } + // } + // else + // { + // isNullable = nullabilityInfo.ReadState == System.Reflection.NullabilityState.Nullable; + // } + // #endif + // } + // catch + // { + // // If we can't determine nullability, assume not nullable + // } + // } + + var types = new (Type type, string name, string? description, bool required)[] { - try - { -#if NET5_0_OR_GREATER - var nullabilityContext = new System.Reflection.NullabilityInfoContext(); - var nullabilityInfo = nullabilityContext.Create(methodInfo.ReturnParameter); + ( + type: unwrappedType, + name: Result, + description: null, + required: true + ) + }; + return GenerateSchema(reflector, types, justRef); + } + + public JsonNode GenerateSchema(Reflector reflector, IEnumerable<(Type type, string name, string? description, bool required)> types, bool justRef = false) + { + var properties = new JsonObject(); + var defines = new JsonObject(); + var required = new JsonArray(); - // For Task or ValueTask, check the generic argument's nullability - if (returnType.IsGenericType) + // Create a schema object manually + var schema = new JsonObject + { + // [SchemaDraft] = JsonValue.Create(SchemaDraftValue), + [Type] = Object, + [Properties] = properties + }; + + foreach (var parameter in types) + { + var parameterSchema = default(JsonNode); + var isPrimitive = TypeUtils.IsPrimitive(parameter.type); + if (isPrimitive) + { + parameterSchema = GetSchema(reflector, parameter.type, justRef: justRef); + if (parameterSchema == null) + continue; + } + else + { + var typeId = parameter.type.GetTypeId(); + if (defines.ContainsKey(typeId)) { - var genericDef = returnType.GetGenericTypeDefinition(); - if (genericDef == typeof(Task<>) || genericDef == typeof(ValueTask<>)) - { - isNullable = isNullable || (nullabilityInfo.GenericTypeArguments.Length > 0 && - nullabilityInfo.GenericTypeArguments[0].ReadState == System.Reflection.NullabilityState.Nullable); - } - else - { - isNullable = nullabilityInfo.ReadState == System.Reflection.NullabilityState.Nullable; - } + parameterSchema = GetSchema(reflector, parameter.type, justRef: true); } else { - isNullable = nullabilityInfo.ReadState == System.Reflection.NullabilityState.Nullable; + var fullSchema = GetSchema(reflector, parameter.type, justRef: false); + if (fullSchema == null) + continue; + defines[typeId] = fullSchema; + parameterSchema = GetSchema(reflector, parameter.type, justRef: true); } -#endif + + if (parameterSchema == null) + continue; } - catch + + properties[parameter.name!] = parameterSchema; + + if (parameterSchema is JsonObject parameterSchemaObject) { - // If we can't determine nullability, assume not nullable + var propertyDescription = parameter.description; + if (!string.IsNullOrEmpty(propertyDescription)) + parameterSchemaObject[Description] = JsonValue.Create(propertyDescription); } - } - // Generate schema for the return type using the shared method - var (resultSchema, defines) = GenerateSchemaWithDefs(reflector, unwrappedType, justRef); + // Check if the parameter has a default value + if (parameter.required) + required.Add(parameter.name!); - // Create the root schema object in the same format as GetArgumentsSchema - var schema = new JsonObject { [Type] = Object }; + // Add generic type parameters recursively if any + foreach (var genericArgument in TypeUtils.GetGenericTypes(parameter.type)) + { + if (TypeUtils.IsPrimitive(genericArgument)) + continue; - if (resultSchema != null) - { - schema[Properties] = new JsonObject { [Result] = resultSchema }; + var typeId = genericArgument.GetTypeId(); + if (defines.ContainsKey(typeId)) + continue; - if (!isNullable) - schema[Required] = new JsonArray { Result }; + var genericSchema = GetSchema(reflector, genericArgument, justRef: false); + if (genericSchema != null) + defines[typeId] = genericSchema; + } } - if (defines != null && defines.Count > 0) + if (defines.Count > 0) schema[Defs] = defines; + if (required.Count > 0) + schema[Required] = required; return schema; } From 4cfd93440cbd7697900415f8c133175ed63d7653 Mon Sep 17 00:00:00 2001 From: Ivan Murzak Date: Wed, 22 Oct 2025 00:09:43 -0700 Subject: [PATCH 18/24] feat: Introduce JSON Schema Converter Interface and Implementations - Added IJsonSchemaConverter interface to define methods for schema generation. - Implemented JsonSchemaConverter abstract class to provide base functionality for specific type converters. - Refactored MethodDataConverter, SerializedMemberConverter, and SerializedMemberListConverter to inherit from JsonSchemaConverter. - Updated methods to utilize new schema generation logic, including GetSchema and GetSchemaRef. - Enhanced Reflector class to support schema generation for method parameters and return types with improved nullability handling. - Introduced defines parameter in schema generation methods to manage recursive type definitions and prevent infinite loops. - Improved error handling and documentation throughout the schema generation process. --- README.md | 2 +- .../JsonConvertors/ObjectRefConverter.cs | 9 +- ...ializedMemberCustomDescriptionConverter.cs | 10 +- .../SchemaTests/CollectionsTests.cs | 3 +- .../SchemaTests/PerformanceTests.cs | 4 +- .../SchemaTests/ReturnSchemaTests.cs | 8 + .../SchemaTests/SchemaTestBase.cs | 6 +- .../SchemaTests/TestDescription.Utils.cs | 2 +- .../SchemaTests/TestDescription.cs | 46 +-- ...meConvertor.cs => IJsonSchemaConvertor.cs} | 7 +- .../src/Convertor/Json/JsonSchemaConverter.cs | 27 ++ .../src/Convertor/Json/MethodDataConverter.cs | 15 +- .../Json/SerializedMemberConverter.cs | 15 +- .../Json/SerializedMemberListConverter.cs | 15 +- ReflectorNet/src/Extension/ExtensionsType.cs | 3 +- ReflectorNet/src/Model/MethodData.cs | 8 +- ReflectorNet/src/Reflector/Reflector.Error.cs | 2 +- ReflectorNet/src/Reflector/Reflector.Json.cs | 56 ++- .../src/Utils/Json/JsonSchema.Internal.cs | 85 +++- ReflectorNet/src/Utils/Json/JsonSchema.cs | 380 +++++++++++------- 20 files changed, 454 insertions(+), 249 deletions(-) rename ReflectorNet/src/Convertor/{IJsonSchemeConvertor.cs => IJsonSchemaConvertor.cs} (71%) create mode 100644 ReflectorNet/src/Convertor/Json/JsonSchemaConverter.cs diff --git a/README.md b/README.md index 38d63a52..ee51aad4 100644 --- a/README.md +++ b/README.md @@ -372,7 +372,7 @@ jsonSerializer.AddConverter(new MyCustomJsonConverter()); // Access to JSON schema generation var jsonSchema = reflector.JsonSchema; -var schema = jsonSchema.GetSchema(reflector, justRef: false); +var schema = jsonSchema.GetSchema(reflector); ``` #### Converter System Architecture diff --git a/ReflectorNet.Tests/Model/JsonConvertors/ObjectRefConverter.cs b/ReflectorNet.Tests/Model/JsonConvertors/ObjectRefConverter.cs index 915a749e..33165524 100644 --- a/ReflectorNet.Tests/Model/JsonConvertors/ObjectRefConverter.cs +++ b/ReflectorNet.Tests/Model/JsonConvertors/ObjectRefConverter.cs @@ -1,16 +1,15 @@ using System; +using System.Collections.Generic; using System.Text.Json; using System.Text.Json.Nodes; -using System.Text.Json.Serialization; using com.IvanMurzak.ReflectorNet.Json; using com.IvanMurzak.ReflectorNet.Utils; namespace com.IvanMurzak.ReflectorNet.Tests.Model { - public class ObjectRefConverter : JsonConverter, IJsonSchemaConverter + public class ObjectRefConverter : JsonSchemaConverter, IJsonSchemaConverter { - public string Id => typeof(ObjectRef).GetSchemaTypeId(); - public JsonNode GetScheme() => new JsonObject + public override JsonNode GetSchema() => new JsonObject { [JsonSchema.Type] = JsonSchema.Object, [JsonSchema.Properties] = new JsonObject @@ -21,7 +20,7 @@ public class ObjectRefConverter : JsonConverter, IJsonSchemaConverter }, [JsonSchema.Required] = new JsonArray { nameof(ObjectRef.instanceID) } }; - public JsonNode GetSchemeRef() => new JsonObject + public override JsonNode GetSchemaRef() => new JsonObject { [JsonSchema.Ref] = JsonSchema.RefValue + Id }; diff --git a/ReflectorNet.Tests/Model/JsonConvertors/SerializedMemberCustomDescriptionConverter.cs b/ReflectorNet.Tests/Model/JsonConvertors/SerializedMemberCustomDescriptionConverter.cs index 5f51e4e2..6e3f512d 100644 --- a/ReflectorNet.Tests/Model/JsonConvertors/SerializedMemberCustomDescriptionConverter.cs +++ b/ReflectorNet.Tests/Model/JsonConvertors/SerializedMemberCustomDescriptionConverter.cs @@ -1,18 +1,16 @@ using System; using System.Text.Json; using System.Text.Json.Nodes; -using System.Text.Json.Serialization; using com.IvanMurzak.ReflectorNet.Json; using com.IvanMurzak.ReflectorNet.Model; using com.IvanMurzak.ReflectorNet.Utils; namespace com.IvanMurzak.ReflectorNet.Tests.Model { - public class SerializedMemberCustomDescriptionConverter : JsonConverter, IJsonSchemaConverter + public class SerializedMemberCustomDescriptionConverter : JsonSchemaConverter, IJsonSchemaConverter { public const string CustomDescription = "Custom description, used for testing purposes."; - public static string StaticId => TypeUtils.GetSchemaTypeId(); public static JsonNode Schema => new JsonObject { [JsonSchema.Type] = JsonSchema.Object, @@ -64,15 +62,13 @@ public class SerializedMemberCustomDescriptionConverter : JsonConverter StaticId; - public SerializedMemberCustomDescriptionConverter(Reflector reflector) { _reflector = reflector ?? throw new ArgumentNullException(nameof(reflector)); } - public JsonNode GetSchemeRef() => SchemaRef; - public JsonNode GetScheme() => Schema; + public override JsonNode GetSchemaRef() => SchemaRef; + public override JsonNode GetSchema() => Schema; public override SerializedMember? Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) { diff --git a/ReflectorNet.Tests/SchemaTests/CollectionsTests.cs b/ReflectorNet.Tests/SchemaTests/CollectionsTests.cs index 889a48c3..81689cb6 100644 --- a/ReflectorNet.Tests/SchemaTests/CollectionsTests.cs +++ b/ReflectorNet.Tests/SchemaTests/CollectionsTests.cs @@ -1,5 +1,6 @@ using System; using System.Collections.Generic; +using System.Text.Json; using com.IvanMurzak.ReflectorNet.Model; using com.IvanMurzak.ReflectorNet.Utils; using Xunit.Abstractions; @@ -57,7 +58,7 @@ public void GetTypeId_SimpleArray_ShouldAppendArray() { var result = reflector.GetSchema(type); - _output.WriteLine($"Type: {type.GetTypeShortName()}\n{result.ToJsonString()}\n"); + _output.WriteLine($"Type: {type.GetTypeShortName()}\n{result.ToJsonString(new JsonSerializerOptions { WriteIndented = true })}\n"); // Assert Assert.NotNull(result); diff --git a/ReflectorNet.Tests/SchemaTests/PerformanceTests.cs b/ReflectorNet.Tests/SchemaTests/PerformanceTests.cs index c2b6e058..44a146ca 100644 --- a/ReflectorNet.Tests/SchemaTests/PerformanceTests.cs +++ b/ReflectorNet.Tests/SchemaTests/PerformanceTests.cs @@ -82,7 +82,7 @@ public void Reflector_Introspection_Tests() var testType = typeof(GameObjectRef); // Act - Test introspection capabilities - var schema = testType.GetSchema(reflector, justRef: false); + var schema = testType.GetSchema(reflector); var typeId = testType.GetTypeId(); // Assert @@ -133,7 +133,7 @@ public void JsonUtils_Comprehensive_Tests() var serializedJson = reflector.JsonSerializer.Serialize(testObject); var deserializedObject = reflector.JsonSerializer.Deserialize(serializedJson); - var schema = reflector.GetSchema(typeof(GameObjectRefList), justRef: false); + var schema = reflector.GetSchema(typeof(GameObjectRefList)); var argumentsSchema = reflector.GetArgumentsSchema( typeof(MethodHelper).GetMethod(nameof(MethodHelper.ListObject_ListObject))!); diff --git a/ReflectorNet.Tests/SchemaTests/ReturnSchemaTests.cs b/ReflectorNet.Tests/SchemaTests/ReturnSchemaTests.cs index 6d438a2a..c93a2faa 100644 --- a/ReflectorNet.Tests/SchemaTests/ReturnSchemaTests.cs +++ b/ReflectorNet.Tests/SchemaTests/ReturnSchemaTests.cs @@ -622,7 +622,11 @@ public void GetReturnSchema_ListNullableComplexType_ReturnsArraySchemaWithNullab [Theory] [InlineData(nameof(TaskListComplexTypeMethod), true)] +#if NET5_0_OR_GREATER [InlineData(nameof(NullableTaskListComplexTypeMethod), false)] +#else + [InlineData(nameof(NullableTaskListComplexTypeMethod), true)] // netstandard2.1 cannot detect Task? nullability +#endif public void GetReturnSchema_TaskListComplexType_UnwrapsToArraySchemaWithComplexItems(string methodName, bool shouldBeRequired) { var schema = GetReturnSchemaForMethod(methodName); @@ -642,7 +646,11 @@ public void GetReturnSchema_TaskNullableListComplexType_UnwrapsToArraySchemaWith [Theory] [InlineData(nameof(TaskListNullableComplexTypeMethod), true)] +#if NET5_0_OR_GREATER [InlineData(nameof(NullableTaskListNullableComplexTypeMethod), false)] +#else + [InlineData(nameof(NullableTaskListNullableComplexTypeMethod), true)] // netstandard2.1 cannot detect Task? nullability +#endif public void GetReturnSchema_TaskListNullableComplexType_UnwrapsToArraySchemaWithNullableItems(string methodName, bool shouldBeRequired) { var schema = GetReturnSchemaForMethod(methodName); diff --git a/ReflectorNet.Tests/SchemaTests/SchemaTestBase.cs b/ReflectorNet.Tests/SchemaTests/SchemaTestBase.cs index 59af7817..cbec0aea 100644 --- a/ReflectorNet.Tests/SchemaTests/SchemaTestBase.cs +++ b/ReflectorNet.Tests/SchemaTests/SchemaTestBase.cs @@ -18,7 +18,7 @@ protected SchemaTestBase(ITestOutputHelper output) : base(output) { reflector ??= new Reflector(); - var schema = reflector.GetSchema(type, justRef: false); + var schema = reflector.GetSchema(type); _output.WriteLine($"Schema for {type.GetTypeShortName()}"); _output.WriteLine($"{schema}"); @@ -36,7 +36,7 @@ protected void TestMethodInputs_PropertyRefs(Reflector? reflector, MethodInfo me { reflector ??= new Reflector(); - var schema = reflector.GetArgumentsSchema(methodInfo, justRef: false)!; + var schema = reflector.GetArgumentsSchema(methodInfo)!; _output.WriteLine(schema.ToString()); @@ -74,7 +74,7 @@ protected void TestMethodInputs_PropertyRefs(Reflector? reflector, MethodInfo me { reflector ??= new Reflector(); - var schema = reflector.GetArgumentsSchema(methodInfo, justRef: false)!; + var schema = reflector.GetArgumentsSchema(methodInfo)!; _output.WriteLine(schema.ToString()); diff --git a/ReflectorNet.Tests/SchemaTests/TestDescription.Utils.cs b/ReflectorNet.Tests/SchemaTests/TestDescription.Utils.cs index bd178478..71aedb19 100644 --- a/ReflectorNet.Tests/SchemaTests/TestDescription.Utils.cs +++ b/ReflectorNet.Tests/SchemaTests/TestDescription.Utils.cs @@ -15,7 +15,7 @@ void TestClassMembersDescription(Type type, Reflector? reflector = null, Binding _output.WriteLine($"Testing members description for type: '{type.GetTypeShortName()}'"); - var schema = reflector.GetSchema(type, justRef: false); + var schema = reflector.GetSchema(type); Assert.NotNull(schema); var properties = default(JsonNode?); diff --git a/ReflectorNet.Tests/SchemaTests/TestDescription.cs b/ReflectorNet.Tests/SchemaTests/TestDescription.cs index 278454b7..f8052bd8 100644 --- a/ReflectorNet.Tests/SchemaTests/TestDescription.cs +++ b/ReflectorNet.Tests/SchemaTests/TestDescription.cs @@ -41,7 +41,7 @@ public void PropertyDescriptionOfCollectionWithDescriptions() TestClassMembersDescription(typeof(TestCollectionWithDescription), reflector); // For List, just test schema generation - var listComplexSchema = reflector.GetSchema(typeof(List), justRef: false); + var listComplexSchema = reflector.GetSchema(typeof(List)); Assert.NotNull(listComplexSchema); _output.WriteLine($"List schema: {listComplexSchema}"); } @@ -58,11 +58,11 @@ public void PropertyDescriptionOfReflectorNetModels() // SerializedMember and SerializedMemberList use custom converters with descriptions // Let's test their schema generation separately - var serializedMemberSchema = reflector.GetSchema(typeof(SerializedMember), justRef: false); + var serializedMemberSchema = reflector.GetSchema(typeof(SerializedMember)); Assert.NotNull(serializedMemberSchema); _output.WriteLine($"SerializedMember schema: {serializedMemberSchema}"); - var serializedMemberListSchema = reflector.GetSchema(typeof(SerializedMemberList), justRef: false); + var serializedMemberListSchema = reflector.GetSchema(typeof(SerializedMemberList)); Assert.NotNull(serializedMemberListSchema); _output.WriteLine($"SerializedMemberList schema: {serializedMemberListSchema}"); } @@ -77,11 +77,11 @@ public void PropertyDescriptionOfArrayTypes() TestClassMembersDescription(typeof(TestClassWithDescriptions[]), reflector); // For primitive arrays, just test schema generation - var stringArraySchema = reflector.GetSchema(typeof(string[]), justRef: false); + var stringArraySchema = reflector.GetSchema(typeof(string[])); Assert.NotNull(stringArraySchema); _output.WriteLine($"string[] schema: {stringArraySchema}"); - var intArraySchema = reflector.GetSchema(typeof(int[]), justRef: false); + var intArraySchema = reflector.GetSchema(typeof(int[])); Assert.NotNull(intArraySchema); _output.WriteLine($"int[] schema: {intArraySchema}"); } @@ -96,15 +96,15 @@ public void PropertyDescriptionOfGenericCollections() TestClassMembersDescription(typeof(List), reflector); // For primitive collections, just test schema generation - var listStringSchema = reflector.GetSchema(typeof(List), justRef: false); + var listStringSchema = reflector.GetSchema(typeof(List)); Assert.NotNull(listStringSchema); _output.WriteLine($"List schema: {listStringSchema}"); - var listIntSchema = reflector.GetSchema(typeof(List), justRef: false); + var listIntSchema = reflector.GetSchema(typeof(List)); Assert.NotNull(listIntSchema); _output.WriteLine($"List schema: {listIntSchema}"); - var dictionarySchema = reflector.GetSchema(typeof(Dictionary), justRef: false); + var dictionarySchema = reflector.GetSchema(typeof(Dictionary)); Assert.NotNull(dictionarySchema); _output.WriteLine($"Dictionary schema: {dictionarySchema}"); } @@ -115,19 +115,19 @@ public void PropertyDescriptionOfPrimitiveTypes() var reflector = new Reflector(); // Primitive types don't have members to test, but we can test schema generation - var stringSchema = reflector.GetSchema(typeof(string), justRef: false); + var stringSchema = reflector.GetSchema(typeof(string)); Assert.NotNull(stringSchema); _output.WriteLine($"String schema: {stringSchema}"); - var intSchema = reflector.GetSchema(typeof(int), justRef: false); + var intSchema = reflector.GetSchema(typeof(int)); Assert.NotNull(intSchema); _output.WriteLine($"Int schema: {intSchema}"); - var boolSchema = reflector.GetSchema(typeof(bool), justRef: false); + var boolSchema = reflector.GetSchema(typeof(bool)); Assert.NotNull(boolSchema); _output.WriteLine($"Bool schema: {boolSchema}"); - var doubleSchema = reflector.GetSchema(typeof(double), justRef: false); + var doubleSchema = reflector.GetSchema(typeof(double)); Assert.NotNull(doubleSchema); _output.WriteLine($"Double schema: {doubleSchema}"); } @@ -138,15 +138,15 @@ public void PropertyDescriptionOfNullableTypes() var reflector = new Reflector(); // Nullable types also don't have members to test, but we can test schema generation - var nullableIntSchema = reflector.GetSchema(typeof(int?), justRef: false); + var nullableIntSchema = reflector.GetSchema(typeof(int?)); Assert.NotNull(nullableIntSchema); _output.WriteLine($"Nullable int schema: {nullableIntSchema}"); - var nullableBoolSchema = reflector.GetSchema(typeof(bool?), justRef: false); + var nullableBoolSchema = reflector.GetSchema(typeof(bool?)); Assert.NotNull(nullableBoolSchema); _output.WriteLine($"Nullable bool schema: {nullableBoolSchema}"); - var nullableDateTimeSchema = reflector.GetSchema(typeof(DateTime?), justRef: false); + var nullableDateTimeSchema = reflector.GetSchema(typeof(DateTime?)); Assert.NotNull(nullableDateTimeSchema); _output.WriteLine($"Nullable DateTime schema: {nullableDateTimeSchema}"); } @@ -157,7 +157,7 @@ public void ClassLevelDescription() var reflector = new Reflector(); // Test that class-level descriptions are properly captured - var schema = reflector.GetSchema(typeof(GameObjectRef), justRef: false); + var schema = reflector.GetSchema(typeof(GameObjectRef)); Assert.NotNull(schema); var description = schema[JsonSchema.Description]?.ToString(); @@ -173,7 +173,7 @@ public void ClassLevelDescriptionForTestClass() var reflector = new Reflector(); // Test class-level description for our test class - var schema = reflector.GetSchema(typeof(TestClassWithDescriptions), justRef: false); + var schema = reflector.GetSchema(typeof(TestClassWithDescriptions)); Assert.NotNull(schema); var description = schema[JsonSchema.Description]?.ToString(); @@ -189,7 +189,7 @@ public void MissingDescriptionsHandledGracefully() var reflector = new Reflector(); // Create a simple class without descriptions to test graceful handling - var schema = reflector.GetSchema(typeof(string), justRef: false); + var schema = reflector.GetSchema(typeof(string)); Assert.NotNull(schema); // Primitive types might not have descriptions, that's OK @@ -223,7 +223,7 @@ public void StructLevelDescription() var reflector = new Reflector(); // Test that struct-level descriptions are properly captured - var schema = reflector.GetSchema(typeof(TestStructWithDescriptions), justRef: false); + var schema = reflector.GetSchema(typeof(TestStructWithDescriptions)); Assert.NotNull(schema); var description = schema[JsonSchema.Description]?.ToString(); @@ -239,7 +239,7 @@ public void EnumLevelDescription() var reflector = new Reflector(); // Test that enum-level descriptions are properly captured - var schema = reflector.GetSchema(typeof(TestEnumWithDescriptions), justRef: false); + var schema = reflector.GetSchema(typeof(TestEnumWithDescriptions)); Assert.NotNull(schema); // For enums, the schema might be different - let's just verify it generates @@ -275,7 +275,7 @@ public void ValidateSpecificDescriptionContent() var reflector = new Reflector(); // Test that specific descriptions are correctly applied to schema properties - var schema = reflector.GetSchema(typeof(GameObjectRef), justRef: false); + var schema = reflector.GetSchema(typeof(GameObjectRef)); Assert.NotNull(schema); var properties = schema[JsonSchema.Properties]?.AsObject(); @@ -318,7 +318,7 @@ public void CustomConverterDescriptions() reflector.JsonSerializer.AddConverter(new SerializedMemberCustomDescriptionConverter(reflector)); // Test that custom JSON converters provide proper descriptions in schema - var serializedMemberSchema = reflector.GetSchema(typeof(SerializedMember), justRef: false); + var serializedMemberSchema = reflector.GetSchema(typeof(SerializedMember)); Assert.NotNull(serializedMemberSchema); var properties = serializedMemberSchema[JsonSchema.Properties]?.AsObject(); @@ -335,7 +335,7 @@ void NoEmptyOrNullDescriptions(Reflector reflector, IEnumerable types) { foreach (var type in types) { - var schema = reflector.GetSchema(type, justRef: false); + var schema = reflector.GetSchema(type); var descriptions = JsonSchema.FindAllProperties(schema, JsonSchema.Description); diff --git a/ReflectorNet/src/Convertor/IJsonSchemeConvertor.cs b/ReflectorNet/src/Convertor/IJsonSchemaConvertor.cs similarity index 71% rename from ReflectorNet/src/Convertor/IJsonSchemeConvertor.cs rename to ReflectorNet/src/Convertor/IJsonSchemaConvertor.cs index 2bfbf197..7223f2ef 100644 --- a/ReflectorNet/src/Convertor/IJsonSchemeConvertor.cs +++ b/ReflectorNet/src/Convertor/IJsonSchemaConvertor.cs @@ -5,6 +5,8 @@ * 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.Text.Json.Nodes; namespace com.IvanMurzak.ReflectorNet.Json @@ -12,7 +14,8 @@ namespace com.IvanMurzak.ReflectorNet.Json public interface IJsonSchemaConverter { string Id { get; } - JsonNode GetScheme(); - JsonNode GetSchemeRef(); + JsonNode GetSchema(); + JsonNode GetSchemaRef(); + IEnumerable GetDefinedTypes(); } } \ No newline at end of file diff --git a/ReflectorNet/src/Convertor/Json/JsonSchemaConverter.cs b/ReflectorNet/src/Convertor/Json/JsonSchemaConverter.cs new file mode 100644 index 00000000..701257cf --- /dev/null +++ b/ReflectorNet/src/Convertor/Json/JsonSchemaConverter.cs @@ -0,0 +1,27 @@ +/* + * 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.Text.Json.Nodes; +using System.Text.Json.Serialization; +using com.IvanMurzak.ReflectorNet.Utils; + +namespace com.IvanMurzak.ReflectorNet.Json +{ + public abstract class JsonSchemaConverter : JsonConverter, IJsonSchemaConverter + { + public static string StaticId => TypeUtils.GetSchemaTypeId(); + + private static Type[] _emptyTypes = new Type[] { }; + + public virtual string Id => StaticId; + public abstract JsonNode GetSchema(); + public abstract JsonNode GetSchemaRef(); + public virtual IEnumerable GetDefinedTypes() => _emptyTypes; + } +} \ No newline at end of file diff --git a/ReflectorNet/src/Convertor/Json/MethodDataConverter.cs b/ReflectorNet/src/Convertor/Json/MethodDataConverter.cs index 3dd9246f..77957a1c 100644 --- a/ReflectorNet/src/Convertor/Json/MethodDataConverter.cs +++ b/ReflectorNet/src/Convertor/Json/MethodDataConverter.cs @@ -10,7 +10,6 @@ using System.Linq; using System.Text.Json; using System.Text.Json.Nodes; -using System.Text.Json.Serialization; using com.IvanMurzak.ReflectorNet.Model; using com.IvanMurzak.ReflectorNet.Utils; @@ -18,10 +17,8 @@ namespace com.IvanMurzak.ReflectorNet.Json { using JsonSerializer = System.Text.Json.JsonSerializer; - public class MethodDataConverter : JsonConverter, IJsonSchemaConverter + public class MethodDataConverter : JsonSchemaConverter { - public static string StaticId => TypeUtils.GetSchemaTypeId(); - public static JsonNode Schema => new JsonObject { [JsonSchema.Type] = JsonSchema.Object, @@ -116,10 +113,12 @@ public class MethodDataConverter : JsonConverter, IJsonSchemaConvert [JsonSchema.Ref] = JsonSchema.RefValue + StaticId }; - public string Id => StaticId; - - public JsonNode GetSchemeRef() => SchemaRef; - public JsonNode GetScheme() => Schema; + public override JsonNode GetSchemaRef() => SchemaRef; + public override JsonNode GetSchema() => Schema; + public override IEnumerable GetDefinedTypes() + { + yield return typeof(MethodData.Parameter); + } public override MethodData? Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) { diff --git a/ReflectorNet/src/Convertor/Json/SerializedMemberConverter.cs b/ReflectorNet/src/Convertor/Json/SerializedMemberConverter.cs index 817b99ba..e458324f 100644 --- a/ReflectorNet/src/Convertor/Json/SerializedMemberConverter.cs +++ b/ReflectorNet/src/Convertor/Json/SerializedMemberConverter.cs @@ -6,18 +6,17 @@ */ using System; +using System.Collections.Generic; using System.Linq; using System.Text.Json; using System.Text.Json.Nodes; -using System.Text.Json.Serialization; using com.IvanMurzak.ReflectorNet.Model; using com.IvanMurzak.ReflectorNet.Utils; namespace com.IvanMurzak.ReflectorNet.Json { - public class SerializedMemberConverter : JsonConverter, IJsonSchemaConverter + public class SerializedMemberConverter : JsonSchemaConverter, IJsonSchemaConverter { - public static string StaticId => TypeUtils.GetSchemaTypeId(); public static JsonNode Schema => new JsonObject { [JsonSchema.Type] = JsonSchema.Object, @@ -84,15 +83,17 @@ public class SerializedMemberConverter : JsonConverter, IJsonS readonly Reflector _reflector; - public string Id => StaticId; - public SerializedMemberConverter(Reflector reflector) { _reflector = reflector ?? throw new ArgumentNullException(nameof(reflector)); } - public JsonNode GetSchemeRef() => SchemaRef; - public JsonNode GetScheme() => Schema; + public override JsonNode GetSchemaRef() => SchemaRef; + public override JsonNode GetSchema() => Schema; + public override IEnumerable GetDefinedTypes() + { + yield return typeof(SerializedMemberList); + } public override SerializedMember? Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) { diff --git a/ReflectorNet/src/Convertor/Json/SerializedMemberListConverter.cs b/ReflectorNet/src/Convertor/Json/SerializedMemberListConverter.cs index 7c7cf4c7..917d9aed 100644 --- a/ReflectorNet/src/Convertor/Json/SerializedMemberListConverter.cs +++ b/ReflectorNet/src/Convertor/Json/SerializedMemberListConverter.cs @@ -6,17 +6,16 @@ */ using System; +using System.Collections.Generic; using System.Text.Json; using System.Text.Json.Nodes; -using System.Text.Json.Serialization; using com.IvanMurzak.ReflectorNet.Model; using com.IvanMurzak.ReflectorNet.Utils; namespace com.IvanMurzak.ReflectorNet.Json { - public class SerializedMemberListConverter : JsonConverter, IJsonSchemaConverter + public class SerializedMemberListConverter : JsonSchemaConverter, IJsonSchemaConverter { - public static string StaticId => TypeUtils.GetSchemaTypeId(); public static JsonNode Schema => new JsonObject { [JsonSchema.Type] = JsonSchema.Array, @@ -26,19 +25,21 @@ public class SerializedMemberListConverter : JsonConverter } }; - public string Id => StaticId; - public SerializedMemberListConverter(Reflector reflector) { if (reflector == null) throw new ArgumentNullException(nameof(reflector)); } - public JsonNode GetScheme() => Schema; - public JsonNode GetSchemeRef() => new JsonObject + public override JsonNode GetSchema() => Schema; + public override JsonNode GetSchemaRef() => new JsonObject { [JsonSchema.Ref] = JsonSchema.RefValue + Id }; + public override IEnumerable GetDefinedTypes() + { + yield return typeof(SerializedMember); + } public override SerializedMemberList? Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) { diff --git a/ReflectorNet/src/Extension/ExtensionsType.cs b/ReflectorNet/src/Extension/ExtensionsType.cs index 118546ea..25b73db8 100644 --- a/ReflectorNet/src/Extension/ExtensionsType.cs +++ b/ReflectorNet/src/Extension/ExtensionsType.cs @@ -13,7 +13,8 @@ namespace com.IvanMurzak.ReflectorNet { public static class ExtensionsType { - public static JsonNode? GetSchema(this Type type, Reflector reflector, bool justRef = false) => reflector.GetSchema(type, justRef); + public static JsonNode? GetSchema(this Type type, Reflector reflector) => reflector.GetSchema(type); + public static JsonNode? GetSchemaRef(this Type type, Reflector reflector) => reflector.GetSchemaRef(type); public static string GetTypeShortName(this Type? type) => TypeUtils.GetTypeShortName(type); public static string GetTypeName(this Type? type, bool pretty = false) => TypeUtils.GetTypeName(type, pretty); public static string GetTypeId(this Type type) => TypeUtils.GetTypeId(type); diff --git a/ReflectorNet/src/Model/MethodData.cs b/ReflectorNet/src/Model/MethodData.cs index 6a44c356..d50b5e36 100644 --- a/ReflectorNet/src/Model/MethodData.cs +++ b/ReflectorNet/src/Model/MethodData.cs @@ -45,9 +45,13 @@ public MethodData(Reflector reflector, MethodInfo methodInfo, bool justRef = fal ReturnType = methodInfo.ReturnType.GetTypeName(pretty: false); ReturnSchema = methodInfo.ReturnType == typeof(void) ? null - : reflector.GetSchema(methodInfo.ReturnType, justRef: justRef); + : justRef + ? reflector.GetSchemaRef(methodInfo.ReturnType) + : reflector.GetSchema(methodInfo.ReturnType); InputParametersSchema = methodInfo.GetParameters() - ?.Select(parameter => reflector.GetSchema(parameter.ParameterType, justRef: justRef)!) + ?.Select(parameter => justRef + ? reflector.GetSchemaRef(parameter.ParameterType) + : reflector.GetSchema(parameter.ParameterType)) ?.ToList(); } } diff --git a/ReflectorNet/src/Reflector/Reflector.Error.cs b/ReflectorNet/src/Reflector/Reflector.Error.cs index acddf4d5..8113a5aa 100644 --- a/ReflectorNet/src/Reflector/Reflector.Error.cs +++ b/ReflectorNet/src/Reflector/Reflector.Error.cs @@ -44,7 +44,7 @@ public static string NotSupportedInRuntime(Type type) public static string MoreThanOneMethodFound(Reflector reflector, List methods) { - var methodDataList = methods.Select(method => new MethodData(reflector, method, justRef: false)); + var methodDataList = methods.Select(method => new MethodData(reflector, method)); var methodsString = methodDataList.ToJson(reflector, options: null); return @$"[Error] Found more than one method. Only single method should be targeted. Please specify the method name more precisely. diff --git a/ReflectorNet/src/Reflector/Reflector.Json.cs b/ReflectorNet/src/Reflector/Reflector.Json.cs index 04329214..fd3c4b16 100644 --- a/ReflectorNet/src/Reflector/Reflector.Json.cs +++ b/ReflectorNet/src/Reflector/Reflector.Json.cs @@ -33,10 +33,47 @@ public partial class Reflector /// - Error handling: Provides detailed error information for schema generation failures /// /// The type for which to generate the JSON Schema. - /// Whether to generate a compact reference schema (true) or full schema definition (false). Default is false. /// A JsonNode containing the JSON Schema representation of the specified type. - public JsonNode GetSchema(bool justRef = false) - => jsonSchema.GetSchema(this, justRef); + public JsonNode GetSchema() + => jsonSchema.GetSchema(this); + + /// + /// Generates a JSON Schema representation for the specified generic type parameter. + /// This method provides comprehensive schema generation supporting both simple references + /// and full schema definitions with proper type metadata and documentation. + /// + /// Behavior: + /// - Type resolution: Automatically handles nullable types by unwrapping to underlying type + /// - Reference mode: When justRef=true, generates compact $ref schemas for non-primitive types + /// - Full schema mode: When justRef=false, generates complete schema definitions with properties + /// - Primitive optimization: Generates inline schemas for primitive types regardless of justRef setting + /// - Documentation extraction: Includes descriptions from DescriptionAttribute and XML documentation + /// - Recursive handling: Manages complex nested types and generic type parameters + /// - Error handling: Provides detailed error information for schema generation failures + /// + /// The type for which to generate the JSON Schema. + /// A JsonNode containing the JSON Schema representation of the specified type. + public JsonNode GetSchemaRef() + => jsonSchema.GetSchemaRef(this); + + /// + /// Generates a JSON Schema representation for the specified type. + /// This method provides comprehensive schema generation supporting both simple references + /// and full schema definitions with proper type metadata and documentation. + /// + /// Behavior: + /// - Type resolution: Automatically handles nullable types by unwrapping to underlying type + /// - Reference mode: When justRef=true, generates compact $ref schemas for non-primitive types + /// - Full schema mode: When justRef=false, generates complete schema definitions with properties + /// - Primitive optimization: Generates inline schemas for primitive types regardless of justRef setting + /// - Documentation extraction: Includes descriptions from DescriptionAttribute and XML documentation + /// - Recursive handling: Manages complex nested types and generic type parameters + /// - Error handling: Provides detailed error information for schema generation failures + /// + /// The Type for which to generate the JSON Schema. + /// A JsonNode containing the JSON Schema representation of the specified type. + public JsonNode GetSchema(Type type) + => jsonSchema.GetSchema(this, type); /// /// Generates a JSON Schema representation for the specified type. @@ -53,10 +90,9 @@ public JsonNode GetSchema(bool justRef = false) /// - Error handling: Provides detailed error information for schema generation failures /// /// The Type for which to generate the JSON Schema. - /// Whether to generate a compact reference schema (true) or full schema definition (false). Default is false. /// A JsonNode containing the JSON Schema representation of the specified type. - public JsonNode GetSchema(Type type, bool justRef = false) - => jsonSchema.GetSchema(this, type, justRef); + public JsonNode GetSchemaRef(Type type) + => jsonSchema.GetSchemaRef(this, type); /// /// Generates a comprehensive JSON Schema for method parameters, enabling dynamic method invocation @@ -82,8 +118,8 @@ public JsonNode GetSchema(Type type, bool justRef = false) /// The MethodInfo for which to generate the parameter schema. /// Whether to use compact references for complex types. Default is false. /// A JsonNode containing the complete JSON Schema for the method's parameters. - public JsonNode GetArgumentsSchema(MethodInfo methodInfo, bool justRef = false) - => jsonSchema.GetArgumentsSchema(this, methodInfo, justRef); + public JsonNode GetArgumentsSchema(MethodInfo methodInfo, bool justRef = false, JsonObject? defines = null) + => jsonSchema.GetArgumentsSchema(this, methodInfo, justRef, defines); /// /// Generates a comprehensive JSON Schema for the return type of a method. @@ -93,7 +129,7 @@ public JsonNode GetArgumentsSchema(MethodInfo methodInfo, bool justRef = false) /// The MethodInfo for which to generate the return type schema. /// Whether to use compact references for complex types. Default is false. /// A JsonNode containing the complete JSON Schema for the method's return type. - public JsonNode? GetReturnSchema(MethodInfo methodInfo, bool justRef = false) - => jsonSchema.GetReturnSchema(this, methodInfo, justRef); + public JsonNode? GetReturnSchema(MethodInfo methodInfo, bool justRef = false, JsonObject? defines = null) + => jsonSchema.GetReturnSchema(this, methodInfo, justRef, defines); } } diff --git a/ReflectorNet/src/Utils/Json/JsonSchema.Internal.cs b/ReflectorNet/src/Utils/Json/JsonSchema.Internal.cs index 47e5470e..4da64a0f 100644 --- a/ReflectorNet/src/Utils/Json/JsonSchema.Internal.cs +++ b/ReflectorNet/src/Utils/Json/JsonSchema.Internal.cs @@ -97,7 +97,7 @@ void PostprocessFields(JsonNode? node) /// Generate a JSON schema from a type using ReflectorNet's introspection capabilities. /// This method uses the Reflector's converter system to understand the type structure. /// - JsonNode GenerateSchemaFromType(Reflector reflector, Type type) + JsonNode GenerateSchemaFromType(Reflector reflector, Type type, JsonObject defines) { // Handle primitive types if (TypeUtils.IsPrimitive(type)) @@ -111,23 +111,39 @@ JsonNode GenerateSchemaFromType(Reflector reflector, Type type) var itemType = TypeUtils.GetEnumerableItemType(type); if (itemType != null) { - return new JsonObject + var itemTypeId = itemType.GetSchemaTypeId(); + if (defines.ContainsKey(itemTypeId) == false) { - [Type] = Array, - [Items] = GetSchema(reflector, itemType, justRef: !TypeUtils.IsPrimitive(itemType)) - }; + // Add placeholder first to prevent infinite recursion + defines[itemTypeId] = new JsonObject { [Type] = Object }; + defines[itemTypeId] = GetSchema(reflector, itemType, defines: defines); + } + var typeId = type.GetSchemaTypeId(); + if (defines.ContainsKey(typeId) == false) + { + var isItemPrimitive = TypeUtils.IsPrimitive(itemType); + // Add placeholder first to prevent infinite recursion + defines[typeId] = new JsonObject + { + [Type] = Array, + [Items] = isItemPrimitive + ? GetSchema(reflector, itemType, defines: defines) + : GetSchemaRef(reflector, itemType) + }; + } + return GetSchemaRef(reflector, type); } } // Handle regular objects by introspecting their fields and properties + var properties = new JsonObject(); + var required = new JsonArray(); var schema = new JsonObject { - [Type] = Object, - [Properties] = new JsonObject() + [Type] = Object }; - var properties = schema[Properties] as JsonObject; - var required = new JsonArray(); + defines ??= new(); // Get serializable fields var fields = reflector.GetSerializableFields(type); @@ -138,18 +154,32 @@ JsonNode GenerateSchemaFromType(Reflector reflector, Type type) if (field.GetCustomAttribute() != null) continue; + var underlyingType = Nullable.GetUnderlyingType(field.FieldType); + var isPrimitive = TypeUtils.IsPrimitive(underlyingType ?? field.FieldType); var fieldName = field.GetCustomAttribute()?.Name ?? field.Name; - var fieldSchema = GetSchema(reflector, field.FieldType, justRef: !TypeUtils.IsPrimitive(field.FieldType)); + var schemaRef = isPrimitive + ? GetSchema(reflector, field.FieldType, defines: defines) + : GetSchemaRef(reflector, field.FieldType); + + if (!isPrimitive) + { + var typeId = field.FieldType.GetSchemaTypeId(); + if (!defines.ContainsKey(typeId)) + { + // Add placeholder first to prevent infinite recursion + defines[typeId] = new JsonObject { [Type] = Object }; + defines[typeId] = GetSchema(reflector, field.FieldType, defines: defines); + } + } // Add description if available var description = TypeUtils.GetFieldDescription(field); - if (!string.IsNullOrEmpty(description) && fieldSchema is JsonObject fieldSchemaObj) + if (!string.IsNullOrEmpty(description) && schemaRef is JsonObject fieldSchemaObj) fieldSchemaObj[Description] = JsonValue.Create(description); - properties![fieldName] = fieldSchema; + properties[fieldName] = schemaRef; // Fields are typically required unless they are nullable - var underlyingType = Nullable.GetUnderlyingType(field.FieldType); if (underlyingType == null && !field.FieldType.IsClass) required.Add(fieldName); } @@ -164,18 +194,32 @@ JsonNode GenerateSchemaFromType(Reflector reflector, Type type) if (prop.GetCustomAttribute() != null) continue; + var underlyingType = Nullable.GetUnderlyingType(prop.PropertyType); + var isPrimitive = TypeUtils.IsPrimitive(underlyingType ?? prop.PropertyType); var propName = prop.GetCustomAttribute()?.Name ?? prop.Name; - var propSchema = GetSchema(reflector, prop.PropertyType, justRef: !TypeUtils.IsPrimitive(prop.PropertyType)); + var schemaRef = isPrimitive + ? GetSchema(reflector, prop.PropertyType, defines: defines) + : GetSchemaRef(reflector, prop.PropertyType); + + if (!isPrimitive) + { + var typeId = prop.PropertyType.GetSchemaTypeId(); + if (!defines.ContainsKey(typeId)) + { + // Add placeholder first to prevent infinite recursion + defines[typeId] = new JsonObject { [Type] = Object }; + defines[typeId] = GetSchema(reflector, prop.PropertyType, defines: defines); + } + } // Add description if available var description = TypeUtils.GetPropertyDescription(prop); - if (!string.IsNullOrEmpty(description) && propSchema is JsonObject propSchemaObj) + if (!string.IsNullOrEmpty(description) && schemaRef is JsonObject propSchemaObj) propSchemaObj[Description] = JsonValue.Create(description); - properties![propName] = propSchema; + properties![propName] = schemaRef; // Properties are required if they are value types and not nullable - var underlyingType = Nullable.GetUnderlyingType(prop.PropertyType); if (underlyingType == null && !prop.PropertyType.IsClass && prop.CanWrite) required.Add(propName); } @@ -185,6 +229,9 @@ JsonNode GenerateSchemaFromType(Reflector reflector, Type type) if (required.Count > 0) schema[Required] = required; + if (properties.Count > 0) + schema[Properties] = properties; + return schema; } @@ -226,8 +273,8 @@ JsonNode GeneratePrimitiveSchema(Type type) enumValues.Add(JsonValue.Create(enumValue.ToString())); } - return new JsonObject - { + return new JsonObject + { [Type] = String, ["enum"] = enumValues }; diff --git a/ReflectorNet/src/Utils/Json/JsonSchema.cs b/ReflectorNet/src/Utils/Json/JsonSchema.cs index 210b825e..8c5589ef 100644 --- a/ReflectorNet/src/Utils/Json/JsonSchema.cs +++ b/ReflectorNet/src/Utils/Json/JsonSchema.cs @@ -7,7 +7,6 @@ using System; using System.Collections.Generic; -using System.ComponentModel; using System.Linq; using System.Reflection; using System.Text.Json.Nodes; @@ -67,11 +66,37 @@ public partial class JsonSchema /// /// The type for which to generate the JSON Schema. /// The Reflector instance used for type analysis and converter access. - /// Whether to generate a compact reference schema for non-primitive types. Default is false. /// A JsonNode containing the JSON Schema representation of the specified type. /// Thrown when schema generation fails for the specified type. - public JsonNode GetSchema(Reflector reflector, bool justRef = false) - => GetSchema(reflector, typeof(T), justRef); + public JsonNode GetSchema(Reflector reflector, JsonObject? defines = null) + => GetSchema(reflector, typeof(T), defines); + + /// + /// Generates a comprehensive JSON Schema representation for the specified generic type. + /// This method provides flexible schema generation supporting both full schema definitions + /// and compact reference schemas, with intelligent handling of primitive vs complex types. + /// + /// Schema Generation Features: + /// - Type unwrapping: Automatically handles nullable types by unwrapping to underlying type + /// - Converter integration: Leverages registered JsonConverter implementations for custom schema logic + /// - Reference optimization: Generates compact $ref schemas for complex types when justRef=true + /// - Documentation extraction: Includes descriptions from DescriptionAttribute and type metadata + /// - Error handling: Provides detailed error information for schema generation failures + /// - Primitive handling: Optimizes schema generation for built-in types + /// + /// Schema Modes: + /// - Full schema (justRef=false): Complete schema definition with all properties and nested types + /// - Reference schema (justRef=true): Compact $ref pointing to type definition in $defs + /// - Primitive inline: Primitive types are always inlined regardless of justRef setting + /// + /// Generated schemas conform to JSON Schema Draft 2020-12 specification. + /// + /// The type for which to generate the JSON Schema. + /// The Reflector instance used for type analysis and converter access. + /// A JsonNode containing the JSON Schema representation of the specified type. + /// Thrown when schema generation fails for the specified type. + public JsonNode GetSchemaRef(Reflector reflector) + => GetSchemaRef(reflector, typeof(T)); /// /// Generates a comprehensive JSON Schema representation for the specified type. @@ -95,51 +120,66 @@ public JsonNode GetSchema(Reflector reflector, bool justRef = false) /// /// The Reflector instance used for type analysis and converter access. /// The Type for which to generate the JSON Schema. - /// Whether to generate a compact reference schema for non-primitive types. Default is false. /// A JsonNode containing the JSON Schema representation of the specified type. /// Thrown when schema generation fails for the specified type. - public JsonNode GetSchema(Reflector reflector, Type type, bool justRef = false) + public JsonNode GetSchema(Reflector reflector, Type type, JsonObject? defines = null) { // Handle nullable types type = Nullable.GetUnderlyingType(type) ?? type; - var schema = default(JsonNode); + var typeId = type.GetSchemaTypeId(); try { + var definesNeeded = defines == null; + defines ??= new JsonObject(); + + var defineContainsType = defines.ContainsKey(typeId); + var jsonConverter = reflector.JsonSerializerOptions.GetConverter(type); if (jsonConverter is IJsonSchemaConverter schemeConvertor) { - schema = justRef - ? schemeConvertor.GetSchemeRef() - : schemeConvertor.GetScheme(); + // Add placeholder to prevent infinite recursion + if (definesNeeded && !defineContainsType) + defines[typeId] = new JsonObject { [Type] = Object }; + + schema = schemeConvertor.GetSchema(); + + if (definesNeeded && !defineContainsType) + defines[typeId] = schema; + + foreach (var defType in schemeConvertor.GetDefinedTypes()) + { + var defTypeId = defType.GetSchemaTypeId(); + if (defines.ContainsKey(defTypeId)) + continue; + + // Add placeholder to prevent infinite recursion + defines[defTypeId] = new JsonObject { [Type] = Object }; + + var def = GetSchema(reflector, defType, defines); + defines[defTypeId] = def; + } } else { - if (justRef && !TypeUtils.IsPrimitive(type)) - { - // If justRef is true and the type is not primitive, we return a reference schema - schema = new JsonObject - { - [Ref] = RefValue + type.GetSchemaTypeId() - }; + // Add placeholder to prevent infinite recursion + // if (definesNeeded && !defineContainsType) + // defines[typeId] = new JsonObject { [Type] = Object }; - // Get description from the type if available - var description = TypeUtils.GetDescription(type); - if (!string.IsNullOrEmpty(description)) - schema[Description] = JsonValue.Create(description); - } - else - { - // If not justRef, we generate the full schema - schema = GenerateSchemaFromType(reflector, type); + schema = GenerateSchemaFromType(reflector, type, defines); - // Get description from the type if available - var description = TypeUtils.GetDescription(type); - if (!string.IsNullOrEmpty(description)) - schema[Description] = JsonValue.Create(description); - } + // if (definesNeeded && !defineContainsType) + // defines[typeId] = schema; + + if (definesNeeded && defines.Count > 0) + schema[Defs] = defines; } + + // Get description from the type if available + var description = TypeUtils.GetDescription(type); + if (!string.IsNullOrEmpty(description)) + schema[Description] = JsonValue.Create(description); } catch (Exception ex) { @@ -155,13 +195,83 @@ public JsonNode GetSchema(Reflector reflector, Type type, bool justRef = false) PostprocessFields(schema); - if (schema is JsonObject parameterSchemaObject) + if (schema is not JsonObject) + { + return new JsonObject() + { + [Error] = $"Unexpected schema type for '{type.GetTypeName(pretty: false)}'. Json Schema type: {schema.GetType().GetTypeName()}" + }; + } + return schema; + } + + /// + /// Generates a comprehensive JSON Schema representation for the specified type. + /// This method provides flexible schema generation supporting both full schema definitions + /// and compact reference schemas, with intelligent handling of primitive vs complex types. + /// + /// Schema Generation Features: + /// - Type unwrapping: Automatically handles nullable types by unwrapping to underlying type + /// - Converter integration: Leverages registered JsonConverter implementations for custom schema logic + /// - Reference optimization: Generates compact $ref schemas for complex types when justRef=true + /// - Documentation extraction: Includes descriptions from DescriptionAttribute and type metadata + /// - Error handling: Provides detailed error information for schema generation failures + /// - Primitive handling: Optimizes schema generation for built-in types + /// + /// Schema Modes: + /// - Full schema (justRef=false): Complete schema definition with all properties and nested types + /// - Reference schema (justRef=true): Compact $ref pointing to type definition in $defs + /// - Primitive inline: Primitive types are always inlined regardless of justRef setting + /// + /// Generated schemas conform to JSON Schema Draft 2020-12 specification. + /// + /// The Reflector instance used for type analysis and converter access. + /// The Type for which to generate the JSON Schema. + /// A JsonNode containing the JSON Schema representation of the specified type. + /// Thrown when schema generation fails for the specified type. + public JsonNode GetSchemaRef(Reflector reflector, Type type) + { + // Handle nullable types + type = Nullable.GetUnderlyingType(type) ?? type; + var schema = default(JsonNode); + + try + { + var jsonConverter = reflector.JsonSerializerOptions.GetConverter(type); + if (jsonConverter is IJsonSchemaConverter schemeConvertor) + { + schema = schemeConvertor.GetSchemaRef(); + } + else + { + var typeId = type.GetSchemaTypeId(); + // If justRef is true and the type is not primitive, we return a reference schema + schema = new JsonObject + { + [Ref] = RefValue + typeId + }; + } + + // Get description from the type if available + var description = TypeUtils.GetDescription(type); + if (!string.IsNullOrEmpty(description)) + schema[Description] = JsonValue.Create(description); + } + catch (Exception ex) { - var propertyDescription = type.GetCustomAttribute()?.Description; - if (!string.IsNullOrEmpty(propertyDescription)) - parameterSchemaObject[Description] = JsonValue.Create(propertyDescription); + // Handle exceptions and return null or an error message + return new JsonObject() + { + [Error] = $"Failed to get schema for '{type.GetTypeName(pretty: false)}':\n{ex.Message}\n{ex.StackTrace}\n" + }; } - else + + if (schema == null) + throw new InvalidOperationException($"Failed to get schema for type '{type.GetTypeName(pretty: false)}'."); + + PostprocessFields(schema); + + if (schema is not JsonObject) { return new JsonObject() { @@ -181,7 +291,7 @@ public JsonNode GetSchema(Reflector reflector, Type type, bool justRef = false) /// The type to analyze for nested types. /// The JsonObject to accumulate type definitions. /// Set of already visited types to prevent infinite recursion. - private void CollectNestedTypes(Reflector reflector, Type type, JsonObject defines, HashSet? visitedTypes = null) + void CollectNestedTypes(Reflector reflector, Type type, HashSet? visitedTypes = null) { visitedTypes ??= new HashSet(); @@ -195,29 +305,16 @@ private void CollectNestedTypes(Reflector reflector, Type type, JsonObject defin if (TypeUtils.IsPrimitive(type)) return; - var typeId = type.GetSchemaTypeId(); - - // Add the type definition if not already present - if (!defines.ContainsKey(typeId)) - { - var fullSchema = GetSchema(reflector, type, justRef: false); - defines[typeId] = fullSchema; - } - // Handle generic type arguments (e.g., List, Dictionary) foreach (var genericArgument in TypeUtils.GetGenericTypes(type)) - { - CollectNestedTypes(reflector, genericArgument, defines, visitedTypes); - } + CollectNestedTypes(reflector, genericArgument, visitedTypes); // Handle collection item types (e.g., T[], List, IEnumerable) if (TypeUtils.IsIEnumerable(type)) { var itemType = TypeUtils.GetEnumerableItemType(type); if (itemType != null) - { - CollectNestedTypes(reflector, itemType, defines, visitedTypes); - } + CollectNestedTypes(reflector, itemType, visitedTypes); } // Handle properties and fields to find nested types @@ -225,18 +322,14 @@ private void CollectNestedTypes(Reflector reflector, Type type, JsonObject defin if (properties != null) { foreach (var prop in properties) - { - CollectNestedTypes(reflector, prop.PropertyType, defines, visitedTypes); - } + CollectNestedTypes(reflector, prop.PropertyType, visitedTypes); } var fields = reflector.GetSerializableFields(type); if (fields != null) { foreach (var field in fields) - { - CollectNestedTypes(reflector, field.FieldType, defines, visitedTypes); - } + CollectNestedTypes(reflector, field.FieldType, visitedTypes); } } @@ -278,7 +371,7 @@ private void CollectNestedTypes(Reflector reflector, Type type, JsonObject defin /// Whether to use compact references for complex types in parameter schemas. Default is false. /// A JsonNode containing the complete JSON Schema for the method's parameters. /// Thrown when method parameter is null. - public JsonNode GetArgumentsSchema(Reflector reflector, MethodInfo method, bool justRef = false) + public JsonNode GetArgumentsSchema(Reflector reflector, MethodInfo method, bool justRef = false, JsonObject? defines = null) { if (method == null) throw new ArgumentNullException(nameof(method)); @@ -290,11 +383,11 @@ public JsonNode GetArgumentsSchema(Reflector reflector, MethodInfo method, bool var types = parameters .Select(p => ( type: p.ParameterType, - name: p.Name, + name: p.Name ?? throw new InvalidOperationException($"Parameter in method '{method.Name}' has no name."), description: TypeUtils.GetDescription(p), required: !p.HasDefaultValue)); - return GenerateSchema(reflector, types, justRef); + return GenerateSchema(reflector, types, justRef, defines); } /// @@ -336,7 +429,7 @@ public JsonNode GetArgumentsSchema(Reflector reflector, MethodInfo method, bool /// Whether to use compact references for complex types. Default is false. /// A JsonNode containing the JSON Schema for the method's return type, or null for void/Task/ValueTask. /// Thrown when methodInfo parameter is null. - public JsonNode? GetReturnSchema(Reflector reflector, MethodInfo methodInfo, bool justRef = false) + public JsonNode? GetReturnSchema(Reflector reflector, MethodInfo methodInfo, bool justRef = false, JsonObject? defines = null) { if (methodInfo == null) throw new ArgumentNullException(nameof(methodInfo)); @@ -349,82 +442,61 @@ public JsonNode GetArgumentsSchema(Reflector reflector, MethodInfo method, bool returnType == typeof(ValueTask)) return null; - // First, check if the return type itself is a nullable Task/ValueTask (e.g., Task? or ValueTask?) - var isTaskNullable = false; -#if NET5_0_OR_GREATER - try - { - var nullabilityContext = new System.Reflection.NullabilityInfoContext(); - var returnTypeNullabilityInfo = nullabilityContext.Create(methodInfo.ReturnParameter); - - // Check if the Task/ValueTask itself is nullable - if (returnType.IsGenericType && - (returnType.GetGenericTypeDefinition() == typeof(Task<>) || returnType.GetGenericTypeDefinition() == typeof(ValueTask<>))) - { - isTaskNullable = returnTypeNullabilityInfo.ReadState == System.Reflection.NullabilityState.Nullable; - } - } - catch - { - // If we can't determine nullability, assume not nullable - } -#endif + // Check if return type is Task or ValueTask + var isAsyncWrapper = returnType.IsGenericType && + (returnType.GetGenericTypeDefinition() == typeof(Task<>) || + returnType.GetGenericTypeDefinition() == typeof(ValueTask<>)); - // Unwrap Task/ValueTask first, then compute nullability on the inner T - var unwrappedType = returnType; - var isNullable = isTaskNullable; // If the Task itself is nullable, the result is also nullable + // Unwrap Task/ValueTask to get the inner T + var unwrappedType = isAsyncWrapper + ? returnType.GetGenericArguments()[0] + : returnType; - if (unwrappedType.IsGenericType) - { - var genericDefinition = unwrappedType.GetGenericTypeDefinition(); - if (genericDefinition == typeof(Task<>) || genericDefinition == typeof(ValueTask<>)) - { - unwrappedType = unwrappedType.GetGenericArguments()[0]; - } - } + var isNullable = false; - // Recompute nullability after unwrapping async wrappers + // Check for Nullable (value types like int?, DateTime?) var nullableUnderlyingType = Nullable.GetUnderlyingType(unwrappedType); if (nullableUnderlyingType != null) { + // This handles Nullable for value types (e.g., Task, int?) isNullable = true; unwrappedType = nullableUnderlyingType; } + else if (!unwrappedType.IsValueType) + { + // For reference types, check nullability using NullabilityInfoContext +#if NET5_0_OR_GREATER + try + { + var nullabilityContext = new NullabilityInfoContext(); + var nullabilityInfo = nullabilityContext.Create(methodInfo.ReturnParameter); - // // Check for nullable reference types using NullabilityInfoContext - // if (!isNullable && (unwrappedType.IsClass || unwrappedType.IsInterface || unwrappedType.IsArray)) - // { - // try - // { - // #if NET5_0_OR_GREATER - // var nullabilityContext = new System.Reflection.NullabilityInfoContext(); - // var nullabilityInfo = nullabilityContext.Create(methodInfo.ReturnParameter); - - // // For Task or ValueTask, check the generic argument's nullability - // if (returnType.IsGenericType) - // { - // var genericDef = returnType.GetGenericTypeDefinition(); - // if (genericDef == typeof(Task<>) || genericDef == typeof(ValueTask<>)) - // { - // isNullable = isNullable || (nullabilityInfo.GenericTypeArguments.Length > 0 && - // nullabilityInfo.GenericTypeArguments[0].ReadState == System.Reflection.NullabilityState.Nullable); - // } - // else - // { - // isNullable = nullabilityInfo.ReadState == System.Reflection.NullabilityState.Nullable; - // } - // } - // else - // { - // isNullable = nullabilityInfo.ReadState == System.Reflection.NullabilityState.Nullable; - // } - // #endif - // } - // catch - // { - // // If we can't determine nullability, assume not nullable - // } - // } + // If the return type is Task or ValueTask, check the generic argument nullability + if (isAsyncWrapper) + { + // Check the nullability of the T inside Task or ValueTask + if (nullabilityInfo.GenericTypeArguments.Length > 0) + { + isNullable = nullabilityInfo.GenericTypeArguments[0].ReadState == NullabilityState.Nullable; + } + } + else + { + // For non-async types, check the return type directly + isNullable = nullabilityInfo.ReadState == NullabilityState.Nullable; + } + } + catch (Exception) + { + // If we can't determine nullability, assume not nullable + isNullable = false; + } +#else + // For .NET Standard 2.1 and earlier, we cannot determine reference type nullability + // Assume not nullable by default + isNullable = false; +#endif + } var types = new (Type type, string name, string? description, bool required)[] { @@ -432,16 +504,31 @@ public JsonNode GetArgumentsSchema(Reflector reflector, MethodInfo method, bool type: unwrappedType, name: Result, description: null, - required: true + required: isNullable == false ) }; - return GenerateSchema(reflector, types, justRef); + return GenerateSchema(reflector, types, justRef, defines); } - public JsonNode GenerateSchema(Reflector reflector, IEnumerable<(Type type, string name, string? description, bool required)> types, bool justRef = false) + /// + /// Generates a JSON Schema for a collection of types, typically representing method parameters. + /// This method constructs a schema object with properties for each type, handling required fields, + /// + /// + /// + /// + /// If it is not null, it will be used to fill new definitions without injecting it into the schema. Needed for recursive call case, when need to generate a single top level definitions object. + /// + public JsonNode GenerateSchema( + Reflector reflector, + IEnumerable<(Type type, string name, string? description, bool required)> types, + bool justRef = false, + JsonObject? defines = null) { + var needToAddDefines = defines == null; + defines ??= new(); + var properties = new JsonObject(); - var defines = new JsonObject(); var required = new JsonArray(); // Create a schema object manually @@ -458,30 +545,25 @@ public JsonNode GenerateSchema(Reflector reflector, IEnumerable<(Type type, stri var isPrimitive = TypeUtils.IsPrimitive(parameter.type); if (isPrimitive) { - parameterSchema = GetSchema(reflector, parameter.type, justRef: justRef); - if (parameterSchema == null) - continue; + parameterSchema = GetSchema(reflector, parameter.type, defines: defines); } else { - var typeId = parameter.type.GetTypeId(); - if (defines.ContainsKey(typeId)) - { - parameterSchema = GetSchema(reflector, parameter.type, justRef: true); - } - else + parameterSchema = GetSchemaRef(reflector, parameter.type); + + var typeId = parameter.type.GetSchemaTypeId(); + if (!defines.ContainsKey(typeId)) { - var fullSchema = GetSchema(reflector, parameter.type, justRef: false); + var fullSchema = GetSchema(reflector, parameter.type, defines: defines); if (fullSchema == null) continue; defines[typeId] = fullSchema; - parameterSchema = GetSchema(reflector, parameter.type, justRef: true); } - - if (parameterSchema == null) - continue; } + if (parameterSchema == null) + continue; + properties[parameter.name!] = parameterSchema; if (parameterSchema is JsonObject parameterSchemaObject) @@ -501,17 +583,17 @@ public JsonNode GenerateSchema(Reflector reflector, IEnumerable<(Type type, stri if (TypeUtils.IsPrimitive(genericArgument)) continue; - var typeId = genericArgument.GetTypeId(); + var typeId = genericArgument.GetSchemaTypeId(); if (defines.ContainsKey(typeId)) continue; - var genericSchema = GetSchema(reflector, genericArgument, justRef: false); + var genericSchema = GetSchema(reflector, genericArgument, defines: defines); if (genericSchema != null) defines[typeId] = genericSchema; } } - if (defines.Count > 0) + if (defines.Count > 0 && needToAddDefines) schema[Defs] = defines; if (required.Count > 0) schema[Required] = required; From 09db9f5aebf72d9c968374ec7a4d5a8380483274 Mon Sep 17 00:00:00 2001 From: Ivan Murzak Date: Wed, 22 Oct 2025 00:31:58 -0700 Subject: [PATCH 19/24] fix: improve nullability handling for method return types in schema generation --- .../src/Utils/Json/JsonSchema.Internal.cs | 20 ++++++++++++------- ReflectorNet/src/Utils/Json/JsonSchema.cs | 10 ++-------- 2 files changed, 15 insertions(+), 15 deletions(-) diff --git a/ReflectorNet/src/Utils/Json/JsonSchema.Internal.cs b/ReflectorNet/src/Utils/Json/JsonSchema.Internal.cs index 4da64a0f..363d181f 100644 --- a/ReflectorNet/src/Utils/Json/JsonSchema.Internal.cs +++ b/ReflectorNet/src/Utils/Json/JsonSchema.Internal.cs @@ -112,16 +112,18 @@ JsonNode GenerateSchemaFromType(Reflector reflector, Type type, JsonObject defin if (itemType != null) { var itemTypeId = itemType.GetSchemaTypeId(); - if (defines.ContainsKey(itemTypeId) == false) + var isItemPrimitive = TypeUtils.IsPrimitive(itemType); + + if (!isItemPrimitive && defines.ContainsKey(itemTypeId) == false) { // Add placeholder first to prevent infinite recursion defines[itemTypeId] = new JsonObject { [Type] = Object }; defines[itemTypeId] = GetSchema(reflector, itemType, defines: defines); } + var typeId = type.GetSchemaTypeId(); if (defines.ContainsKey(typeId) == false) { - var isItemPrimitive = TypeUtils.IsPrimitive(itemType); // Add placeholder first to prevent infinite recursion defines[typeId] = new JsonObject { @@ -131,17 +133,21 @@ JsonNode GenerateSchemaFromType(Reflector reflector, Type type, JsonObject defin : GetSchemaRef(reflector, itemType) }; } - return GetSchemaRef(reflector, type); + + return new JsonObject + { + [Type] = Array, + [Items] = isItemPrimitive + ? GetSchema(reflector, itemType, defines: defines) + : GetSchemaRef(reflector, itemType) + }; } } // Handle regular objects by introspecting their fields and properties var properties = new JsonObject(); var required = new JsonArray(); - var schema = new JsonObject - { - [Type] = Object - }; + var schema = new JsonObject { [Type] = Object }; defines ??= new(); diff --git a/ReflectorNet/src/Utils/Json/JsonSchema.cs b/ReflectorNet/src/Utils/Json/JsonSchema.cs index 8c5589ef..568ed61d 100644 --- a/ReflectorNet/src/Utils/Json/JsonSchema.cs +++ b/ReflectorNet/src/Utils/Json/JsonSchema.cs @@ -471,19 +471,13 @@ public JsonNode GetArgumentsSchema(Reflector reflector, MethodInfo method, bool var nullabilityContext = new NullabilityInfoContext(); var nullabilityInfo = nullabilityContext.Create(methodInfo.ReturnParameter); + isNullable = nullabilityInfo.ReadState == NullabilityState.Nullable; // If the return type is Task or ValueTask, check the generic argument nullability if (isAsyncWrapper) { // Check the nullability of the T inside Task or ValueTask if (nullabilityInfo.GenericTypeArguments.Length > 0) - { - isNullable = nullabilityInfo.GenericTypeArguments[0].ReadState == NullabilityState.Nullable; - } - } - else - { - // For non-async types, check the return type directly - isNullable = nullabilityInfo.ReadState == NullabilityState.Nullable; + isNullable |= nullabilityInfo.GenericTypeArguments[0].ReadState == NullabilityState.Nullable; } } catch (Exception) From b3db32df6f1bf40b5032818dbff59360f2537a56 Mon Sep 17 00:00:00 2001 From: Ivan Murzak Date: Wed, 22 Oct 2025 02:15:05 -0700 Subject: [PATCH 20/24] Update ReflectorNet/src/Convertor/Json/JsonSchemaConverter.cs Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- ReflectorNet/src/Convertor/Json/JsonSchemaConverter.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/ReflectorNet/src/Convertor/Json/JsonSchemaConverter.cs b/ReflectorNet/src/Convertor/Json/JsonSchemaConverter.cs index 701257cf..43824ec7 100644 --- a/ReflectorNet/src/Convertor/Json/JsonSchemaConverter.cs +++ b/ReflectorNet/src/Convertor/Json/JsonSchemaConverter.cs @@ -17,7 +17,7 @@ public abstract class JsonSchemaConverter : JsonConverter, IJsonSchemaConv { public static string StaticId => TypeUtils.GetSchemaTypeId(); - private static Type[] _emptyTypes = new Type[] { }; + private static readonly Type[] _emptyTypes = Array.Empty(); public virtual string Id => StaticId; public abstract JsonNode GetSchema(); From 9c8265bbfd8e5f1f971b68eb70fa2066cce539f4 Mon Sep 17 00:00:00 2001 From: Ivan Murzak Date: Wed, 22 Oct 2025 02:15:32 -0700 Subject: [PATCH 21/24] Update ReflectorNet/src/Utils/Json/JsonSchema.cs Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- ReflectorNet/src/Utils/Json/JsonSchema.cs | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/ReflectorNet/src/Utils/Json/JsonSchema.cs b/ReflectorNet/src/Utils/Json/JsonSchema.cs index 568ed61d..44cdeda4 100644 --- a/ReflectorNet/src/Utils/Json/JsonSchema.cs +++ b/ReflectorNet/src/Utils/Json/JsonSchema.cs @@ -291,6 +291,13 @@ public JsonNode GetSchemaRef(Reflector reflector, Type type) /// The type to analyze for nested types. /// The JsonObject to accumulate type definitions. /// Set of already visited types to prevent infinite recursion. + /// + /// This method is recursive: it calls itself for each generic argument, collection item type, + /// property type, and field type found within the provided . To prevent + /// infinite recursion in the case of cyclic type references, it maintains a + /// set and skips types that have already been processed. The recursion ensures that all nested, + /// non-primitive types are discovered and can be included in the schema definitions. + /// void CollectNestedTypes(Reflector reflector, Type type, HashSet? visitedTypes = null) { visitedTypes ??= new HashSet(); From 938f1fc4a81abce30a5c20c32bec0e79390fb52d Mon Sep 17 00:00:00 2001 From: Ivan Murzak Date: Wed, 22 Oct 2025 02:15:48 -0700 Subject: [PATCH 22/24] Update ReflectorNet.Tests/SchemaTests/TestType.cs Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- ReflectorNet.Tests/SchemaTests/TestType.cs | 7 +------ 1 file changed, 1 insertion(+), 6 deletions(-) diff --git a/ReflectorNet.Tests/SchemaTests/TestType.cs b/ReflectorNet.Tests/SchemaTests/TestType.cs index 34be46ea..c5cb9ff5 100644 --- a/ReflectorNet.Tests/SchemaTests/TestType.cs +++ b/ReflectorNet.Tests/SchemaTests/TestType.cs @@ -66,12 +66,7 @@ void TestTypeName(Type type) Assert.Equal(type, TypeUtils.GetType(typeName)); } - // [Fact] - // public void GetTypeName_CurrentAssembly() - // { - // TestTypeName(typeof(Model.ParentClass)); - // TestTypeName(typeof(Model.ParentClass.NestedClass)); - // } + [Fact] public void GetTypeName_OuterAssembly() From f664124b7025b69d18a8c2862808fdeb43ad85f3 Mon Sep 17 00:00:00 2001 From: Ivan Murzak Date: Wed, 22 Oct 2025 02:16:30 -0700 Subject: [PATCH 23/24] refactor: remove redundant placeholder comments in schema generation logic --- ReflectorNet/src/Utils/Json/JsonSchema.cs | 7 ------- 1 file changed, 7 deletions(-) diff --git a/ReflectorNet/src/Utils/Json/JsonSchema.cs b/ReflectorNet/src/Utils/Json/JsonSchema.cs index 44cdeda4..849bcc7b 100644 --- a/ReflectorNet/src/Utils/Json/JsonSchema.cs +++ b/ReflectorNet/src/Utils/Json/JsonSchema.cs @@ -163,15 +163,8 @@ public JsonNode GetSchema(Reflector reflector, Type type, JsonObject? defines = } else { - // Add placeholder to prevent infinite recursion - // if (definesNeeded && !defineContainsType) - // defines[typeId] = new JsonObject { [Type] = Object }; - schema = GenerateSchemaFromType(reflector, type, defines); - // if (definesNeeded && !defineContainsType) - // defines[typeId] = schema; - if (definesNeeded && defines.Count > 0) schema[Defs] = defines; } From 67fac976a9db4cabc3f3b3d411b1c7a4cf46fe57 Mon Sep 17 00:00:00 2001 From: Ivan Murzak Date: Wed, 22 Oct 2025 02:27:11 -0700 Subject: [PATCH 24/24] fix: enhance null check for schema definitions in JsonSchema generation --- ReflectorNet.Tests/SchemaTests/SchemaTestBase.cs | 2 +- ReflectorNet/src/Utils/Json/JsonSchema.cs | 3 ++- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/ReflectorNet.Tests/SchemaTests/SchemaTestBase.cs b/ReflectorNet.Tests/SchemaTests/SchemaTestBase.cs index cbec0aea..845a9ac6 100644 --- a/ReflectorNet.Tests/SchemaTests/SchemaTestBase.cs +++ b/ReflectorNet.Tests/SchemaTests/SchemaTestBase.cs @@ -153,7 +153,7 @@ protected void AssertResultNotRequired(JsonNode schema) } /// - /// Asserts that "result" IS in the required array (for non-nullable types + /// Asserts that "result" IS in the required array (for non-nullable types) /// protected void AssertResultRequired(JsonNode schema) { diff --git a/ReflectorNet/src/Utils/Json/JsonSchema.cs b/ReflectorNet/src/Utils/Json/JsonSchema.cs index 849bcc7b..455f5d20 100644 --- a/ReflectorNet/src/Utils/Json/JsonSchema.cs +++ b/ReflectorNet/src/Utils/Json/JsonSchema.cs @@ -158,7 +158,8 @@ public JsonNode GetSchema(Reflector reflector, Type type, JsonObject? defines = defines[defTypeId] = new JsonObject { [Type] = Object }; var def = GetSchema(reflector, defType, defines); - defines[defTypeId] = def; + if (def != null) + defines[defTypeId] = def; } } else