Skip to content

fix(normalizer): support OAS 3.1 annotated enums (oneOf + const) for consistent enum generation (C#)#23869

Open
nagabalaji-b wants to merge 16 commits into
OpenAPITools:masterfrom
nagabalaji-b:openapitools-master
Open

fix(normalizer): support OAS 3.1 annotated enums (oneOf + const) for consistent enum generation (C#)#23869
nagabalaji-b wants to merge 16 commits into
OpenAPITools:masterfrom
nagabalaji-b:openapitools-master

Conversation

@nagabalaji-b

@nagabalaji-b nagabalaji-b commented May 25, 2026

Copy link
Copy Markdown

Summary

This PR addresses normalization for OpenAPI 3.1 annotated enums, where enum values are expressed using oneOf + const. The main focus is on ensuring C# behavior aligns with traditional enum handling.

Problem

OpenAPI 3.0 typically utilizes enum arrays. In contrast, OpenAPI 3.1 often adopts an annotated enum style with oneOf + const and per-value descriptions. Prior to this fix, const-based composed schemas were not consistently simplified into enum-like schemas, potentially impacting strong enum generation in C#.

Example (OAS 3.1)

StatusType:
type: string
oneOf:
- const: ""
description: Unknown status
- const: ACTIVE
description: Active status
- const: INACTIVE
description: Inactive status
default: ""
x-omitempty: true

Equivalent OAS 3.0 Shape

StatusType:
type: string
enum:
- ""
- ACTIVE
- INACTIVE
default: ""
x-omitempty: true

C# Before vs After

Before Fix (String Property)

[DataMember(Name = "statusType", EmitDefaultValue = true)]
public string StatusType { get; set; }

private string _statusType;
private bool _flagStatusType;

The property is a plain string because oneOf + const was not normalized to enum semantics.

After Fix (Strong Enum)

[JsonConverter(typeof(StringEnumConverter))]
public enum StatusTypeEnum
{
/// /// Enum Empty for value: 
/// [EnumMember(Value = "")]
Empty = 1,

/// /// Enum ACTIVE for value: ACTIVE
/// [EnumMember(Value = "ACTIVE")]
ACTIVE = 2,

/// /// Enum INACTIVE for value: INACTIVE
/// [EnumMember(Value = "INACTIVE")]
INACTIVE = 3
}

[DataMember(Name = "statusType", EmitDefaultValue = true)]
public StatusTypeEnum? StatusType { get; set; }

After normalization, the schema is recognized as an enum and generates a strongly-typed enum with the appropriate JSON converter and member attributes.

Actual Behavior Before

OAS 3.1 oneOf + const was not consistently normalized to enum semantics. C# output could revert to non-enum-style modeling for this pattern.

Expected Behavior

OAS 3.1 oneOf + const should normalize in the same manner as OAS 3.0 enums. C# should consistently produce strongly typed enum output.

Changes in This PR

  • Treat const as a single enum value when an enum is absent in composed sub-schemas.
  • Apply composed enum simplification consistently for oneOf and anyOf enum-like paths.
  • Update OAS 3.1 normalizer test expectations for the new normalized shape.
  • Regenerate samples and documentation for the generator.

C# Result After Fix (Illustrative)

[JsonConverter(typeof(StringEnumConverter))]
public enum ComponentTypeEnum
{
/// /// Enum Empty for value: 
/// [EnumMember(Value = "")]
Empty = 1,

/// /// Enum CORE for value: CORE
/// [EnumMember(Value = "CORE")]
CORE = 2,

/// /// Enum EDGE for value: EDGE
/// [EnumMember(Value = "EDGE")]
EDGE = 3
}

public class SampleModel
{
[DataMember(Name = "componentType", EmitDefaultValue = true)]
public ComponentTypeEnum? ComponentType { get; set; }
}

Validation

The targeted normalizer test for OAS 3.1 simplifying oneOf/anyOf paths passed. Samples have been regenerated for Java configurations. Documentation for the generator has been exported.

Compatibility

This is a bug fix only. No intended breaking behavior changes. The normalization improvement is generator-agnostic; C# is the primary verified use case.

PR Checklist

  • Read the contribution guidelines.
  • The Pull Request title clearly describes the work in the pull request, and the Pull Request description provides details on how to validate the work. Missing information here may result in a delayed response from the community.
  • Run the following commands to build the project and update samples:
./mvnw clean package -DskipTests
./bin/generate-samples.sh bin/configs/csharp-restsharp*.yaml bin/configs/csharp-httpclient*.yaml
./bin/utils/export_docs_generators.sh
  • File the PR against the correct branch: master.
  • If your PR targets a specific programming language, @mention the technical committee members for that language:
    @muttleyxd @devhl-labs @lucasheim @shibayan (C# generators)

Summary by cubic

Normalize OpenAPI 3.1 annotated enums (oneOf + const) as true enums. This ensures that C# code generation produces strong enums consistently, matching OAS 3.0 behavior.

  • Bug Fixes
  • Treat const as a single enum value when an enum is missing in sub-schemas.
  • Collapse oneOf/anyOf enum-like schemas into a single enum in the parent, preserving types and per-value descriptions.
  • Update tests to expect enums after normalization; regenerate documentation and samples.
    Written for commit 51e0b0f. Summary will update with new commits. Review in cubic

sagadira and others added 4 commits April 24, 2026 12:23
…oding on .NET 9+

On .NET 9+, HttpUtility.ParseQueryString() already URL-encodes values internally.
The previous fix only checked for '.NET 9' literally, missing .NET 10+ (PowerShell 7.6).
Replace the string prefix check with a numeric major version comparison (>= 9) and
move the RuntimeInformation check outside the foreach loop to avoid redundant parsing.

Fixes: CSCwu08056
fix(csharp): numeric .NET version check to prevent double URL-encoding on .NET 9+ (CSCwu08056)

@cubic-dev-ai cubic-dev-ai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

3 issues found across 8 files

Reply with feedback, questions, or to request a fix.

Re-trigger cubic

Comment thread docs/generators/swift6.md Outdated
Comment thread docs/generators/swift6.md Outdated
Comment thread docs/generators/java-inflector.md Outdated
if (subSchema.getEnum() == null || subSchema.getEnum().isEmpty()) {
// Check if this sub-schema has an enum or const value (OpenAPI 3.1 uses const for single-value enums)
List<Object> subSchemaEnumValues = subSchema.getEnum();
if ((subSchemaEnumValues == null || subSchemaEnumValues.isEmpty()) && subSchema.getConst() == null) {

@Mattias-Sehlstedt Mattias-Sehlstedt May 25, 2026

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Rather than doing (subSchemaEnumValues == null || subSchemaEnumValues.isEmpty()) twice, could we extract this to a variable boolean definesEnum = ModelUtils.hasEnum(subSchema) and then have

if (!definesEnum && subSchema.getConst() == null) {

This could allow use to skip the comments almost entirely and instead allow the variable/method names be the explanation themselves.

Comment thread docs/generators/fsharp-functions.md Outdated
|sortModelPropertiesByRequiredFlag|Sort model properties to place required parameters before optional parameters.| |true|
|sortParamsByRequiredFlag|Sort method arguments to place required parameters before optional parameters.| |true|
|sourceFolder|source folder for generated code| |OpenAPI/src|
|sourceFolder|source folder for generated code| |OpenAPI\src|

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Are these path changes intentional?

@Mattias-Sehlstedt

Copy link
Copy Markdown
Contributor

Thanks for adjusting to the suggestions. 👍

I am not a maintainer of the project, so I cannot make a formal review with an approve.

// After normalization, oneOf with const values should be simplified to enum
assertEquals(schema16.getOneOf(), null);
assertEquals(schema16.getEnum().size(), 3);
assertEquals(schema16.getEnum().get(0), 1);

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

(I'm not repo maintainer but the PR is something I was thinking about myself: so thank you)

The test against deprecated (https://github.com/nagabalaji-b/openapi-generator/blob/3496c3b7cffbc0cdc728cc77bd64708b8ba6ae9a/modules/openapi-generator/src/test/resources/3_1/simplifyOneOfAnyOf_test.yaml#L133) is lost here (and the information from schema too)

Here, seems it is not possible with current enum support implementation ("simple" list of values and optional list of descriptions with x-enum-descriptions) to support enum as "schema" enabling to keep all "schemas" related information with no loss.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

@nagabalaji-b can you please take a look and add back the deprecated test?

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

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

@wing328 Done. I added back the deprecated test coverage:

Code change: Modified simplifyComposedSchemaWithEnums() to collect and preserve per-value deprecated flags from OAS 3.1 oneOf/anyOf + const sub-schemas into x-enum-deprecated extension.

Test assertion: Added explicit checks in testOpenAPINormalizerSimplifyOneOfAnyOf31Spec() for TypeIntegerWithOneOf schema:

Verifies x-enum-deprecated list size = 3
Verifies flags match expected values: [true, false, false] (first enum value was deprecated in the original YAML)
Validation: Ran the test locally and confirmed BUILD SUCCESS.

The deprecated metadata from the annotated enum pattern (oneOf + const) is now preserved through normalization, so generators can access it via the x-enum-deprecated extension.

@nagabalaji-b nagabalaji-b requested a review from wing328 June 9, 2026 12:06
@wing328

wing328 commented Jun 17, 2026

Copy link
Copy Markdown
Member

@cubic-dev-ai cubic-dev-ai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

1 issue found across 74 files (changes from recent commits).

Tip: Review your code locally with the cubic CLI to iterate faster.

Re-trigger cubic

Comment thread samples/client/echo_api/php-nextgen/phpunit.xml.dist Outdated
@nagabalaji-b nagabalaji-b force-pushed the openapitools-master branch from ddbb944 to b45f152 Compare June 18, 2026 11:27

@cubic-dev-ai cubic-dev-ai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

1 issue found across 74 files (changes from recent commits).

Tip: Review your code locally with the cubic CLI to iterate faster.

Re-trigger cubic

@nagabalaji-b

Copy link
Copy Markdown
Author

please update the samples to fix https://github.com/OpenAPITools/openapi-generator/actions/runs/26996697206/job/81834665420?pr=23869

@wing328 , i ran bin\generate-samples.sh and commited the generated samples.
Kindly the trigger the job.
Thanks

@cubic-dev-ai cubic-dev-ai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

7 issues found across 746 files

Prompt for AI agents (unresolved issues)

Check if these issues are valid — if so, understand the root cause of each and fix them. If appropriate, use sub-agents to investigate and fix each issue separately.


<file name="samples/client/petstore/csharp/generichost/latest/AnnotatedEnum/README.md">

<violation number="1" location="samples/client/petstore/csharp/generichost/latest/AnnotatedEnum/README.md:2">
P2: Typo in README link label: `REAMDE` should be `README`. This appears to propagate from the C# generichost Mustache template (`modules/openapi-generator/src/main/resources/csharp/libraries/generichost/README.solution.mustache:2`).</violation>
</file>

<file name="samples/client/petstore/csharp/generichost/latest/AnnotatedEnum/appveyor.yml">

<violation number="1" location="samples/client/petstore/csharp/generichost/latest/AnnotatedEnum/appveyor.yml:3">
P2: AppVeyor image Visual Studio 2019 is too old for the sample's net10.0 target framework</violation>
</file>

<file name="samples/client/petstore/csharp/generichost/latest/AnnotatedEnum/docs/models/ParentWithPluralOneOfPropertyNumber.md">

<violation number="1" location="samples/client/petstore/csharp/generichost/latest/AnnotatedEnum/docs/models/ParentWithPluralOneOfPropertyNumber.md:6">
P2: Model documentation is missing properties for `ParentWithPluralOneOfPropertyNumber`</violation>
</file>

<file name="samples/client/petstore/csharp/generichost/latest/AnnotatedEnum/src/Org.OpenAPITools/Client/DateTimeJsonConverter.cs">

<violation number="1" location="samples/client/petstore/csharp/generichost/latest/AnnotatedEnum/src/Org.OpenAPITools/Client/DateTimeJsonConverter.cs:26">
P2: Public mutable `static string[] Formats` exposes internal deserialization state, allowing external mutation that can silently alter accepted date formats at runtime.</violation>
</file>

<file name="samples/client/petstore/csharp/generichost/latest/AnnotatedEnum/src/Org.OpenAPITools/Client/DateOnlyJsonConverter.cs">

<violation number="1" location="samples/client/petstore/csharp/generichost/latest/AnnotatedEnum/src/Org.OpenAPITools/Client/DateOnlyJsonConverter.cs:41">
P2: Custom System.Text.Json converter throws NotSupportedException for invalid/null JSON values instead of JsonException</violation>
</file>

<file name="samples/client/petstore/csharp/generichost/latest/AnnotatedEnum/docs/scripts/git_push.ps1">

<violation number="1" location="samples/client/petstore/csharp/generichost/latest/AnnotatedEnum/docs/scripts/git_push.ps1:20">
P2: Remote detection logic incorrectly checks for any remote but unconditionally uses `origin`. If a repo has a different remote (e.g. `upstream`) and no `origin`, the existence check passes but `git pull origin` will fail.</violation>
</file>

<file name="samples/client/petstore/csharp/generichost/latest/AnnotatedEnum/src/Org.OpenAPITools/Client/DateTimeNullableJsonConverter.cs">

<violation number="1" location="samples/client/petstore/csharp/generichost/latest/AnnotatedEnum/src/Org.OpenAPITools/Client/DateTimeNullableJsonConverter.cs:55">
P1: Invalid date-time strings are silently coerced to null instead of throwing a deserialization exception, conflating malformed input with explicit JSON null and masking upstream data issues.</violation>
</file>

Note: This PR contains a large number of files. cubic only reviews up to 40 files per PR, so some files may not have been reviewed. cubic prioritizes the most important files to review.
On a pro plan you can use ultrareview for larger PRs.

Re-trigger cubic

/// <returns></returns>
public override DateTime? Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) {
if (reader.TokenType == JsonTokenType.Null)
return null;

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

P1: Invalid date-time strings are silently coerced to null instead of throwing a deserialization exception, conflating malformed input with explicit JSON null and masking upstream data issues.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At samples/client/petstore/csharp/generichost/latest/AnnotatedEnum/src/Org.OpenAPITools/Client/DateTimeNullableJsonConverter.cs, line 55:

<comment>Invalid date-time strings are silently coerced to null instead of throwing a deserialization exception, conflating malformed input with explicit JSON null and masking upstream data issues.</comment>

<file context>
@@ -0,0 +1,80 @@
+        /// <returns></returns>
+        public override DateTime? Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) {
+            if (reader.TokenType == JsonTokenType.Null)
+                return null;
+
+            string value = reader.GetString()!;
</file context>

@@ -0,0 +1,2 @@
# Created with Openapi Generator
See the project's [REAMDE](src/Org.OpenAPITools/README.md) No newline at end of file

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

P2: Typo in README link label: REAMDE should be README. This appears to propagate from the C# generichost Mustache template (modules/openapi-generator/src/main/resources/csharp/libraries/generichost/README.solution.mustache:2).

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At samples/client/petstore/csharp/generichost/latest/AnnotatedEnum/README.md, line 2:

<comment>Typo in README link label: `REAMDE` should be `README`. This appears to propagate from the C# generichost Mustache template (`modules/openapi-generator/src/main/resources/csharp/libraries/generichost/README.solution.mustache:2`).</comment>

<file context>
@@ -0,0 +1,2 @@
+# Created with Openapi Generator
+See the project's [REAMDE](src/Org.OpenAPITools/README.md)
\ No newline at end of file
</file context>

@@ -0,0 +1,9 @@
# auto-generated by OpenAPI Generator (https://github.com/OpenAPITools/openapi-generator)
#
image: Visual Studio 2019

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

P2: AppVeyor image Visual Studio 2019 is too old for the sample's net10.0 target framework

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At samples/client/petstore/csharp/generichost/latest/AnnotatedEnum/appveyor.yml, line 3:

<comment>AppVeyor image Visual Studio 2019 is too old for the sample's net10.0 target framework</comment>

<file context>
@@ -0,0 +1,9 @@
+# auto-generated by OpenAPI Generator (https://github.com/OpenAPITools/openapi-generator)
+#
+image: Visual Studio 2019
+clone_depth: 1
+build_script:
</file context>
Suggested change
image: Visual Studio 2019
image: Visual Studio 2022

## Properties

Name | Type | Description | Notes
------------ | ------------- | ------------- | -------------

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

P2: Model documentation is missing properties for ParentWithPluralOneOfPropertyNumber

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At samples/client/petstore/csharp/generichost/latest/AnnotatedEnum/docs/models/ParentWithPluralOneOfPropertyNumber.md, line 6:

<comment>Model documentation is missing properties for `ParentWithPluralOneOfPropertyNumber`</comment>

<file context>
@@ -0,0 +1,9 @@
+## Properties
+
+Name | Type | Description | Notes
+------------ | ------------- | ------------- | -------------
+
+[[Back to Model list]](../../README.md#documentation-for-models) [[Back to API list]](../../README.md#documentation-for-api-endpoints) [[Back to README]](../../README.md)
</file context>

/// <summary>
/// The formats used to deserialize the date
/// </summary>
public static string[] Formats { get; } = {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

P2: Public mutable static string[] Formats exposes internal deserialization state, allowing external mutation that can silently alter accepted date formats at runtime.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At samples/client/petstore/csharp/generichost/latest/AnnotatedEnum/src/Org.OpenAPITools/Client/DateTimeJsonConverter.cs, line 26:

<comment>Public mutable `static string[] Formats` exposes internal deserialization state, allowing external mutation that can silently alter accepted date formats at runtime.</comment>

<file context>
@@ -0,0 +1,75 @@
+        /// <summary>
+        /// The formats used to deserialize the date
+        /// </summary>
+        public static string[] Formats { get; } = {
+            "yyyy'-'MM'-'dd'T'HH':'mm':'ss'.'fffffffK",
+            "yyyy'-'MM'-'dd'T'HH':'mm':'ss'.'ffffffK",
</file context>

/// <returns></returns>
public override DateOnly Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) {
if (reader.TokenType == JsonTokenType.Null)
throw new NotSupportedException();

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

P2: Custom System.Text.Json converter throws NotSupportedException for invalid/null JSON values instead of JsonException

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At samples/client/petstore/csharp/generichost/latest/AnnotatedEnum/src/Org.OpenAPITools/Client/DateOnlyJsonConverter.cs, line 41:

<comment>Custom System.Text.Json converter throws NotSupportedException for invalid/null JSON values instead of JsonException</comment>

<file context>
@@ -0,0 +1,61 @@
+        /// <returns></returns>
+        public override DateOnly Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) {
+            if (reader.TokenType == JsonTokenType.Null)
+                throw new NotSupportedException();
+
+            string value = reader.GetString()!;
</file context>

git add .
git commit -am "${Message}"
$branchName=$(git rev-parse --abbrev-ref HEAD)
$gitRemote=$(git remote)

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

P2: Remote detection logic incorrectly checks for any remote but unconditionally uses origin. If a repo has a different remote (e.g. upstream) and no origin, the existence check passes but git pull origin will fail.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At samples/client/petstore/csharp/generichost/latest/AnnotatedEnum/docs/scripts/git_push.ps1, line 20:

<comment>Remote detection logic incorrectly checks for any remote but unconditionally uses `origin`. If a repo has a different remote (e.g. `upstream`) and no `origin`, the existence check passes but `git pull origin` will fail.</comment>

<file context>
@@ -0,0 +1,75 @@
+    git add .
+    git commit -am "${Message}"
+    $branchName=$(git rev-parse --abbrev-ref HEAD)
+    $gitRemote=$(git remote)
+
+    if([string]::IsNullOrWhiteSpace($gitRemote)){
</file context>
Suggested change
$gitRemote=$(git remote)
$gitRemote=$(git remote | Where-Object { $_ -eq "origin" })

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

6 participants