diff --git a/src/OpenApi/src/Extensions/JsonNodeSchemaExtensions.cs b/src/OpenApi/src/Extensions/JsonNodeSchemaExtensions.cs
index feb32d33b8e0..48c14321c062 100644
--- a/src/OpenApi/src/Extensions/JsonNodeSchemaExtensions.cs
+++ b/src/OpenApi/src/Extensions/JsonNodeSchemaExtensions.cs
@@ -15,6 +15,7 @@
using Microsoft.AspNetCore.Mvc.ModelBinding;
using Microsoft.AspNetCore.Routing;
using Microsoft.AspNetCore.Routing.Constraints;
+using Microsoft.Extensions.Logging;
namespace Microsoft.AspNetCore.OpenApi;
@@ -170,7 +171,8 @@ internal static void ApplyValidationAttributes(this JsonNode schema, IEnumerable
/// The produced by the underlying schema generator.
/// An object representing the associated with the default value.
/// The associated with the target type.
- internal static void ApplyDefaultValue(this JsonNode schema, object? defaultValue, JsonTypeInfo? jsonTypeInfo)
+ /// The logger to use for warning messages when default value type mismatches occur.
+ internal static void ApplyDefaultValue(this JsonNode schema, object? defaultValue, JsonTypeInfo? jsonTypeInfo, ILogger? logger = null)
{
if (jsonTypeInfo is null)
{
@@ -183,7 +185,16 @@ internal static void ApplyDefaultValue(this JsonNode schema, object? defaultValu
}
else
{
- schema[OpenApiSchemaKeywords.DefaultKeyword] = JsonSerializer.SerializeToNode(defaultValue, jsonTypeInfo);
+ try
+ {
+ schema[OpenApiSchemaKeywords.DefaultKeyword] = JsonSerializer.SerializeToNode(defaultValue, jsonTypeInfo);
+ }
+ catch (Exception ex) when (ex is InvalidCastException or NotSupportedException or InvalidOperationException)
+ {
+ // Log warning when there's a type mismatch that prevents serialization
+ logger?.DefaultValueTypeMismatch(defaultValue.GetType().Name, jsonTypeInfo.Type.Name);
+ // Do not apply the default value when there's a type mismatch
+ }
}
}
@@ -305,7 +316,8 @@ internal static void ApplyRouteConstraints(this JsonNode schema, IEnumerableThe produced by the underlying schema generator.
/// The associated with the .
/// The associated with the .
- internal static void ApplyParameterInfo(this JsonNode schema, ApiParameterDescription parameterDescription, JsonTypeInfo? jsonTypeInfo)
+ /// The logger to use for warning messages when default value type mismatches occur.
+ internal static void ApplyParameterInfo(this JsonNode schema, ApiParameterDescription parameterDescription, JsonTypeInfo? jsonTypeInfo, ILogger? logger = null)
{
// This is special handling for parameters that are not bound from the body but represented in a complex type.
// For example:
@@ -338,11 +350,11 @@ internal static void ApplyParameterInfo(this JsonNode schema, ApiParameterDescri
{
if (parameterInfo.HasDefaultValue)
{
- schema.ApplyDefaultValue(parameterInfo.DefaultValue, jsonTypeInfo);
+ schema.ApplyDefaultValue(parameterInfo.DefaultValue, jsonTypeInfo, logger);
}
else if (parameterInfo.GetCustomAttributes().LastOrDefault() is { } defaultValueAttribute)
{
- schema.ApplyDefaultValue(defaultValueAttribute.Value, jsonTypeInfo);
+ schema.ApplyDefaultValue(defaultValueAttribute.Value, jsonTypeInfo, logger);
}
if (parameterInfo.GetCustomAttributes() is { } validationAttributes)
diff --git a/src/OpenApi/src/Extensions/OpenApiLoggingExtensions.cs b/src/OpenApi/src/Extensions/OpenApiLoggingExtensions.cs
new file mode 100644
index 000000000000..516b39b97c5f
--- /dev/null
+++ b/src/OpenApi/src/Extensions/OpenApiLoggingExtensions.cs
@@ -0,0 +1,12 @@
+// Licensed to the .NET Foundation under one or more agreements.
+// The .NET Foundation licenses this file to you under the MIT license.
+
+using Microsoft.Extensions.Logging;
+
+namespace Microsoft.AspNetCore.OpenApi;
+
+internal static partial class OpenApiLoggingExtensions
+{
+ [LoggerMessage(1, LogLevel.Warning, "Failed to apply default value for parameter due to type mismatch. Default value type: '{DefaultValueType}', Parameter type: '{ParameterType}'. Default value will be omitted from the OpenAPI schema.", EventName = "DefaultValueTypeMismatch")]
+ public static partial void DefaultValueTypeMismatch(this ILogger logger, string defaultValueType, string parameterType);
+}
\ No newline at end of file
diff --git a/src/OpenApi/src/Services/Schemas/OpenApiSchemaService.cs b/src/OpenApi/src/Services/Schemas/OpenApiSchemaService.cs
index 10737cbd5f20..8abae3195383 100644
--- a/src/OpenApi/src/Services/Schemas/OpenApiSchemaService.cs
+++ b/src/OpenApi/src/Services/Schemas/OpenApiSchemaService.cs
@@ -16,6 +16,7 @@
using Microsoft.AspNetCore.Mvc.ApiExplorer;
using Microsoft.AspNetCore.Mvc.Infrastructure;
using Microsoft.Extensions.DependencyInjection;
+using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Options;
namespace Microsoft.AspNetCore.OpenApi;
@@ -28,7 +29,8 @@ namespace Microsoft.AspNetCore.OpenApi;
internal sealed class OpenApiSchemaService(
[ServiceKey] string documentName,
IOptions jsonOptions,
- IOptionsMonitor optionsMonitor)
+ IOptionsMonitor optionsMonitor,
+ ILogger? logger = null)
{
private readonly ConcurrentDictionary _schemaIdCache = new();
private readonly OpenApiJsonSchemaContext _jsonSchemaContext = new(new(jsonOptions.Value.SerializerOptions));
@@ -105,7 +107,7 @@ internal sealed class OpenApiSchemaService(
}
if (attributeProvider.GetCustomAttributes(inherit: false).OfType().LastOrDefault() is DefaultValueAttribute defaultValueAttribute)
{
- schema.ApplyDefaultValue(defaultValueAttribute.Value, context.TypeInfo);
+ schema.ApplyDefaultValue(defaultValueAttribute.Value, context.TypeInfo, logger);
}
if (attributeProvider.GetCustomAttributes(inherit: false).OfType().LastOrDefault() is DescriptionAttribute descriptionAttribute)
{
@@ -125,7 +127,7 @@ internal async Task GetOrCreateUnresolvedSchemaAsync(OpenApiDocum
var schemaAsJsonObject = CreateSchema(key);
if (parameterDescription is not null)
{
- schemaAsJsonObject.ApplyParameterInfo(parameterDescription, _jsonSerializerOptions.GetTypeInfo(type));
+ schemaAsJsonObject.ApplyParameterInfo(parameterDescription, _jsonSerializerOptions.GetTypeInfo(type), logger);
}
// Use _jsonSchemaContext constructed from _jsonSerializerOptions to respect shared config set by end-user,
// particularly in the case of maxDepth.
diff --git a/src/OpenApi/test/Microsoft.AspNetCore.OpenApi.Tests/Services/OpenApiDocumentServiceTestsBase.cs b/src/OpenApi/test/Microsoft.AspNetCore.OpenApi.Tests/Services/OpenApiDocumentServiceTestsBase.cs
index 9ab90d9f52a0..69dc5d698842 100644
--- a/src/OpenApi/test/Microsoft.AspNetCore.OpenApi.Tests/Services/OpenApiDocumentServiceTestsBase.cs
+++ b/src/OpenApi/test/Microsoft.AspNetCore.OpenApi.Tests/Services/OpenApiDocumentServiceTestsBase.cs
@@ -20,6 +20,8 @@
using Microsoft.AspNetCore.Routing;
using Microsoft.AspNetCore.Routing.Constraints;
using Microsoft.Extensions.DependencyInjection;
+using Microsoft.Extensions.Logging;
+using Microsoft.Extensions.Logging.Abstractions;
using Microsoft.Extensions.Options;
using Microsoft.Net.Http.Headers;
using Moq;
@@ -88,7 +90,8 @@ internal static OpenApiDocumentService CreateDocumentService(IEndpointRouteBuild
// Set strict number handling by default to make integer type checks more straightforward
jsonOptions.SerializerOptions.NumberHandling = System.Text.Json.Serialization.JsonNumberHandling.Strict;
- var schemaService = new OpenApiSchemaService("Test", Options.Create(jsonOptions), openApiOptions.Object);
+ var logger = builder.ServiceProvider.GetService>() ?? NullLogger.Instance;
+ var schemaService = new OpenApiSchemaService("Test", Options.Create(jsonOptions), openApiOptions.Object, logger);
((TestServiceProvider)builder.ServiceProvider).TestSchemaService = schemaService;
var documentService = new OpenApiDocumentService("Test", apiDescriptionGroupCollectionProvider, hostEnvironment, openApiOptions.Object, builder.ServiceProvider, new OpenApiTestServer());
((TestServiceProvider)builder.ServiceProvider).TestDocumentService = documentService;
@@ -134,7 +137,8 @@ internal static OpenApiDocumentService CreateDocumentService(IEndpointRouteBuild
defaultJsonOptions.SerializerOptions.NumberHandling = System.Text.Json.Serialization.JsonNumberHandling.Strict;
var jsonOptions = builder.ServiceProvider.GetService>() ?? Options.Create(defaultJsonOptions);
- var schemaService = new OpenApiSchemaService("Test", jsonOptions, options.Object);
+ var logger = builder.ServiceProvider.GetService>() ?? NullLogger.Instance;
+ var schemaService = new OpenApiSchemaService("Test", jsonOptions, options.Object, logger);
((TestServiceProvider)builder.ServiceProvider).TestSchemaService = schemaService;
var documentService = new OpenApiDocumentService("Test", apiDescriptionGroupCollectionProvider, hostEnvironment, options.Object, builder.ServiceProvider, new OpenApiTestServer());
((TestServiceProvider)builder.ServiceProvider).TestDocumentService = documentService;
diff --git a/src/OpenApi/test/Microsoft.AspNetCore.OpenApi.Tests/Services/OpenApiSchemaService/OpenApiSchemaService.ParameterSchemas.cs b/src/OpenApi/test/Microsoft.AspNetCore.OpenApi.Tests/Services/OpenApiSchemaService/OpenApiSchemaService.ParameterSchemas.cs
index ede9a0b3d1b2..6156b2190c30 100644
--- a/src/OpenApi/test/Microsoft.AspNetCore.OpenApi.Tests/Services/OpenApiSchemaService/OpenApiSchemaService.ParameterSchemas.cs
+++ b/src/OpenApi/test/Microsoft.AspNetCore.OpenApi.Tests/Services/OpenApiSchemaService/OpenApiSchemaService.ParameterSchemas.cs
@@ -14,6 +14,7 @@
using Microsoft.AspNetCore.Mvc;
using Microsoft.AspNetCore.Routing;
using Microsoft.Extensions.DependencyInjection;
+using Microsoft.Extensions.Logging;
public partial class OpenApiSchemaServiceTests : OpenApiDocumentServiceTestBase
{
@@ -642,6 +643,50 @@ await VerifyOpenApiDocument(builder, document =>
});
}
+ public static object[][] RouteParametersWithDefaultValueTypeMismatch =>
+ [
+ // F# scenarios where type mismatch causes InvalidCastException and logging should occur
+ [([DefaultValue(10)] ulong id) => { }, (Action)((logMessages) =>
+ {
+ Assert.Single(logMessages);
+ Assert.Contains("Failed to apply default value for parameter due to type mismatch", logMessages[0]);
+ Assert.Contains("Default value type: 'Int32', Parameter type: 'UInt64'", logMessages[0]);
+ })],
+ [([DefaultValue(10u)] ulong id) => { }, (Action)((logMessages) =>
+ {
+ Assert.Single(logMessages);
+ Assert.Contains("Failed to apply default value for parameter due to type mismatch", logMessages[0]);
+ Assert.Contains("Default value type: 'UInt32', Parameter type: 'UInt64'", logMessages[0]);
+ })],
+ ];
+
+ [Theory]
+ [MemberData(nameof(RouteParametersWithDefaultValueTypeMismatch))]
+ public async Task GetOpenApiParameters_LogsWarningForDefaultValueTypeMismatch(Delegate requestHandler, Action assertLogMessages)
+ {
+ // Arrange
+ var logMessages = new List();
+ var serviceCollection = new ServiceCollection();
+ serviceCollection.AddSingleton(new TestLoggerProvider(logMessages));
+ serviceCollection.AddLogging();
+
+ var builder = CreateBuilder(serviceCollection);
+ builder.MapGet("/api/{id}", requestHandler);
+
+ // Act & Assert
+ await VerifyOpenApiDocument(builder, document =>
+ {
+ var operation = document.Paths["/api/{id}"].Operations[HttpMethod.Get];
+ var parameter = Assert.Single(operation.Parameters);
+
+ // Verify that no default value is set when there's a type mismatch
+ Assert.Null(parameter.Schema.Default);
+
+ // Verify the warning log was emitted
+ assertLogMessages(logMessages.ToArray());
+ });
+ }
+
public struct CustomType { }
public class CustomTypeConverter : JsonConverter
@@ -890,6 +935,48 @@ public override void Write(Utf8JsonWriter writer, EnumArrayType value, JsonSeria
}
}
+ public class TestLoggerProvider : ILoggerProvider
+ {
+ private readonly List _logMessages;
+
+ public TestLoggerProvider(List logMessages)
+ {
+ _logMessages = logMessages;
+ }
+
+ public ILogger CreateLogger(string categoryName)
+ {
+ return new TestLogger(_logMessages);
+ }
+
+ public void Dispose() { }
+ }
+
+ public class TestLogger : ILogger
+ {
+ private readonly List _logMessages;
+
+ public TestLogger(List logMessages)
+ {
+ _logMessages = logMessages;
+ }
+
+ public IDisposable BeginScope(TState state) => NullScope.Instance;
+
+ public bool IsEnabled(LogLevel logLevel) => true;
+
+ public void Log(LogLevel logLevel, EventId eventId, TState state, Exception exception, Func formatter)
+ {
+ _logMessages.Add(formatter(state, exception));
+ }
+
+ private class NullScope : IDisposable
+ {
+ public static NullScope Instance { get; } = new();
+ public void Dispose() { }
+ }
+ }
+
[ApiController]
[Route("[controller]/[action]")]
private class TestFromQueryController : ControllerBase