From 65b8488ad2fdfa3458d2d208462ac51304db54d0 Mon Sep 17 00:00:00 2001 From: Minaxi Patil Date: Sat, 4 Apr 2026 15:33:25 +0530 Subject: [PATCH 1/3] Prevent InvalidCastException when attribute type does not match parameter type by adding safe type conversion. Fixes #60019 --- .../Extensions/JsonNodeSchemaExtensions.cs | 16 ++++++ .../OpenApiSchemaService.ParameterSchemas.cs | 49 +++++++++++++++++++ 2 files changed, 65 insertions(+) diff --git a/src/OpenApi/src/Extensions/JsonNodeSchemaExtensions.cs b/src/OpenApi/src/Extensions/JsonNodeSchemaExtensions.cs index 969cc614a421..cdf1f9c3e6ea 100644 --- a/src/OpenApi/src/Extensions/JsonNodeSchemaExtensions.cs +++ b/src/OpenApi/src/Extensions/JsonNodeSchemaExtensions.cs @@ -186,6 +186,22 @@ internal static void ApplyDefaultValue(this JsonNode schema, object? defaultValu } else { + // The default value type may differ from the target type when attributes like + // DefaultParameterValue carry a literal (e.g. int32 literal for a uint64 parameter). + // Convert to the target type so SerializeToNode can unbox it correctly. + var targetType = Nullable.GetUnderlyingType(jsonTypeInfo.Type) ?? jsonTypeInfo.Type; + if (defaultValue.GetType() != targetType) + { + try + { + defaultValue = Convert.ChangeType(defaultValue, targetType, CultureInfo.InvariantCulture); + } + catch + { + // Value is not convertible to the target type; skip applying the default. + return; + } + } schema[schemaAttribute] = JsonSerializer.SerializeToNode(defaultValue, jsonTypeInfo); } } 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 2903064bf31c..7fa0e86cff21 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 @@ -198,6 +198,55 @@ await VerifyOpenApiDocument(builder, document => } #nullable restore + // Static helper methods used by DefaultParameterValue tests below. + // DefaultParameterValue(10) stores an int32 literal for a uint64 parameter, + // matching what F# produces when the literal type doesn't match the parameter type. + private static void HandlerWithInt32DefaultForUInt64( + [System.Runtime.InteropServices.Optional, System.Runtime.InteropServices.DefaultParameterValue(10)] ulong id) { } + private static void HandlerWithUInt32DefaultForUInt64( + [System.Runtime.InteropServices.Optional, System.Runtime.InteropServices.DefaultParameterValue((uint)10)] ulong id) { } + + [Fact] + public async Task GetOpenApiParameters_HandlesDefaultParameterValueWithInt32LiteralOnUInt64Parameter() + { + // Arrange - int32 literal (10) stored via DefaultParameterValue on a uint64 parameter; + // this is the type mismatch that F# produces and previously caused InvalidCastException. + var builder = CreateBuilder(); + + // Act + builder.MapGet("/api", HandlerWithInt32DefaultForUInt64); + + // Assert + await VerifyOpenApiDocument(builder, document => + { + var operation = document.Paths["/api"].Operations[HttpMethod.Get]; + var parameter = Assert.Single(operation.Parameters); + var openApiDefault = parameter.Schema!.Default; + Assert.NotNull(openApiDefault); + Assert.Equal(10ul, openApiDefault.GetValue()); + }); + } + + [Fact] + public async Task GetOpenApiParameters_HandlesDefaultParameterValueWithUInt32LiteralOnUInt64Parameter() + { + // Arrange - uint32 literal (10u) stored via DefaultParameterValue on a uint64 parameter. + var builder = CreateBuilder(); + + // Act + builder.MapGet("/api", HandlerWithUInt32DefaultForUInt64); + + // Assert + await VerifyOpenApiDocument(builder, document => + { + var operation = document.Paths["/api"].Operations[HttpMethod.Get]; + var parameter = Assert.Single(operation.Parameters); + var openApiDefault = parameter.Schema!.Default; + Assert.NotNull(openApiDefault); + Assert.Equal(10ul, openApiDefault.GetValue()); + }); + } + [Fact] public async Task GetOpenApiParameters_HandlesEnumParameterWithoutConverter() { From 8741e2fbc6638c7e7c7efd519ce8eba84dd48f8c Mon Sep 17 00:00:00 2001 From: Minaxi Patil Date: Sat, 4 Apr 2026 16:04:16 +0530 Subject: [PATCH 2/3] updated test case --- .../JsonNodeSchemaExtensionsTests.cs | 38 +++++++++++++++ .../OpenApiSchemaService.ParameterSchemas.cs | 48 ------------------- 2 files changed, 38 insertions(+), 48 deletions(-) diff --git a/src/OpenApi/test/Microsoft.AspNetCore.OpenApi.Tests/Extensions/JsonNodeSchemaExtensionsTests.cs b/src/OpenApi/test/Microsoft.AspNetCore.OpenApi.Tests/Extensions/JsonNodeSchemaExtensionsTests.cs index bf4126735a9d..e51bad88da39 100644 --- a/src/OpenApi/test/Microsoft.AspNetCore.OpenApi.Tests/Extensions/JsonNodeSchemaExtensionsTests.cs +++ b/src/OpenApi/test/Microsoft.AspNetCore.OpenApi.Tests/Extensions/JsonNodeSchemaExtensionsTests.cs @@ -3,6 +3,7 @@ using System.ComponentModel.DataAnnotations; using System.Globalization; +using System.Text.Json; using System.Text.Json.Nodes; namespace Microsoft.AspNetCore.OpenApi.Tests; @@ -147,4 +148,41 @@ public static void ApplyValidationAttributes_Handles_Invalid_RangeAttribute_Valu Assert.False(schema.TryGetPropertyValue("exclusiveMinimum", out _)); Assert.False(schema.TryGetPropertyValue("exclusiveMaximum", out _)); } + + [Theory] + // int32 literal for uint64 — what F# emits for DefaultParameterValue(10) on a uint64 parameter + [InlineData(typeof(ulong), 10, (ulong)10)] + // uint32 literal for uint64 — F# DefaultParameterValue(10ul) on uint64 + [InlineData(typeof(ulong), (uint)10, (ulong)10)] + // int32 literal for int64 — common numeric promotion case + [InlineData(typeof(long), 10, (long)10)] + // int32 literal for int16 + [InlineData(typeof(short), 10, (short)10)] + public static void ApplyDefaultValue_ConvertsLiteralTypeMismatch(Type targetType, object defaultValue, object expected) + { + // Arrange — simulates what happens when DefaultParameterValue carries a literal whose + // CLR type doesn't exactly match the parameter type (e.g. F# int32 literal on uint64). + var schema = new JsonObject(); + var jsonTypeInfo = JsonSerializerOptions.Default.GetTypeInfo(targetType); + + // Act — must not throw InvalidCastException + schema.ApplyDefaultValue(defaultValue, jsonTypeInfo); + + // Assert + Assert.Equal(expected, schema["default"]!.GetValue(targetType)); + } + + [Fact] + public static void ApplyDefaultValue_SkipsNonConvertibleTypeMismatch() + { + // Arrange — a string default on a ulong parameter cannot be converted; should not throw. + var schema = new JsonObject(); + var jsonTypeInfo = JsonSerializerOptions.Default.GetTypeInfo(); + + // Act + schema.ApplyDefaultValue("not-a-number", jsonTypeInfo); + + // Assert — default should be absent rather than throwing + Assert.False(schema.TryGetPropertyValue("default", out _)); + } } 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 7fa0e86cff21..ab61ead3a108 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 @@ -198,54 +198,6 @@ await VerifyOpenApiDocument(builder, document => } #nullable restore - // Static helper methods used by DefaultParameterValue tests below. - // DefaultParameterValue(10) stores an int32 literal for a uint64 parameter, - // matching what F# produces when the literal type doesn't match the parameter type. - private static void HandlerWithInt32DefaultForUInt64( - [System.Runtime.InteropServices.Optional, System.Runtime.InteropServices.DefaultParameterValue(10)] ulong id) { } - private static void HandlerWithUInt32DefaultForUInt64( - [System.Runtime.InteropServices.Optional, System.Runtime.InteropServices.DefaultParameterValue((uint)10)] ulong id) { } - - [Fact] - public async Task GetOpenApiParameters_HandlesDefaultParameterValueWithInt32LiteralOnUInt64Parameter() - { - // Arrange - int32 literal (10) stored via DefaultParameterValue on a uint64 parameter; - // this is the type mismatch that F# produces and previously caused InvalidCastException. - var builder = CreateBuilder(); - - // Act - builder.MapGet("/api", HandlerWithInt32DefaultForUInt64); - - // Assert - await VerifyOpenApiDocument(builder, document => - { - var operation = document.Paths["/api"].Operations[HttpMethod.Get]; - var parameter = Assert.Single(operation.Parameters); - var openApiDefault = parameter.Schema!.Default; - Assert.NotNull(openApiDefault); - Assert.Equal(10ul, openApiDefault.GetValue()); - }); - } - - [Fact] - public async Task GetOpenApiParameters_HandlesDefaultParameterValueWithUInt32LiteralOnUInt64Parameter() - { - // Arrange - uint32 literal (10u) stored via DefaultParameterValue on a uint64 parameter. - var builder = CreateBuilder(); - - // Act - builder.MapGet("/api", HandlerWithUInt32DefaultForUInt64); - - // Assert - await VerifyOpenApiDocument(builder, document => - { - var operation = document.Paths["/api"].Operations[HttpMethod.Get]; - var parameter = Assert.Single(operation.Parameters); - var openApiDefault = parameter.Schema!.Default; - Assert.NotNull(openApiDefault); - Assert.Equal(10ul, openApiDefault.GetValue()); - }); - } [Fact] public async Task GetOpenApiParameters_HandlesEnumParameterWithoutConverter() From acf651951df8dcef112ab829462ceb24eb926d57 Mon Sep 17 00:00:00 2001 From: Minaxi Patil Date: Sat, 18 Apr 2026 15:26:00 +0530 Subject: [PATCH 3/3] changes --- .../Extensions/JsonNodeSchemaExtensionsTests.cs | 2 +- .../OpenApiSchemaService.ParameterSchemas.cs | 1 - 2 files changed, 1 insertion(+), 2 deletions(-) diff --git a/src/OpenApi/test/Microsoft.AspNetCore.OpenApi.Tests/Extensions/JsonNodeSchemaExtensionsTests.cs b/src/OpenApi/test/Microsoft.AspNetCore.OpenApi.Tests/Extensions/JsonNodeSchemaExtensionsTests.cs index e51bad88da39..6b37865fa1c1 100644 --- a/src/OpenApi/test/Microsoft.AspNetCore.OpenApi.Tests/Extensions/JsonNodeSchemaExtensionsTests.cs +++ b/src/OpenApi/test/Microsoft.AspNetCore.OpenApi.Tests/Extensions/JsonNodeSchemaExtensionsTests.cs @@ -169,7 +169,7 @@ public static void ApplyDefaultValue_ConvertsLiteralTypeMismatch(Type targetType schema.ApplyDefaultValue(defaultValue, jsonTypeInfo); // Assert - Assert.Equal(expected, schema["default"]!.GetValue(targetType)); + Assert.Equal(expected, JsonSerializer.Deserialize(schema["default"]!, targetType)); } [Fact] 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 ab61ead3a108..2903064bf31c 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 @@ -198,7 +198,6 @@ await VerifyOpenApiDocument(builder, document => } #nullable restore - [Fact] public async Task GetOpenApiParameters_HandlesEnumParameterWithoutConverter() {