From bb6f3f100efe5d18b210108f866fb9c102afa695 Mon Sep 17 00:00:00 2001 From: Nathan Broadbent Date: Wed, 21 Oct 2026 04:05:49 +1300 Subject: [PATCH 1/2] [csharp] Map unknown enum values to the unknown default member With enumUnknownDefaultCase, the generator adds an UnknownDefaultOpenApi member to each enum, but FromString threw and FromStringOrDefault returned null for any value the enum doesn't list. The generichost JSON converters turn that null into a JsonException, so a response with a new enum value still failed to deserialize. Return the unknown default member from both methods when the option is on, for inner and top-level enums, as Java's fromValue does. Without the option nothing changes. --- .../main/resources/csharp/modelEnum.mustache | 12 ++++++++ .../resources/csharp/modelInnerEnum.mustache | 12 ++++++++ .../CSharpClientCodegenTest.java | 30 +++++++++++++++++++ 3 files changed, 54 insertions(+) diff --git a/modules/openapi-generator/src/main/resources/csharp/modelEnum.mustache b/modules/openapi-generator/src/main/resources/csharp/modelEnum.mustache index 96b408d8d37e..63a6a43f7325 100644 --- a/modules/openapi-generator/src/main/resources/csharp/modelEnum.mustache +++ b/modules/openapi-generator/src/main/resources/csharp/modelEnum.mustache @@ -62,7 +62,13 @@ {{/enumVars}} {{/allowableValues}} + {{#enumUnknownDefaultCase}} + // Unknown values map to the unknown default member + return {{#allowableValues}}{{#enumVars}}{{#-last}}{{datatypeWithEnum}}{{^datatypeWithEnum}}{{classname}}{{/datatypeWithEnum}}.{{name}}{{/-last}}{{/enumVars}}{{/allowableValues}}; + {{/enumUnknownDefaultCase}} + {{^enumUnknownDefaultCase}} throw new NotImplementedException($"Could not convert value to type {{datatypeWithEnum}}{{^datatypeWithEnum}}{{classname}}{{/datatypeWithEnum}}: '{value}'"); + {{/enumUnknownDefaultCase}} } /// @@ -79,7 +85,13 @@ {{/enumVars}} {{/allowableValues}} + {{#enumUnknownDefaultCase}} + // Unknown values map to the unknown default member + return {{#allowableValues}}{{#enumVars}}{{#-last}}{{datatypeWithEnum}}{{^datatypeWithEnum}}{{classname}}{{/datatypeWithEnum}}.{{name}}{{/-last}}{{/enumVars}}{{/allowableValues}}; + {{/enumUnknownDefaultCase}} + {{^enumUnknownDefaultCase}} return null; + {{/enumUnknownDefaultCase}} } /// diff --git a/modules/openapi-generator/src/main/resources/csharp/modelInnerEnum.mustache b/modules/openapi-generator/src/main/resources/csharp/modelInnerEnum.mustache index 90e2d9b3a583..310998609646 100644 --- a/modules/openapi-generator/src/main/resources/csharp/modelInnerEnum.mustache +++ b/modules/openapi-generator/src/main/resources/csharp/modelInnerEnum.mustache @@ -46,7 +46,13 @@ {{/enumVars}} {{/allowableValues}} + {{#enumUnknownDefaultCase}} + // Unknown values map to the unknown default member + return {{#allowableValues}}{{#enumVars}}{{#-last}}{{datatypeWithEnum}}{{^datatypeWithEnum}}{{classname}}{{/datatypeWithEnum}}.{{name}}{{/-last}}{{/enumVars}}{{/allowableValues}}; + {{/enumUnknownDefaultCase}} + {{^enumUnknownDefaultCase}} throw new NotImplementedException($"Could not convert value to type {{datatypeWithEnum}}{{^datatypeWithEnum}}{{classname}}{{/datatypeWithEnum}}: '{value}'"); + {{/enumUnknownDefaultCase}} } /// @@ -63,7 +69,13 @@ {{/enumVars}} {{/allowableValues}} + {{#enumUnknownDefaultCase}} + // Unknown values map to the unknown default member + return {{#allowableValues}}{{#enumVars}}{{#-last}}{{datatypeWithEnum}}{{^datatypeWithEnum}}{{classname}}{{/datatypeWithEnum}}.{{name}}{{/-last}}{{/enumVars}}{{/allowableValues}}; + {{/enumUnknownDefaultCase}} + {{^enumUnknownDefaultCase}} return null; + {{/enumUnknownDefaultCase}} } /// diff --git a/modules/openapi-generator/src/test/java/org/openapitools/codegen/csharpnetcore/CSharpClientCodegenTest.java b/modules/openapi-generator/src/test/java/org/openapitools/codegen/csharpnetcore/CSharpClientCodegenTest.java index 1b1d8e5a84fe..99c52cb204db 100644 --- a/modules/openapi-generator/src/test/java/org/openapitools/codegen/csharpnetcore/CSharpClientCodegenTest.java +++ b/modules/openapi-generator/src/test/java/org/openapitools/codegen/csharpnetcore/CSharpClientCodegenTest.java @@ -122,6 +122,36 @@ public void testGenericHostInjectedDateFormatOverridesProperty() throws IOExcept ".CreatedAtOption.Value!.Value.ToString(CreatedAtFormat, global::System.Globalization.CultureInfo.InvariantCulture)"); } + @Test + public void testGenericHostEnumUnknownDefaultCaseMapsUnknownValues() throws IOException { + OpenAPI openAPI = new OpenAPI().components(new Components() + .addSchemas("Color", new StringSchema()._enum(List.of("red", "green"))) + .addSchemas("Pet", new ObjectSchema() + .addProperty("status", new StringSchema()._enum(List.of("available", "sold"))) + .addProperty("color", new Schema<>().$ref("#/components/schemas/Color")))); + + File output = Files.createTempDirectory("test").toFile().getCanonicalFile(); + output.deleteOnExit(); + DefaultGenerator generator = new DefaultGenerator(); + ClientOptInput input = new ClientOptInput().openAPI(openAPI); + CSharpClientCodegen codegen = new CSharpClientCodegen(); + codegen.setLibrary("generichost"); + codegen.setOutputDir(output.getAbsolutePath()); + codegen.additionalProperties().put(CodegenConstants.ENUM_UNKNOWN_DEFAULT_CASE, "true"); + input.config(codegen); + + Map files = generator.opts(input).generate().stream() + .collect(Collectors.toMap(File::getPath, Function.identity())); + File pet = getGeneratedModel(files, output, "Pet"); + File color = getGeneratedModel(files, output, "Color"); + + // FromString and FromStringOrDefault both end by returning the unknown default member + assertFileContains(pet.toPath(), "return StatusEnum.UnknownDefaultOpenApi; }"); + assertFileNotContains(pet.toPath(), "Could not convert value to type StatusEnum"); + assertFileContains(color.toPath(), "return Color.UnknownDefaultOpenApi; }"); + assertFileNotContains(color.toPath(), "Could not convert value to type Color"); + } + @Test public void testGenericHostOneOfConstructorsRespectApiVisibility() throws IOException { Map publicModels = generateIssue23046Models(false); From 143efb788c48dd1df7d2325789ba59b64483fb6a Mon Sep 17 00:00:00 2001 From: Nathan Broadbent Date: Wed, 21 Oct 2026 21:53:17 +1300 Subject: [PATCH 2/2] [csharp] Assert that both enum parsers return the unknown default member Match each of FromString and FromStringOrDefault up to its closing brace, so the test fails if either method ends any other way, such as FromStringOrDefault returning null. --- .../csharpnetcore/CSharpClientCodegenTest.java | 12 ++++++++++-- 1 file changed, 10 insertions(+), 2 deletions(-) diff --git a/modules/openapi-generator/src/test/java/org/openapitools/codegen/csharpnetcore/CSharpClientCodegenTest.java b/modules/openapi-generator/src/test/java/org/openapitools/codegen/csharpnetcore/CSharpClientCodegenTest.java index 99c52cb204db..248d1167c777 100644 --- a/modules/openapi-generator/src/test/java/org/openapitools/codegen/csharpnetcore/CSharpClientCodegenTest.java +++ b/modules/openapi-generator/src/test/java/org/openapitools/codegen/csharpnetcore/CSharpClientCodegenTest.java @@ -146,9 +146,17 @@ public void testGenericHostEnumUnknownDefaultCaseMapsUnknownValues() throws IOEx File color = getGeneratedModel(files, output, "Color"); // FromString and FromStringOrDefault both end by returning the unknown default member - assertFileContains(pet.toPath(), "return StatusEnum.UnknownDefaultOpenApi; }"); + String returnsUnknown = "\\(string value\\)\\s*\\{[^}]*" + + "return %s\\.UnknownDefaultOpenApi;\\s*\\}"; + String statusReturnsUnknown = String.format(returnsUnknown, "StatusEnum"); + String colorReturnsUnknown = String.format(returnsUnknown, "Color"); + assertThat(Files.readString(pet.toPath())) + .containsPattern("StatusEnumFromString" + statusReturnsUnknown) + .containsPattern("StatusEnumFromStringOrDefault" + statusReturnsUnknown); assertFileNotContains(pet.toPath(), "Could not convert value to type StatusEnum"); - assertFileContains(color.toPath(), "return Color.UnknownDefaultOpenApi; }"); + assertThat(Files.readString(color.toPath())) + .containsPattern(" FromString" + colorReturnsUnknown) + .containsPattern(" FromStringOrDefault" + colorReturnsUnknown); assertFileNotContains(color.toPath(), "Could not convert value to type Color"); }