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 23 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 23 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

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

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

No issues found across 3 files

Re-trigger cubic

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

10 issues found across 84 files (changes from recent commits).

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/src/Org.OpenAPITools.Test/Api/ApiTestsBase.cs">

<violation number="1" location="samples/client/petstore/csharp/generichost/latest/AnnotatedEnum/src/Org.OpenAPITools.Test/Api/ApiTestsBase.cs:22">
P2: Embedded manual instructions in generated test base file reference non-existent file `ApiTests.Base.cs` (actual file is `ApiTestsBase.cs`) and method `ApiTestsBase#AddApiHttpClients` which is not defined on this class; it exists on `HostConfiguration` instead. These stale template comments mislead developers following setup steps.</violation>
</file>

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

<violation number="1" location="samples/client/petstore/csharp/generichost/latest/AnnotatedEnum/src/Org.OpenAPITools/Model/OneOfNullAndRef.cs:100">
P2: Deserializer accepts top-level JSON arrays for an object model, silently producing a default instance instead of rejecting malformed input.</violation>

<violation number="2" location="samples/client/petstore/csharp/generichost/latest/AnnotatedEnum/src/Org.OpenAPITools/Model/OneOfNullAndRef.cs:165">
P1: Serializer can throw when `Number` is explicitly set to null because nullable `.Value` is dereferenced under `IsSet`.</violation>
</file>

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

<violation number="1" location="samples/client/petstore/csharp/generichost/latest/AnnotatedEnum/src/Org.OpenAPITools/Model/AnyOfStringArrayOfString.cs:112">
P1: Deserializer incorrectly rejects raw string payloads and silently drops array payloads for `anyOf(string, array<string>)`</violation>
</file>

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

<violation number="1" location="samples/client/petstore/csharp/generichost/latest/AnnotatedEnum/src/Org.OpenAPITools/Model/ParentWithPluralOneOfPropertyNumber.cs:37">
P2: Public oneOf wrapper type has only internal constructors, making it non-instantiable for external SDK consumers</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: Deserializer silently converts invalid date-time strings to null instead of throwing an exception, making invalid data indistinguishable from JSON null and inconsistent with the non-nullable DateTimeJsonConverter.</violation>
</file>

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

<violation number="1" location="samples/client/petstore/csharp/generichost/latest/AnnotatedEnum/src/Org.OpenAPITools/Model/TypeIntegerWithOneOf.cs:119">
P1: Integer enum JSON converter uses string-based read/write instead of numeric tokens, breaking interoperability for integer-typed OpenAPI enums.</violation>
</file>

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

<violation number="1" location="samples/client/petstore/csharp/generichost/latest/AnnotatedEnum/src/Org.OpenAPITools/Model/OneOfNullableTest.cs:125">
P0: OneOfNullableTestJsonConverter.Read uses copies of the un-advanced utf8JsonReader instead of utf8JsonReaderOneOf, causing both TryDeserialize calls to start from the root of the JSON document rather than from the current property value. This prevents correct oneOf variant detection and will throw JsonException for valid payloads.</violation>
</file>

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

<violation number="1" location="samples/client/petstore/csharp/generichost/latest/AnnotatedEnum/src/Org.OpenAPITools/Model/OneOfNullAndRef2.cs:100">
P2: Converter accepts `StartArray` for an object model, silently deserializing invalid JSON arrays instead of rejecting them. This weakens schema enforcement and creates an asymmetric contract since `Write()` only produces objects.</violation>

<violation number="2" location="samples/client/petstore/csharp/generichost/latest/AnnotatedEnum/src/Org.OpenAPITools/Model/OneOfNullAndRef2.cs:165">
P1: Serializer can throw when `NumberOption` is set to null due to forced nullable dereference (`Number!.Value`). The `Number` setter allows `null`, and `Option<T>.IsSet` is `true` even when the wrapped value is `null`. `WriteProperties` only checks `IsSet` before dereferencing `Number!.Value`, which will crash at runtime if `Number` is null. The same file's `Read` method explicitly rejects this state with `ArgumentNullException`, and `ParentWithPluralOneOfProperty.cs` in the same sample has a defensive pre-write null check, demonstrating the generator should emit one here as well.</violation>
</file>

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

Re-trigger cubic


