Skip to content

Fix OpenAPI enum schema for non-body parameters with naming policy#66228

Open
mikekistler wants to merge 3 commits intodotnet:mainfrom
mikekistler:fix/65026-openapi-enum-query-params
Open

Fix OpenAPI enum schema for non-body parameters with naming policy#66228
mikekistler wants to merge 3 commits intodotnet:mainfrom
mikekistler:fix/65026-openapi-enum-query-params

Conversation

@mikekistler
Copy link
Copy Markdown
Contributor

Summary

Fixes #65026

When a global JsonStringEnumConverter with a naming policy (e.g. KebabCaseLower) is configured via ConfigureHttpJsonOptions, the OpenAPI spec emits naming-policy-transformed enum values (e.g. my-value) for all parameters. However, query/path/header parameter binding uses Enum.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 with Enum.GetNames() so the schema matches what Enum.TryParse accepts.

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:

  • Query/path/header params show PascalCase enum values (matching Enum.TryParse behavior)
  • Body params/responses keep naming-policy-transformed values (matching JSON serialization)
  • Enums with attribute-level converters (no global naming policy) are unaffected

Tests

  • GetOpenApiParameters_EnumWithGlobalNamingPolicy_UsesOriginalMemberNames — verifies query param enum values are PascalCase when a global KebabCaseLower policy is configured
  • GetOpenApiParameters_EnumWithGlobalNamingPolicy_HandlesQueryAndBodyUsage — verifies the same enum type produces PascalCase for query params and kebab-case for body responses

All 222 OpenApiSchemaServiceTests pass.

mikekistler and others added 2 commits April 8, 2026 13:28
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>
@mikekistler mikekistler requested a review from a team as a code owner April 8, 2026 20:40
Copilot AI review requested due to automatic review settings April 8, 2026 20:40
@github-actions github-actions bot added the needs-area-label Used by the dotnet-issue-labeler to label those issues which couldn't be triaged automatically label Apr 8, 2026
@mikekistler mikekistler added area-minimal Includes minimal APIs, endpoint filters, parameter binding, request delegate generator etc feature-openapi and removed needs-area-label Used by the dotnet-issue-labeler to label those issues which couldn't be triaged automatically labels Apr 8, 2026
Copy link
Copy Markdown
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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;
Copy link

Copilot AI Apr 8, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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).

Suggested change
|| bindingSource == BindingSource.Path;
|| bindingSource == BindingSource.Path
|| bindingSource == BindingSource.Form;

Copilot uses AI. Check for mistakes.
Comment on lines +294 to +296
private static bool IsNonBodyBindingSource(BindingSource bindingSource) => bindingSource == BindingSource.Header
|| bindingSource == BindingSource.Query
|| bindingSource == BindingSource.Path;
Copy link

Copilot AI Apr 8, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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).

Suggested change
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,
};

Copilot uses AI. Check for mistakes.
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>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

area-minimal Includes minimal APIs, endpoint filters, parameter binding, request delegate generator etc feature-openapi

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Mismatch between OpenAPI spec and enum deserialization

2 participants