Fix OpenAPI enum schema for non-body parameters with naming policy#66228
Fix OpenAPI enum schema for non-body parameters with naming policy#66228mikekistler wants to merge 5 commits intodotnet:mainfrom
Conversation
When a global JsonStringEnumConverter with a naming policy (e.g. KebabCaseLower) is configured, the OpenAPI spec now emits the original C# enum member names for query/path/header parameters instead of the naming-policy-transformed values. This matches the actual binding behavior, which uses Enum.TryParse and only accepts the original member names. The fix adds logic in ApplyParameterInfo to detect non-body enum parameters and replace their schema enum values with Enum.GetNames(). Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
When an enum with a global naming policy is used as both a query parameter and a body property/return type, the query param needs PascalCase values (for Enum.TryParse) while the body needs the naming-policy-transformed values (for JSON serialization). Skip componentization for non-body enum parameters when a naming policy transforms the values, returning an inline schema instead. This preserves the component schema with naming-policy values for body use while giving query/path/header parameters their own inline schema with the original C# member names. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
There was a problem hiding this comment.
Pull request overview
Fixes an OpenAPI/spec mismatch where global JsonStringEnumConverter naming policies were being applied to non-body parameters (query/path/header), even though runtime binding uses Enum.TryParse and only accepts the original C# enum member names.
Changes:
- Adjust enum parameter schemas for non-body parameters to emit original enum member names.
- Avoid schema componentization for affected non-body enum parameters to prevent shared component schemas (used for body serialization) from overwriting corrected parameter schemas.
- Add tests covering query-parameter enums with a global naming policy, including mixed query + response usage of the same enum.
Reviewed changes
Copilot reviewed 4 out of 4 changed files in this pull request and generated 2 comments.
| File | Description |
|---|---|
| src/OpenApi/test/Microsoft.AspNetCore.OpenApi.Tests/Shared/SharedTypes.cs | Adds a new test enum (Priority) used for global-naming-policy scenarios. |
| src/OpenApi/test/Microsoft.AspNetCore.OpenApi.Tests/Services/OpenApiSchemaService/OpenApiSchemaService.ParameterSchemas.cs | Adds coverage ensuring query parameter enums use original member names while response bodies keep naming-policy output. |
| src/OpenApi/src/Services/Schemas/OpenApiSchemaService.cs | Skips componentization for affected non-body enum parameters when naming policy transforms enum values. |
| src/OpenApi/src/Extensions/JsonNodeSchemaExtensions.cs | Rewrites enum values for applicable parameter schemas to match Enum.TryParse expectations. |
Add BindingSource.Form and BindingSource.FormFile to IsNonBodyBindingSource so that enum form fields also use original C# member names instead of naming-policy-transformed values. Minimal APIs report [FromForm] enum parameters with BindingSource.FormFile, so both sources are needed. Add test: GetOpenApiRequestBody_EnumFormFieldWithGlobalNamingPolicy_UsesOriginalMemberNames Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
| { | ||
| enumArray.Add((JsonNode)name); | ||
| } | ||
| schema[OpenApiSchemaKeywords.EnumKeyword] = enumArray; |
There was a problem hiding this comment.
Do we also need to consider the default/x-ref-default values that will still be kebab-case?
| var memberNames = Enum.GetNames(enumType); | ||
| for (var i = 0; i < memberNames.Length && i < rawEnum.Count; i++) | ||
| { | ||
| if (rawEnum[i]?.GetValue<string>() != memberNames[i]) |
There was a problem hiding this comment.
Are the enum values always going to line up with the member names if they are the 'correct' case already?
| if (rawNode[OpenApiSchemaKeywords.EnumKeyword] is JsonArray rawEnum && rawEnum.Count > 0) | ||
| { | ||
| var memberNames = Enum.GetNames(enumType); | ||
| for (var i = 0; i < memberNames.Length && i < rawEnum.Count; i++) |
There was a problem hiding this comment.
AI doesn't like this as there might be a count mismatch if names have the same value. e.g. enum E { A = 0, B = 1, C = 1} It says schema generation usually collapses duplicate values so you'd get ["A", "B"] but be comparing against ["A", "B", "C"].
I'm not sure how true that is, but we might want to test and handle that?
|
Looks like this PR hasn't been active for some time and the codebase could have been changed in the meantime. |
Summary
Fixes #65026
When a global
JsonStringEnumConverterwith a naming policy (e.g.KebabCaseLower) is configured viaConfigureHttpJsonOptions, the OpenAPI spec can emit naming-policy-transformed enum values (e.g.my-value) for non-body parameters. However, query/path/header/form parameter binding useEnum.TryParse, which only accepts the original C# member names (e.g.MyValue). This causes clients following the spec to send values that the server rejects with 400.Changes
JsonNodeSchemaExtensions.ApplyParameterInfo— For non-body enum parameters (query, path, header, and form), replaces the naming-policy-transformed enum values withEnum.GetNames()so the schema matches whatEnum.TryParseaccepts.OpenApiSchemaService.GetOrCreateSchemaAsync— When a naming policy transforms enum values, skips schema componentization for non-body enum parameters and returns an inline schema instead. This prevents the shared component (which retains naming-policy values for body serialization) from overwriting the corrected parameter schema.These two changes work together so that:
Enum.TryParsebehavior)Tests
GetOpenApiParameters_EnumWithGlobalNamingPolicy_UsesOriginalMemberNames— verifies query param enum values are PascalCase when a globalKebabCaseLowerpolicy is configuredGetOpenApiParameters_EnumWithGlobalNamingPolicy_HandlesQueryAndBodyUsage— verifies the same enum type produces PascalCase for query params and kebab-case for body responsesGetOpenApiRequestBody_EnumFormFieldWithGlobalNamingPolicy_UsesOriginalMemberNames— verifies[FromForm]enum values stay in their original C# member formAll 222
OpenApiSchemaServiceTestspass.