From 2b7b258f7f7d600e61c2ee1c61bb6a501b3af66a Mon Sep 17 00:00:00 2001 From: Simon Schulte Date: Fri, 27 Mar 2026 11:17:42 +0100 Subject: [PATCH 1/4] fix(csharp): use enum rendering functions for array-of-enum properties in JSON converter Array-typed properties whose items are enums fell through to the generic JsonSerializer.Serialize/Deserialize path, which ignored the defined *ValueConverter.ToJsonValue/*FromStringOrDefault functions and serialized enum values using the default .NET JSON behaviour (PascalCase names). Add explicit {{#items.isEnum}} branches in both the read (Deserialize) and write (Serialize) paths of the generated JsonConverter so that each item in an enum array is processed via the same converter functions used for scalar enum properties. Co-Authored-By: Claude Sonnet 4.6 --- .../generichost/JsonConverter.mustache | 46 +++++++++++++++++ .../test/resources/3_0/csharp/enum-array.yaml | 51 +++++++++++++++++++ 2 files changed, 97 insertions(+) create mode 100644 modules/openapi-generator/src/test/resources/3_0/csharp/enum-array.yaml diff --git a/modules/openapi-generator/src/main/resources/csharp/libraries/generichost/JsonConverter.mustache b/modules/openapi-generator/src/main/resources/csharp/libraries/generichost/JsonConverter.mustache index 24810c9cee43..6c58c1f67b54 100644 --- a/modules/openapi-generator/src/main/resources/csharp/libraries/generichost/JsonConverter.mustache +++ b/modules/openapi-generator/src/main/resources/csharp/libraries/generichost/JsonConverter.mustache @@ -251,7 +251,21 @@ {{^isNumeric}} {{^isDate}} {{^isDateTime}} + {{#items.isEnum}} + var {{#lambda.camelcase_sanitize_param}}{{name}}{{/lambda.camelcase_sanitize_param}}Items = new List<{{#items.isInnerEnum}}{{classname}}.{{/items.isInnerEnum}}{{{items.datatypeWithEnum}}}>(); + while (utf8JsonReader.Read()) + { + if (utf8JsonReader.TokenType == JsonTokenType.EndArray) + break; + string{{nrt?}} {{#lambda.camelcase_sanitize_param}}{{name}}{{/lambda.camelcase_sanitize_param}}ItemRawValue = utf8JsonReader.GetString(); + if ({{#lambda.camelcase_sanitize_param}}{{name}}{{/lambda.camelcase_sanitize_param}}ItemRawValue != null) + {{#lambda.camelcase_sanitize_param}}{{name}}{{/lambda.camelcase_sanitize_param}}Items.Add({{^items.isInnerEnum}}{{{items.datatypeWithEnum}}}ValueConverter.FromStringOrDefault({{#lambda.camelcase_sanitize_param}}{{name}}{{/lambda.camelcase_sanitize_param}}ItemRawValue){{/items.isInnerEnum}}{{#items.isInnerEnum}}{{classname}}.{{{items.datatypeWithEnum}}}FromStringOrDefault({{#lambda.camelcase_sanitize_param}}{{name}}{{/lambda.camelcase_sanitize_param}}ItemRawValue){{/items.isInnerEnum}}); + } + {{#lambda.camelcase_sanitize_param}}{{name}}{{/lambda.camelcase_sanitize_param}} = {{>OptionProperty}}{{#lambda.camelcase_sanitize_param}}{{name}}{{/lambda.camelcase_sanitize_param}}Items); + {{/items.isEnum}} + {{^items.isEnum}} {{#lambda.camelcase_sanitize_param}}{{name}}{{/lambda.camelcase_sanitize_param}} = {{>OptionProperty}}JsonSerializer.Deserialize<{{{datatypeWithEnum}}}>(ref utf8JsonReader, jsonSerializerOptions){{^isNullable}}{{nrt!}}{{/isNullable}}); + {{/items.isEnum}} {{/isDateTime}} {{/isDate}} {{/isNumeric}} @@ -612,14 +626,30 @@ if ({{#lambda.camelcase_sanitize_param}}{{classname}}{{/lambda.camelcase_sanitize_param}}.{{name}} != null) { writer.WritePropertyName("{{baseName}}"); + {{^items.isEnum}} JsonSerializer.Serialize(writer, {{#lambda.camelcase_sanitize_param}}{{classname}}{{/lambda.camelcase_sanitize_param}}.{{name}}, jsonSerializerOptions); + {{/items.isEnum}} + {{#items.isEnum}} + writer.WriteStartArray(); + foreach (var {{#lambda.camelcase_sanitize_param}}{{name}}{{/lambda.camelcase_sanitize_param}}Item in {{#lambda.camelcase_sanitize_param}}{{classname}}{{/lambda.camelcase_sanitize_param}}.{{name}}) + {{^items.isNumeric}}writer.WriteStringValue({{^items.isInnerEnum}}{{{items.datatypeWithEnum}}}ValueConverter.ToJsonValue({{#lambda.camelcase_sanitize_param}}{{name}}{{/lambda.camelcase_sanitize_param}}Item){{/items.isInnerEnum}}{{#items.isInnerEnum}}{{classname}}.{{{items.datatypeWithEnum}}}ToJsonValue({{#lambda.camelcase_sanitize_param}}{{name}}{{/lambda.camelcase_sanitize_param}}Item){{/items.isInnerEnum}});{{/items.isNumeric}}{{#items.isNumeric}}writer.WriteNumberValue({{^items.isInnerEnum}}{{{items.datatypeWithEnum}}}ValueConverter.ToJsonValue({{#lambda.camelcase_sanitize_param}}{{name}}{{/lambda.camelcase_sanitize_param}}Item){{/items.isInnerEnum}}{{#items.isInnerEnum}}{{classname}}.{{{items.datatypeWithEnum}}}ToJsonValue({{#lambda.camelcase_sanitize_param}}{{name}}{{/lambda.camelcase_sanitize_param}}Item){{/items.isInnerEnum}});{{/items.isNumeric}} + writer.WriteEndArray(); + {{/items.isEnum}} } else writer.WriteNull("{{baseName}}"); {{/isNullable}} {{^isNullable}} writer.WritePropertyName("{{baseName}}"); + {{^items.isEnum}} JsonSerializer.Serialize(writer, {{#lambda.camelcase_sanitize_param}}{{classname}}{{/lambda.camelcase_sanitize_param}}.{{name}}, jsonSerializerOptions); + {{/items.isEnum}} + {{#items.isEnum}} + writer.WriteStartArray(); + foreach (var {{#lambda.camelcase_sanitize_param}}{{name}}{{/lambda.camelcase_sanitize_param}}Item in {{#lambda.camelcase_sanitize_param}}{{classname}}{{/lambda.camelcase_sanitize_param}}.{{name}}) + {{^items.isNumeric}}writer.WriteStringValue({{^items.isInnerEnum}}{{{items.datatypeWithEnum}}}ValueConverter.ToJsonValue({{#lambda.camelcase_sanitize_param}}{{name}}{{/lambda.camelcase_sanitize_param}}Item){{/items.isInnerEnum}}{{#items.isInnerEnum}}{{classname}}.{{{items.datatypeWithEnum}}}ToJsonValue({{#lambda.camelcase_sanitize_param}}{{name}}{{/lambda.camelcase_sanitize_param}}Item){{/items.isInnerEnum}});{{/items.isNumeric}}{{#items.isNumeric}}writer.WriteNumberValue({{^items.isInnerEnum}}{{{items.datatypeWithEnum}}}ValueConverter.ToJsonValue({{#lambda.camelcase_sanitize_param}}{{name}}{{/lambda.camelcase_sanitize_param}}Item){{/items.isInnerEnum}}{{#items.isInnerEnum}}{{classname}}.{{{items.datatypeWithEnum}}}ToJsonValue({{#lambda.camelcase_sanitize_param}}{{name}}{{/lambda.camelcase_sanitize_param}}Item){{/items.isInnerEnum}});{{/items.isNumeric}} + writer.WriteEndArray(); + {{/items.isEnum}} {{/isNullable}} {{/required}} {{^required}} @@ -628,7 +658,15 @@ if ({{#lambda.camelcase_sanitize_param}}{{classname}}{{/lambda.camelcase_sanitize_param}}.{{name}}Option.Value != null) { writer.WritePropertyName("{{baseName}}"); + {{^items.isEnum}} JsonSerializer.Serialize(writer, {{#lambda.camelcase_sanitize_param}}{{classname}}{{/lambda.camelcase_sanitize_param}}.{{name}}, jsonSerializerOptions); + {{/items.isEnum}} + {{#items.isEnum}} + writer.WriteStartArray(); + foreach (var {{#lambda.camelcase_sanitize_param}}{{name}}{{/lambda.camelcase_sanitize_param}}Item in {{#lambda.camelcase_sanitize_param}}{{classname}}{{/lambda.camelcase_sanitize_param}}.{{name}}Option.Value{{nrt!}}) + {{^items.isNumeric}}writer.WriteStringValue({{^items.isInnerEnum}}{{{items.datatypeWithEnum}}}ValueConverter.ToJsonValue({{#lambda.camelcase_sanitize_param}}{{name}}{{/lambda.camelcase_sanitize_param}}Item){{/items.isInnerEnum}}{{#items.isInnerEnum}}{{classname}}.{{{items.datatypeWithEnum}}}ToJsonValue({{#lambda.camelcase_sanitize_param}}{{name}}{{/lambda.camelcase_sanitize_param}}Item){{/items.isInnerEnum}});{{/items.isNumeric}}{{#items.isNumeric}}writer.WriteNumberValue({{^items.isInnerEnum}}{{{items.datatypeWithEnum}}}ValueConverter.ToJsonValue({{#lambda.camelcase_sanitize_param}}{{name}}{{/lambda.camelcase_sanitize_param}}Item){{/items.isInnerEnum}}{{#items.isInnerEnum}}{{classname}}.{{{items.datatypeWithEnum}}}ToJsonValue({{#lambda.camelcase_sanitize_param}}{{name}}{{/lambda.camelcase_sanitize_param}}Item){{/items.isInnerEnum}});{{/items.isNumeric}} + writer.WriteEndArray(); + {{/items.isEnum}} } else writer.WriteNull("{{baseName}}"); @@ -636,7 +674,15 @@ {{^isNullable}} { writer.WritePropertyName("{{baseName}}"); + {{^items.isEnum}} JsonSerializer.Serialize(writer, {{#lambda.camelcase_sanitize_param}}{{classname}}{{/lambda.camelcase_sanitize_param}}.{{name}}, jsonSerializerOptions); + {{/items.isEnum}} + {{#items.isEnum}} + writer.WriteStartArray(); + foreach (var {{#lambda.camelcase_sanitize_param}}{{name}}{{/lambda.camelcase_sanitize_param}}Item in {{#lambda.camelcase_sanitize_param}}{{classname}}{{/lambda.camelcase_sanitize_param}}.{{name}}{{nrt!}}) + {{^items.isNumeric}}writer.WriteStringValue({{^items.isInnerEnum}}{{{items.datatypeWithEnum}}}ValueConverter.ToJsonValue({{#lambda.camelcase_sanitize_param}}{{name}}{{/lambda.camelcase_sanitize_param}}Item){{/items.isInnerEnum}}{{#items.isInnerEnum}}{{classname}}.{{{items.datatypeWithEnum}}}ToJsonValue({{#lambda.camelcase_sanitize_param}}{{name}}{{/lambda.camelcase_sanitize_param}}Item){{/items.isInnerEnum}});{{/items.isNumeric}}{{#items.isNumeric}}writer.WriteNumberValue({{^items.isInnerEnum}}{{{items.datatypeWithEnum}}}ValueConverter.ToJsonValue({{#lambda.camelcase_sanitize_param}}{{name}}{{/lambda.camelcase_sanitize_param}}Item){{/items.isInnerEnum}}{{#items.isInnerEnum}}{{classname}}.{{{items.datatypeWithEnum}}}ToJsonValue({{#lambda.camelcase_sanitize_param}}{{name}}{{/lambda.camelcase_sanitize_param}}Item){{/items.isInnerEnum}});{{/items.isNumeric}} + writer.WriteEndArray(); + {{/items.isEnum}} } {{/isNullable}} {{/required}} diff --git a/modules/openapi-generator/src/test/resources/3_0/csharp/enum-array.yaml b/modules/openapi-generator/src/test/resources/3_0/csharp/enum-array.yaml new file mode 100644 index 000000000000..26e5f6873504 --- /dev/null +++ b/modules/openapi-generator/src/test/resources/3_0/csharp/enum-array.yaml @@ -0,0 +1,51 @@ +openapi: 3.0.0 +info: + title: Enum Array Test + description: >- + Tests that array-of-enum properties are serialized and deserialized using + the defined enum rendering functions rather than falling back to default + PascalCase JSON serialization. + version: 1.0.0 + license: + name: Apache-2.0 + url: 'https://www.apache.org/licenses/LICENSE-2.0.html' +paths: + /shipments: + get: + operationId: GetShipments + tags: + - Shipments + responses: + '200': + description: Returns a list of shipments + content: + application/json: + schema: + type: array + items: + $ref: '#/components/schemas/Shipment' +servers: + - url: 'http://localhost' +components: + schemas: + Shipment: + type: object + required: + - id + - allowedMethods + properties: + id: + type: integer + format: int32 + allowedMethods: + type: array + items: + $ref: '#/components/schemas/ShippingMethod' + preferredMethod: + $ref: '#/components/schemas/ShippingMethod' + ShippingMethod: + type: string + enum: + - standard + - express + - overnight From d0ba6160858aeff09afa8b57bd69c7fee6c50802 Mon Sep 17 00:00:00 2001 From: Simon Schulte Date: Fri, 27 Mar 2026 12:28:16 +0100 Subject: [PATCH 2/4] fix(csharp): guard array-of-enum deserialization against null JSON values Without the null check, a JSON null value for an array-of-enum property caused the reader to advance past the null token and attempt to read the next property as array content, desynchronizing the Utf8JsonReader. --- .../generichost/JsonConverter.mustache | 19 +++++++++++-------- 1 file changed, 11 insertions(+), 8 deletions(-) diff --git a/modules/openapi-generator/src/main/resources/csharp/libraries/generichost/JsonConverter.mustache b/modules/openapi-generator/src/main/resources/csharp/libraries/generichost/JsonConverter.mustache index 6c58c1f67b54..813a58bd2316 100644 --- a/modules/openapi-generator/src/main/resources/csharp/libraries/generichost/JsonConverter.mustache +++ b/modules/openapi-generator/src/main/resources/csharp/libraries/generichost/JsonConverter.mustache @@ -252,16 +252,19 @@ {{^isDate}} {{^isDateTime}} {{#items.isEnum}} - var {{#lambda.camelcase_sanitize_param}}{{name}}{{/lambda.camelcase_sanitize_param}}Items = new List<{{#items.isInnerEnum}}{{classname}}.{{/items.isInnerEnum}}{{{items.datatypeWithEnum}}}>(); - while (utf8JsonReader.Read()) + if (utf8JsonReader.TokenType != JsonTokenType.Null) { - if (utf8JsonReader.TokenType == JsonTokenType.EndArray) - break; - string{{nrt?}} {{#lambda.camelcase_sanitize_param}}{{name}}{{/lambda.camelcase_sanitize_param}}ItemRawValue = utf8JsonReader.GetString(); - if ({{#lambda.camelcase_sanitize_param}}{{name}}{{/lambda.camelcase_sanitize_param}}ItemRawValue != null) - {{#lambda.camelcase_sanitize_param}}{{name}}{{/lambda.camelcase_sanitize_param}}Items.Add({{^items.isInnerEnum}}{{{items.datatypeWithEnum}}}ValueConverter.FromStringOrDefault({{#lambda.camelcase_sanitize_param}}{{name}}{{/lambda.camelcase_sanitize_param}}ItemRawValue){{/items.isInnerEnum}}{{#items.isInnerEnum}}{{classname}}.{{{items.datatypeWithEnum}}}FromStringOrDefault({{#lambda.camelcase_sanitize_param}}{{name}}{{/lambda.camelcase_sanitize_param}}ItemRawValue){{/items.isInnerEnum}}); + var {{#lambda.camelcase_sanitize_param}}{{name}}{{/lambda.camelcase_sanitize_param}}Items = new List<{{#items.isInnerEnum}}{{classname}}.{{/items.isInnerEnum}}{{{items.datatypeWithEnum}}}>(); + while (utf8JsonReader.Read()) + { + if (utf8JsonReader.TokenType == JsonTokenType.EndArray) + break; + string{{nrt?}} {{#lambda.camelcase_sanitize_param}}{{name}}{{/lambda.camelcase_sanitize_param}}ItemRawValue = utf8JsonReader.GetString(); + if ({{#lambda.camelcase_sanitize_param}}{{name}}{{/lambda.camelcase_sanitize_param}}ItemRawValue != null) + {{#lambda.camelcase_sanitize_param}}{{name}}{{/lambda.camelcase_sanitize_param}}Items.Add({{^items.isInnerEnum}}{{{items.datatypeWithEnum}}}ValueConverter.FromStringOrDefault({{#lambda.camelcase_sanitize_param}}{{name}}{{/lambda.camelcase_sanitize_param}}ItemRawValue){{/items.isInnerEnum}}{{#items.isInnerEnum}}{{classname}}.{{{items.datatypeWithEnum}}}FromStringOrDefault({{#lambda.camelcase_sanitize_param}}{{name}}{{/lambda.camelcase_sanitize_param}}ItemRawValue){{/items.isInnerEnum}}); + } + {{#lambda.camelcase_sanitize_param}}{{name}}{{/lambda.camelcase_sanitize_param}} = {{>OptionProperty}}{{#lambda.camelcase_sanitize_param}}{{name}}{{/lambda.camelcase_sanitize_param}}Items); } - {{#lambda.camelcase_sanitize_param}}{{name}}{{/lambda.camelcase_sanitize_param}} = {{>OptionProperty}}{{#lambda.camelcase_sanitize_param}}{{name}}{{/lambda.camelcase_sanitize_param}}Items); {{/items.isEnum}} {{^items.isEnum}} {{#lambda.camelcase_sanitize_param}}{{name}}{{/lambda.camelcase_sanitize_param}} = {{>OptionProperty}}JsonSerializer.Deserialize<{{{datatypeWithEnum}}}>(ref utf8JsonReader, jsonSerializerOptions){{^isNullable}}{{nrt!}}{{/isNullable}}); From ba82277fe91c49112e1d4c38dcf7b2d8e4eded64 Mon Sep 17 00:00:00 2001 From: Simon Schulte Date: Tue, 7 Apr 2026 14:04:40 +0200 Subject: [PATCH 3/4] Apply recommended null-fix --- .../csharp/libraries/generichost/JsonConverter.mustache | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/modules/openapi-generator/src/main/resources/csharp/libraries/generichost/JsonConverter.mustache b/modules/openapi-generator/src/main/resources/csharp/libraries/generichost/JsonConverter.mustache index 813a58bd2316..e8f7fde2179e 100644 --- a/modules/openapi-generator/src/main/resources/csharp/libraries/generichost/JsonConverter.mustache +++ b/modules/openapi-generator/src/main/resources/csharp/libraries/generichost/JsonConverter.mustache @@ -251,8 +251,11 @@ {{^isNumeric}} {{^isDate}} {{^isDateTime}} + {{#isArray}} {{#items.isEnum}} - if (utf8JsonReader.TokenType != JsonTokenType.Null) + if (utf8JsonReader.TokenType == JsonTokenType.Null) + {{#lambda.camelcase_sanitize_param}}{{name}}{{/lambda.camelcase_sanitize_param}} = {{>OptionProperty}}null); + else { var {{#lambda.camelcase_sanitize_param}}{{name}}{{/lambda.camelcase_sanitize_param}}Items = new List<{{#items.isInnerEnum}}{{classname}}.{{/items.isInnerEnum}}{{{items.datatypeWithEnum}}}>(); while (utf8JsonReader.Read()) @@ -269,6 +272,10 @@ {{^items.isEnum}} {{#lambda.camelcase_sanitize_param}}{{name}}{{/lambda.camelcase_sanitize_param}} = {{>OptionProperty}}JsonSerializer.Deserialize<{{{datatypeWithEnum}}}>(ref utf8JsonReader, jsonSerializerOptions){{^isNullable}}{{nrt!}}{{/isNullable}}); {{/items.isEnum}} + {{/isArray}} + {{^isArray}} + {{#lambda.camelcase_sanitize_param}}{{name}}{{/lambda.camelcase_sanitize_param}} = {{>OptionProperty}}JsonSerializer.Deserialize<{{{datatypeWithEnum}}}>(ref utf8JsonReader, jsonSerializerOptions){{^isNullable}}{{nrt!}}{{/isNullable}}); + {{/isArray}} {{/isDateTime}} {{/isDate}} {{/isNumeric}} From 66bc169d05e18143858ca9f9bf325834a1982f68 Mon Sep 17 00:00:00 2001 From: Simon Schulte Date: Tue, 7 Apr 2026 14:05:04 +0200 Subject: [PATCH 4/4] remove test-yml and extend petstore with single new property instead --- .../test/resources/3_0/csharp/enum-array.yaml | 51 ------------------- ...odels-for-testing-with-http-signature.yaml | 8 +++ 2 files changed, 8 insertions(+), 51 deletions(-) delete mode 100644 modules/openapi-generator/src/test/resources/3_0/csharp/enum-array.yaml diff --git a/modules/openapi-generator/src/test/resources/3_0/csharp/enum-array.yaml b/modules/openapi-generator/src/test/resources/3_0/csharp/enum-array.yaml deleted file mode 100644 index 26e5f6873504..000000000000 --- a/modules/openapi-generator/src/test/resources/3_0/csharp/enum-array.yaml +++ /dev/null @@ -1,51 +0,0 @@ -openapi: 3.0.0 -info: - title: Enum Array Test - description: >- - Tests that array-of-enum properties are serialized and deserialized using - the defined enum rendering functions rather than falling back to default - PascalCase JSON serialization. - version: 1.0.0 - license: - name: Apache-2.0 - url: 'https://www.apache.org/licenses/LICENSE-2.0.html' -paths: - /shipments: - get: - operationId: GetShipments - tags: - - Shipments - responses: - '200': - description: Returns a list of shipments - content: - application/json: - schema: - type: array - items: - $ref: '#/components/schemas/Shipment' -servers: - - url: 'http://localhost' -components: - schemas: - Shipment: - type: object - required: - - id - - allowedMethods - properties: - id: - type: integer - format: int32 - allowedMethods: - type: array - items: - $ref: '#/components/schemas/ShippingMethod' - preferredMethod: - $ref: '#/components/schemas/ShippingMethod' - ShippingMethod: - type: string - enum: - - standard - - express - - overnight diff --git a/modules/openapi-generator/src/test/resources/3_0/csharp/petstore-with-fake-endpoints-models-for-testing-with-http-signature.yaml b/modules/openapi-generator/src/test/resources/3_0/csharp/petstore-with-fake-endpoints-models-for-testing-with-http-signature.yaml index 1c9e901ad0dd..f477f9443501 100644 --- a/modules/openapi-generator/src/test/resources/3_0/csharp/petstore-with-fake-endpoints-models-for-testing-with-http-signature.yaml +++ b/modules/openapi-generator/src/test/resources/3_0/csharp/petstore-with-fake-endpoints-models-for-testing-with-http-signature.yaml @@ -2039,6 +2039,14 @@ components: enum: - fish - crab + array_enum_nullable: + type: array + nullable: true + items: + type: string + enum: + - fish + - crab FreeFormObject: type: object description: A schema consisting only of additional properties