diff --git a/ConsoleApp/ConsoleApp.csproj b/ConsoleApp/ConsoleApp.csproj index 99a3af7..42d5456 100644 --- a/ConsoleApp/ConsoleApp.csproj +++ b/ConsoleApp/ConsoleApp.csproj @@ -13,7 +13,7 @@ - + diff --git a/ReflectorNet.Tests.OuterAssembly/ReflectorNet.Tests.OuterAssembly.csproj b/ReflectorNet.Tests.OuterAssembly/ReflectorNet.Tests.OuterAssembly.csproj index aceedd7..51ca04f 100644 --- a/ReflectorNet.Tests.OuterAssembly/ReflectorNet.Tests.OuterAssembly.csproj +++ b/ReflectorNet.Tests.OuterAssembly/ReflectorNet.Tests.OuterAssembly.csproj @@ -8,7 +8,7 @@ - + diff --git a/ReflectorNet.Tests/ReflectorNet.Tests.csproj b/ReflectorNet.Tests/ReflectorNet.Tests.csproj index a808c7b..8b3ce88 100644 --- a/ReflectorNet.Tests/ReflectorNet.Tests.csproj +++ b/ReflectorNet.Tests/ReflectorNet.Tests.csproj @@ -8,10 +8,10 @@ - - + + - + diff --git a/ReflectorNet.Tests/SchemaTests/CollectionsTests.cs b/ReflectorNet.Tests/SchemaTests/CollectionsTests.cs index 81689cb..30868c5 100644 --- a/ReflectorNet.Tests/SchemaTests/CollectionsTests.cs +++ b/ReflectorNet.Tests/SchemaTests/CollectionsTests.cs @@ -63,9 +63,18 @@ public void GetTypeId_SimpleArray_ShouldAppendArray() // Assert Assert.NotNull(result); Assert.NotNull(result[JsonSchema.Type]); - Assert.Equal(JsonSchema.Array, result[JsonSchema.Type]!.ToString()); - Assert.NotNull(result[JsonSchema.Items]); + if (TypeUtils.IsDictionary(type)) + { + Assert.Equal(JsonSchema.Object, result[JsonSchema.Type]!.ToString()); + Assert.NotNull(result[JsonSchema.AdditionalProperties]); + } + else + { + Assert.Equal(JsonSchema.Array, result[JsonSchema.Type]!.ToString()); + Assert.NotNull(result[JsonSchema.Items]); + } + Assert.Null(result[nameof(ListType.shouldBeIgnored)]); Assert.Null(result[nameof(ListType.ShouldBeIgnored)]); diff --git a/ReflectorNet.Tests/SchemaTests/SchemaTestBase.cs b/ReflectorNet.Tests/SchemaTests/SchemaTestBase.cs index 845a9ac..b110387 100644 --- a/ReflectorNet.Tests/SchemaTests/SchemaTestBase.cs +++ b/ReflectorNet.Tests/SchemaTests/SchemaTestBase.cs @@ -10,6 +10,16 @@ namespace com.IvanMurzak.ReflectorNet.Tests.SchemaTests { public abstract class SchemaTestBase : BaseTest { + static readonly Type[] RestrictedDefineTypes = new Type[] + { + typeof(string), + typeof(object), + typeof(DateTime), + typeof(DateTimeOffset), + typeof(Guid), + typeof(TimeSpan), + typeof(Uri) + }; protected SchemaTestBase(ITestOutputHelper output) : base(output) { } @@ -264,6 +274,13 @@ protected void AssertAllRefsDefined(JsonNode schema) return; } + // References don't include restricted types + foreach (var reference in allReferences) + { + Assert.False(RestrictedDefineTypes.Any(x => JsonSchema.RefValue + x.GetSchemaTypeId() == reference), + $"Reference '{reference}' is for a restricted type that should not appear as a $ref."); + } + // 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)}"); @@ -281,6 +298,13 @@ protected void AssertAllRefsDefined(JsonNode schema) $"Reference '{reference}' (type ID: '{typeId}') is not defined in $defs. " + $"Available definitions: {string.Join(", ", defines.Select(d => d.Key))}"); } + + // Defines don't include restricted types + foreach (var define in defines) + { + Assert.False(RestrictedDefineTypes.Any(x => x.GetSchemaTypeId() == define.Key), + $"Reference '{define.Key}' is for a restricted type that should not appear as a $ref."); + } } /// diff --git a/ReflectorNet/ReflectorNet.csproj b/ReflectorNet/ReflectorNet.csproj index 2948a2e..fb9102d 100644 --- a/ReflectorNet/ReflectorNet.csproj +++ b/ReflectorNet/ReflectorNet.csproj @@ -27,8 +27,8 @@ - - + + \ No newline at end of file diff --git a/ReflectorNet/src/Convertor/Json/SerializedMemberConverter.cs b/ReflectorNet/src/Convertor/Json/SerializedMemberConverter.cs index e458324..4f818b8 100644 --- a/ReflectorNet/src/Convertor/Json/SerializedMemberConverter.cs +++ b/ReflectorNet/src/Convertor/Json/SerializedMemberConverter.cs @@ -93,6 +93,7 @@ public SerializedMemberConverter(Reflector reflector) public override IEnumerable GetDefinedTypes() { yield return typeof(SerializedMemberList); + yield return typeof(SerializedMember); } 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 917d9ae..937f493 100644 --- a/ReflectorNet/src/Convertor/Json/SerializedMemberListConverter.cs +++ b/ReflectorNet/src/Convertor/Json/SerializedMemberListConverter.cs @@ -38,6 +38,7 @@ public SerializedMemberListConverter(Reflector reflector) }; public override IEnumerable GetDefinedTypes() { + yield return typeof(SerializedMemberList); yield return typeof(SerializedMember); } diff --git a/ReflectorNet/src/Utils/Json/JsonSchema.Internal.cs b/ReflectorNet/src/Utils/Json/JsonSchema.Internal.cs index 363d181..2158712 100644 --- a/ReflectorNet/src/Utils/Json/JsonSchema.Internal.cs +++ b/ReflectorNet/src/Utils/Json/JsonSchema.Internal.cs @@ -149,8 +149,6 @@ JsonNode GenerateSchemaFromType(Reflector reflector, Type type, JsonObject defin var required = new JsonArray(); var schema = new JsonObject { [Type] = Object }; - defines ??= new(); - // Get serializable fields var fields = reflector.GetSerializableFields(type); if (fields != null) @@ -231,13 +229,13 @@ JsonNode GenerateSchemaFromType(Reflector reflector, Type type, JsonObject defin } } + if (properties.Count > 0) + schema[Properties] = properties; + // Add required array if it has items if (required.Count > 0) schema[Required] = required; - if (properties.Count > 0) - schema[Properties] = properties; - return schema; } diff --git a/ReflectorNet/src/Utils/Json/JsonSchema.cs b/ReflectorNet/src/Utils/Json/JsonSchema.cs index 455f5d2..28df10e 100644 --- a/ReflectorNet/src/Utils/Json/JsonSchema.cs +++ b/ReflectorNet/src/Utils/Json/JsonSchema.cs @@ -162,6 +162,31 @@ public JsonNode GetSchema(Reflector reflector, Type type, JsonObject? defines = defines[defTypeId] = def; } } + else if (TypeUtils.IsDictionary(type)) + { + var genericArgs = TypeUtils.GetDictionaryGenericArguments(type); + if (genericArgs == null) + throw new InvalidOperationException($"Unable to get generic arguments for dictionary type '{type.GetTypeName(pretty: false)}'."); + + foreach (var genericArgument in genericArgs) + { + if (TypeUtils.IsPrimitive(genericArgument)) + continue; + + var defTypeId = genericArgument.GetSchemaTypeId(); + if (defines.ContainsKey(defTypeId)) + continue; + + var defSchema = GetSchema(reflector, genericArgument, defines); + if (defSchema != null) + defines[defTypeId] = defSchema; + } + + if (definesNeeded && !defineContainsType) + defines[typeId] = schema; + + schema = new JsonObject { [Type] = Object, [AdditionalProperties] = true }; + } else { schema = GenerateSchemaFromType(reflector, type, defines); @@ -530,8 +555,7 @@ public JsonNode GenerateSchema( var schema = new JsonObject { // [SchemaDraft] = JsonValue.Create(SchemaDraftValue), - [Type] = Object, - [Properties] = properties + [Type] = Object }; foreach (var parameter in types) @@ -588,8 +612,12 @@ public JsonNode GenerateSchema( } } + if (properties.Count > 0) + schema[Properties] = properties; + if (defines.Count > 0 && needToAddDefines) schema[Defs] = defines; + if (required.Count > 0) schema[Required] = required; diff --git a/ReflectorNet/src/Utils/TypeUtils.cs b/ReflectorNet/src/Utils/TypeUtils.cs index 54c0c59..7cd07e3 100644 --- a/ReflectorNet/src/Utils/TypeUtils.cs +++ b/ReflectorNet/src/Utils/TypeUtils.cs @@ -38,7 +38,33 @@ public static partial class TypeUtils return type; } + public static bool IsDictionary(Type type) + { + if (type.IsGenericType && + (type.GetGenericTypeDefinition() == typeof(Dictionary<,>) || + type.GetGenericTypeDefinition() == typeof(IDictionary<,>))) + { + return true; + } + + return type.GetInterfaces() + .Any(i => i.IsGenericType && (i.GetGenericTypeDefinition() == typeof(IDictionary<,>))); + } + public static Type[]? GetDictionaryGenericArguments(Type type) + { + if (type.IsGenericType && + (type.GetGenericTypeDefinition() == typeof(Dictionary<,>) || + type.GetGenericTypeDefinition() == typeof(IDictionary<,>))) + { + return type.GetGenericArguments(); + } + + var dictionaryInterface = type.GetInterfaces() + .FirstOrDefault(i => i.IsGenericType && (i.GetGenericTypeDefinition() == typeof(IDictionary<,>))); + + return dictionaryInterface?.GetGenericArguments(); + } public static string? GetDescription(Type type) {