if (utf8JsonReaderOneOf.TokenType == JsonTokenType.PropertyName && currentDepth == utf8JsonReaderOneOf.CurrentDepth - 1)
{
Utf8JsonReader utf8JsonReaderInt = utf8JsonReader;

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.

P0: OneOfNullableTestJsonConverter.Read uses copies of the un-advanced utf8JsonReader instead of utf8JsonReaderOneOf, causing both TryDeserialize calls to start from the root of the JSON document rather than from the current property value. This prevents correct oneOf variant detection and will throw JsonException for valid payloads.

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/Model/OneOfNullableTest.cs, line 125:

<comment>OneOfNullableTestJsonConverter.Read uses copies of the un-advanced utf8JsonReader instead of utf8JsonReaderOneOf, causing both TryDeserialize calls to start from the root of the JSON document rather than from the current property value. This prevents correct oneOf variant detection and will throw JsonException for valid payloads.</comment>

<file context>
@@ -0,0 +1,190 @@
+
+                if (utf8JsonReaderOneOf.TokenType == JsonTokenType.PropertyName && currentDepth == utf8JsonReaderOneOf.CurrentDepth - 1)
+                {
+                    Utf8JsonReader utf8JsonReaderInt = utf8JsonReader;
+                    ClientUtils.TryDeserialize<int?>(ref utf8JsonReaderInt, jsonSerializerOptions, out varInt);
+
</file context>

{
if (oneOfNullAndRef.NumberOption.IsSet)
{
var numberRawValue = NumberValueConverter.ToJsonValue(oneOfNullAndRef.Number!.Value);

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: Serializer can throw when Number is explicitly set to null because nullable .Value is dereferenced under IsSet.

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/Model/OneOfNullAndRef.cs, line 165:

<comment>Serializer can throw when `Number` is explicitly set to null because nullable `.Value` is dereferenced under `IsSet`.</comment>

<file context>
@@ -0,0 +1,170 @@
+        {
+            if (oneOfNullAndRef.NumberOption.IsSet)
+            {
+                var numberRawValue = NumberValueConverter.ToJsonValue(oneOfNullAndRef.Number!.Value);
+                writer.WriteString("number", numberRawValue);
+            }
</file context>

{
int currentDepth = utf8JsonReader.CurrentDepth;

if (utf8JsonReader.TokenType != JsonTokenType.StartObject && utf8JsonReader.TokenType != JsonTokenType.StartArray)

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: Deserializer incorrectly rejects raw string payloads and silently drops array payloads for anyOf(string, array<string>)

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/Model/AnyOfStringArrayOfString.cs, line 112:

<comment>Deserializer incorrectly rejects raw string payloads and silently drops array payloads for `anyOf(string, array<string>)`</comment>

<file context>
@@ -0,0 +1,202 @@
+        {
+            int currentDepth = utf8JsonReader.CurrentDepth;
+
+            if (utf8JsonReader.TokenType != JsonTokenType.StartObject && utf8JsonReader.TokenType != JsonTokenType.StartArray)
+                throw new JsonException();
+
</file context>

/// <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: Deserializer silently converts invalid date-time strings to null instead of throwing an exception, making invalid data indistinguishable from JSON null and inconsistent with the non-nullable DateTimeJsonConverter.

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>Deserializer silently converts invalid date-time strings to null instead of throwing an exception, making invalid data indistinguishable from JSON null and inconsistent with the non-nullable DateTimeJsonConverter.</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>

/// <returns></returns>
public override TypeIntegerWithOneOf Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options)
{
string? rawValue = reader.GetString();

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: Integer enum JSON converter uses string-based read/write instead of numeric tokens, breaking interoperability for integer-typed OpenAPI enums.

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/Model/TypeIntegerWithOneOf.cs, line 119:

<comment>Integer enum JSON converter uses string-based read/write instead of numeric tokens, breaking interoperability for integer-typed OpenAPI enums.</comment>

<file context>
@@ -0,0 +1,180 @@
+        /// <returns></returns>
+        public override TypeIntegerWithOneOf Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options)
+        {
+            string? rawValue = reader.GetString();
+
+            TypeIntegerWithOneOf? result = rawValue == null
</file context>

{
if (oneOfNullAndRef2.NumberOption.IsSet)
{
var numberRawValue = NumberValueConverter.ToJsonValue(oneOfNullAndRef2.Number!.Value);

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: Serializer can throw when NumberOption is set to null due to forced nullable dereference (Number!.Value). The Number setter allows null, and Option<T>.IsSet is true even when the wrapped value is null. WriteProperties only checks IsSet before dereferencing Number!.Value, which will crash at runtime if Number is null. The same file's Read method explicitly rejects this state with ArgumentNullException, and ParentWithPluralOneOfProperty.cs in the same sample has a defensive pre-write null check, demonstrating the generator should emit one here as well.

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/Model/OneOfNullAndRef2.cs, line 165:

<comment>Serializer can throw when `NumberOption` is set to null due to forced nullable dereference (`Number!.Value`). The `Number` setter allows `null`, and `Option<T>.IsSet` is `true` even when the wrapped value is `null`. `WriteProperties` only checks `IsSet` before dereferencing `Number!.Value`, which will crash at runtime if `Number` is null. The same file's `Read` method explicitly rejects this state with `ArgumentNullException`, and `ParentWithPluralOneOfProperty.cs` in the same sample has a defensive pre-write null check, demonstrating the generator should emit one here as well.</comment>

<file context>
@@ -0,0 +1,170 @@
+        {
+            if (oneOfNullAndRef2.NumberOption.IsSet)
+            {
+                var numberRawValue = NumberValueConverter.ToJsonValue(oneOfNullAndRef2.Number!.Value);
+                writer.WriteString("number", numberRawValue);
+            }
</file context>

* Follow these manual steps to construct tests.
* This file will not be overwritten.
* *********************************************************************************
* 1. Navigate to ApiTests.Base.cs and ensure any tokens are being created correctly.

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: Embedded manual instructions in generated test base file reference non-existent file ApiTests.Base.cs (actual file is ApiTestsBase.cs) and method ApiTestsBase#AddApiHttpClients which is not defined on this class; it exists on HostConfiguration instead. These stale template comments mislead developers following setup steps.

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.Test/Api/ApiTestsBase.cs, line 22:

<comment>Embedded manual instructions in generated test base file reference non-existent file `ApiTests.Base.cs` (actual file is `ApiTestsBase.cs`) and method `ApiTestsBase#AddApiHttpClients` which is not defined on this class; it exists on `HostConfiguration` instead. These stale template comments mislead developers following setup steps.</comment>

<file context>
@@ -0,0 +1,58 @@
+*              Follow these manual steps to construct tests.
+*              This file will not be overwritten.
+*  *********************************************************************************
+* 1. Navigate to ApiTests.Base.cs and ensure any tokens are being created correctly.
+*    Take care not to commit credentials to any repository.
+*
</file context>

{
int currentDepth = utf8JsonReader.CurrentDepth;

if (utf8JsonReader.TokenType != JsonTokenType.StartObject && utf8JsonReader.TokenType != JsonTokenType.StartArray)

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: Deserializer accepts top-level JSON arrays for an object model, silently producing a default instance instead of rejecting malformed input.

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/Model/OneOfNullAndRef.cs, line 100:

<comment>Deserializer accepts top-level JSON arrays for an object model, silently producing a default instance instead of rejecting malformed input.</comment>

<file context>
@@ -0,0 +1,170 @@
+        {
+            int currentDepth = utf8JsonReader.CurrentDepth;
+
+            if (utf8JsonReader.TokenType != JsonTokenType.StartObject && utf8JsonReader.TokenType != JsonTokenType.StartArray)
+                throw new JsonException();
+
</file context>

/// Initializes a new instance of the <see cref="ParentWithPluralOneOfPropertyNumber" /> class.
/// </summary>
/// <param name="number"></param>
internal ParentWithPluralOneOfPropertyNumber(Number number)

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 oneOf wrapper type has only internal constructors, making it non-instantiable for external SDK consumers

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/Model/ParentWithPluralOneOfPropertyNumber.cs, line 37:

<comment>Public oneOf wrapper type has only internal constructors, making it non-instantiable for external SDK consumers</comment>

<file context>
@@ -0,0 +1,189 @@
+        /// Initializes a new instance of the <see cref="ParentWithPluralOneOfPropertyNumber" /> class.
+        /// </summary>
+        /// <param name="number"></param>
+        internal ParentWithPluralOneOfPropertyNumber(Number number)
+        {
+            Number = number;
</file context>

{
int currentDepth = utf8JsonReader.CurrentDepth;

if (utf8JsonReader.TokenType != JsonTokenType.StartObject && utf8JsonReader.TokenType != JsonTokenType.StartArray)

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: Converter accepts StartArray for an object model, silently deserializing invalid JSON arrays instead of rejecting them. This weakens schema enforcement and creates an asymmetric contract since Write() only produces objects.

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/Model/OneOfNullAndRef2.cs, line 100:

<comment>Converter accepts `StartArray` for an object model, silently deserializing invalid JSON arrays instead of rejecting them. This weakens schema enforcement and creates an asymmetric contract since `Write()` only produces objects.</comment>

<file context>
@@ -0,0 +1,170 @@
+        {
+            int currentDepth = utf8JsonReader.CurrentDepth;
+
+            if (utf8JsonReader.TokenType != JsonTokenType.StartObject && utf8JsonReader.TokenType != JsonTokenType.StartArray)
+                throw new JsonException();
+
</file context>

@@ -0,0 +1,10 @@
# for csharp generichost - OAS 3.1 annotated enum with const + deprecated
generatorName: csharp
outputDir: samples/client/petstore/csharp/generichost/latest/AnnotatedEnum

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.

please add the new output folder to the c# github workflow so that it will be tested moving forward.

https://github.com/OpenAPITools/openapi-generator/blob/master/.github/workflows/samples-dotnet10.yaml#L26

@wing328 wing328 added this to the 7.24.0 milestone Jun 29, 2026
@wing328 wing328 added the OpenAPI Normalizer Normalize the spec for easier processing label Jun 29, 2026
@devhl-labs

Copy link
Copy Markdown
Contributor

Is a new sample necessary? Can it be added to the petstore sample?

@wing328

wing328 commented Jun 29, 2026

Copy link
Copy Markdown
Member

@devhl-labs thanks for reviewing this change.

I'll take a look later to see if these samples are needed and whether these can be added to existing specs/samples instead.

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

4 issues found across 88 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/src/Org.OpenAPITools/Model/OneOfNullableTest.cs">

<violation number="1" location="samples/client/petstore/csharp/generichost/latest/AnnotatedEnum/src/Org.OpenAPITools/Model/OneOfNullableTest.cs:195">
P1: WriteProperties is empty, causing serialization to produce an empty JSON object and lose all property data</violation>
</file>

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

<violation number="1" location="samples/client/petstore/csharp/generichost/latest/AnnotatedEnum/src/Org.OpenAPITools/Model/PropertiesWithAnyOf.cs:134">
P2: Object converter `PropertiesWithAnyOfJsonConverter` accepts `StartArray` for a non-collection model and silently returns an empty object instead of throwing `JsonException`.</violation>
</file>

<file name="modules/openapi-generator/src/main/java/org/openapitools/codegen/OpenAPINormalizer.java">

<violation number="1" location="modules/openapi-generator/src/main/java/org/openapitools/codegen/OpenAPINormalizer.java:1652">
P2: `const: null` is conflated with an absent const because `getConst() == null` cannot distinguish the two. In OAS 3.1 / JSON Schema, `null` is a valid const value, so a oneOf/anyOf branch with `const: null` (or a mixed schema containing it) causes the normalizer to bail out and skip enum simplification instead of treating the null as a valid enum member. In practice this means nullable annotated enums expressed with `const: null` will not be normalized.</violation>
</file>

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

<violation number="1" location="samples/client/petstore/csharp/generichost/latest/AnnotatedEnum/src/Org.OpenAPITools/Model/ParentWithOneOfProperty.cs:134">
P1: `number` null semantics are inconsistent between read and write paths. On deserialization, explicit JSON `null` causes `GetString()` to return null, bypassing the Option assignment so `IsSet` stays false and the explicit null is silently lost. On serialization, if `NumberOption.IsSet` is true but the value is null (possible via the public setter), `Number!.Value` dereferences null and throws at runtime.</violation>
</file>

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

Re-trigger cubic

/// <param name="oneOfNullableTest"></param>
/// <param name="jsonSerializerOptions"></param>
/// <exception cref="NotImplementedException"></exception>
public void WriteProperties(Utf8JsonWriter writer, OneOfNullableTest oneOfNullableTest, JsonSerializerOptions jsonSerializerOptions)

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: WriteProperties is empty, causing serialization to produce an empty JSON object and lose all property data

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/Model/OneOfNullableTest.cs, line 195:

<comment>WriteProperties is empty, causing serialization to produce an empty JSON object and lose all property data</comment>

<file context>
@@ -0,0 +1,200 @@
+        /// <param name="oneOfNullableTest"></param>
+        /// <param name="jsonSerializerOptions"></param>
+        /// <exception cref="NotImplementedException"></exception>
+        public void WriteProperties(Utf8JsonWriter writer, OneOfNullableTest oneOfNullableTest, JsonSerializerOptions jsonSerializerOptions)
+        {
+
</file context>

{
case "number":
string? numberRawValue = utf8JsonReader.GetString();
if (numberRawValue != 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: number null semantics are inconsistent between read and write paths. On deserialization, explicit JSON null causes GetString() to return null, bypassing the Option assignment so IsSet stays false and the explicit null is silently lost. On serialization, if NumberOption.IsSet is true but the value is null (possible via the public setter), Number!.Value dereferences null and throws 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/Model/ParentWithOneOfProperty.cs, line 134:

<comment>`number` null semantics are inconsistent between read and write paths. On deserialization, explicit JSON `null` causes `GetString()` to return null, bypassing the Option assignment so `IsSet` stays false and the explicit null is silently lost. On serialization, if `NumberOption.IsSet` is true but the value is null (possible via the public setter), `Number!.Value` dereferences null and throws at runtime.</comment>

<file context>
@@ -0,0 +1,180 @@
+                    {
+                        case "number":
+                            string? numberRawValue = utf8JsonReader.GetString();
+                            if (numberRawValue != null)
+                                number = new Option<Number?>(NumberValueConverter.FromStringOrDefault(numberRawValue));
+                            break;
</file context>

{
int currentDepth = utf8JsonReader.CurrentDepth;

if (utf8JsonReader.TokenType != JsonTokenType.StartObject && utf8JsonReader.TokenType != JsonTokenType.StartArray)

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: Object converter PropertiesWithAnyOfJsonConverter accepts StartArray for a non-collection model and silently returns an empty object instead of throwing 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/Model/PropertiesWithAnyOf.cs, line 134:

<comment>Object converter `PropertiesWithAnyOfJsonConverter` accepts `StartArray` for a non-collection model and silently returns an empty object instead of throwing `JsonException`.</comment>

<file context>
@@ -0,0 +1,209 @@
+        {
+            int currentDepth = utf8JsonReader.CurrentDepth;
+
+            if (utf8JsonReader.TokenType != JsonTokenType.StartObject && utf8JsonReader.TokenType != JsonTokenType.StartArray)
+                throw new JsonException();
+
</file context>
Suggested change
if (utf8JsonReader.TokenType != JsonTokenType.StartObject && utf8JsonReader.TokenType != JsonTokenType.StartArray)
if (utf8JsonReader.TokenType != JsonTokenType.StartObject)
throw new JsonException();

if (subSchema.getEnum() == null || subSchema.getEnum().isEmpty()) {
// Check if this sub-schema has an enum or const value (OAS 3.1 uses const for single-value enums)
boolean definesEnum = ModelUtils.hasEnum(subSchema);
if (!definesEnum && subSchema.getConst() == 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.

P2: const: null is conflated with an absent const because getConst() == null cannot distinguish the two. In OAS 3.1 / JSON Schema, null is a valid const value, so a oneOf/anyOf branch with const: null (or a mixed schema containing it) causes the normalizer to bail out and skip enum simplification instead of treating the null as a valid enum member. In practice this means nullable annotated enums expressed with const: null will not be normalized.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At modules/openapi-generator/src/main/java/org/openapitools/codegen/OpenAPINormalizer.java, line 1652:

<comment>`const: null` is conflated with an absent const because `getConst() == null` cannot distinguish the two. In OAS 3.1 / JSON Schema, `null` is a valid const value, so a oneOf/anyOf branch with `const: null` (or a mixed schema containing it) causes the normalizer to bail out and skip enum simplification instead of treating the null as a valid enum member. In practice this means nullable annotated enums expressed with `const: null` will not be normalized.</comment>

<file context>
@@ -1646,10 +1647,15 @@ protected Schema simplifyComposedSchemaWithEnums(Schema schema, List<Object> sub
-            if (subSchema.getEnum() == null || subSchema.getEnum().isEmpty()) {
+            // Check if this sub-schema has an enum or const value (OAS 3.1 uses const for single-value enums)
+            boolean definesEnum = ModelUtils.hasEnum(subSchema);
+            if (!definesEnum && subSchema.getConst() == null) {
                 return schema;
             }
</file context>

…convention

CI's samples job runs bin/generate-samples.sh twice. On the second pass the generator skips idempotent files (.openapi-generator-ignore and Test/{Api,Model}/*Tests.cs already on disk), so they are omitted from the FILES manifest. Other generichost samples (e.g. ComposedEnum) follow this convention; AnnotatedEnum's FILES was previously committed after a single regen and included 17 stale entries. Regenerated to match CI output.
@JPPortier

Copy link
Copy Markdown
Contributor

Hi not really reviewing full list of of updates because of out if time and may be not the best one.

Even if I'm myself interested by the feature at short term because of it is a quick-win, I'm questioning about long term: we are adding a new vendor extension to support the deprecation for enums so another dedicated one.

Was there some reflexion to have a new "CodegenEnum" usage based onto same pattern in place for schema, variables, ...
Then with proper overload function from generator (fromXXX) it will enable more and not based onto vendor extension .

@wing328 what is you feedback ? (not to change this PR but regarding long term solution outside of vendor extension usage)

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

Labels

OpenAPI Normalizer Normalize the spec for easier processing

Projects

None yet

Development

Successfully merging this pull request may close these issues.

7 participants