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 3 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. |
|
|
||
| static bool IsNonBodyBindingSource(BindingSource bindingSource) => bindingSource == BindingSource.Header | ||
| || bindingSource == BindingSource.Query | ||
| || bindingSource == BindingSource.Path; |
There was a problem hiding this comment.
The comment and intent here mention form parameters, but IsNonBodyBindingSource excludes BindingSource.Form. As a result, enums coming from form fields (application/x-www-form-urlencoded or multipart) will keep naming-policy-transformed values in the schema even though form binding does not honor JSON enum naming policies. Include BindingSource.Form in IsNonBodyBindingSource (or update the comment if forms are intentionally out of scope).
| || bindingSource == BindingSource.Path; | |
| || bindingSource == BindingSource.Path | |
| || bindingSource == BindingSource.Form; |
| private static bool IsNonBodyBindingSource(BindingSource bindingSource) => bindingSource == BindingSource.Header | ||
| || bindingSource == BindingSource.Query | ||
| || bindingSource == BindingSource.Path; |
There was a problem hiding this comment.
IsNonBodyBindingSource excludes BindingSource.Form, but form fields are also not JSON-body serialized and their enum parsing won't respect JsonStringEnumConverter naming policies. If forms can contain enums, they can hit the same schema mismatch and also the same componentization collision with body/response schemas. Consider treating BindingSource.Form as non-body here (or otherwise ensuring form enum schemas are corrected and not overwritten by shared components).
| private static bool IsNonBodyBindingSource(BindingSource bindingSource) => bindingSource == BindingSource.Header | |
| || bindingSource == BindingSource.Query | |
| || bindingSource == BindingSource.Path; | |
| private static bool IsNonBodyBindingSource(BindingSource bindingSource) => bindingSource switch | |
| { | |
| var source when source == BindingSource.Header => true, | |
| var source when source == BindingSource.Query => true, | |
| var source when source == BindingSource.Path => true, | |
| var source when source == BindingSource.Form => true, | |
| _ => false, | |
| }; |
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>
Summary
Fixes #65026
When a global
JsonStringEnumConverterwith a naming policy (e.g.KebabCaseLower) is configured viaConfigureHttpJsonOptions, the OpenAPI spec emits naming-policy-transformed enum values (e.g.my-value) for all parameters. However, query/path/header parameter binding usesEnum.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), 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 responsesAll 222
OpenApiSchemaServiceTestspass.