diff --git a/src/OpenApi/src/Services/Schemas/OpenApiSchemaService.cs b/src/OpenApi/src/Services/Schemas/OpenApiSchemaService.cs index 0090c39a617f..0d2998cbd180 100644 --- a/src/OpenApi/src/Services/Schemas/OpenApiSchemaService.cs +++ b/src/OpenApi/src/Services/Schemas/OpenApiSchemaService.cs @@ -91,7 +91,7 @@ internal sealed class OpenApiSchemaService( schema.ApplyPrimitiveTypesAndFormats(context); schema.ApplySchemaReferenceId(context); schema.ApplyPolymorphismOptions(context); - if (context.PropertyInfo is { AttributeProvider: { } attributeProvider } jsonPropertyInfo) + if (context.PropertyInfo is { } jsonPropertyInfo) { // Short-circuit STJ's handling of nested properties, which uses a reference to the // properties type schema with a schema that uses a document level reference. @@ -102,6 +102,9 @@ internal sealed class OpenApiSchemaService( return new JsonObject { [OpenApiSchemaKeywords.RefKeyword] = context.TypeInfo.GetSchemaReferenceId() }; } schema.ApplyNullabilityContextInfo(jsonPropertyInfo); + } + if (context.PropertyInfo is { AttributeProvider: { } attributeProvider }) + { if (attributeProvider.GetCustomAttributes(inherit: false).OfType() is { } validationAttributes) { schema.ApplyValidationAttributes(validationAttributes); diff --git a/src/OpenApi/test/Services/OpenApiDocumentServiceTestsBase.cs b/src/OpenApi/test/Services/OpenApiDocumentServiceTestsBase.cs index 9b72ee79e8df..15372d9fbfb3 100644 --- a/src/OpenApi/test/Services/OpenApiDocumentServiceTestsBase.cs +++ b/src/OpenApi/test/Services/OpenApiDocumentServiceTestsBase.cs @@ -98,8 +98,9 @@ internal static OpenApiDocumentService CreateDocumentService(IEndpointRouteBuild provider.OnProvidersExecuted(context); var apiDescriptionGroupCollectionProvider = CreateApiDescriptionGroupCollectionProvider(context.Results); + var jsonOptions = builder.ServiceProvider.GetService>() ?? Options.Create(new Microsoft.AspNetCore.Http.Json.JsonOptions()); - var schemaService = new OpenApiSchemaService("Test", Options.Create(new Microsoft.AspNetCore.Http.Json.JsonOptions()), builder.ServiceProvider, options.Object); + var schemaService = new OpenApiSchemaService("Test", jsonOptions, builder.ServiceProvider, options.Object); ((TestServiceProvider)builder.ServiceProvider).TestSchemaService = schemaService; var documentService = new OpenApiDocumentService("Test", apiDescriptionGroupCollectionProvider, hostEnvironment, options.Object, builder.ServiceProvider); ((TestServiceProvider)builder.ServiceProvider).TestDocumentService = documentService; diff --git a/src/OpenApi/test/Services/OpenApiSchemaService/OpenApiSchemaService.RequestBodySchemas.cs b/src/OpenApi/test/Services/OpenApiSchemaService/OpenApiSchemaService.RequestBodySchemas.cs index 8930d88c0410..4da4cf7b72c8 100644 --- a/src/OpenApi/test/Services/OpenApiSchemaService/OpenApiSchemaService.RequestBodySchemas.cs +++ b/src/OpenApi/test/Services/OpenApiSchemaService/OpenApiSchemaService.RequestBodySchemas.cs @@ -4,8 +4,10 @@ using System.ComponentModel; using System.ComponentModel.DataAnnotations; using System.IO.Pipelines; +using System.Text.Json.Serialization.Metadata; using Microsoft.AspNetCore.Builder; using Microsoft.AspNetCore.Mvc; +using Microsoft.Extensions.DependencyInjection; using Microsoft.OpenApi.Any; using Microsoft.OpenApi.Models; @@ -468,6 +470,51 @@ await VerifyOpenApiDocument(builder, document => }); } + [Fact] + public async Task SupportsNestedTypes_WithNoAttributeProvider() + { + // Arrange: this test ensures that we can correctly handle the scenario + // where the attribute provider is null and we need to patch the property mappings + // that are created by the underlying JsonSchemaExporter. + var serviceCollection = new ServiceCollection(); + serviceCollection.ConfigureHttpJsonOptions(options => + { + options.SerializerOptions.TypeInfoResolver = options.SerializerOptions.TypeInfoResolver?.WithAddedModifier(jsonTypeInfo => + { + foreach (var propertyInfo in jsonTypeInfo.Properties) + { + propertyInfo.AttributeProvider = null; + } + + }); + }); + var builder = CreateBuilder(serviceCollection); + + // Act + builder.MapPost("/api", (NestedType type) => { }); + + // Assert + await VerifyOpenApiDocument(builder, document => + { + var operation = document.Paths["/api"].Operations[OperationType.Post]; + var requestBody = operation.RequestBody; + var content = Assert.Single(requestBody.Content); + Assert.Equal("NestedType", content.Value.Schema.Reference.Id); + var schema = content.Value.Schema.GetEffective(document); + Assert.Collection(schema.Properties, + property => + { + Assert.Equal("name", property.Key); + Assert.Equal("string", property.Value.Type); + }, + property => + { + Assert.Equal("nested", property.Key); + Assert.Equal("NestedType", property.Value.Reference.Id); + }); + }); + } + private class DescriptionTodo { [Description("The unique identifier for a todo item.")]