Skip to content

Commit 417840c

Browse files
authored
[csharp][generichost] Deserialize present-but-null nullable enums (#23912)
For a property that is both `required` and `nullable` with an enum type, the generated JsonConverter only assigned the backing Option when the raw JSON string was non-null. An explicit `null` therefore left the Option unset, and the required-property check then threw `ArgumentException: Property is required` — even though the property was present (just null). Assign the Option unconditionally for nullable enums, mapping a null raw value to a null enum value. Non-nullable enums keep the existing guard, so their generated output is unchanged. Regenerated all generichost samples. Adds CSharpClientCodegenTest#testGenericHostNullableEnumDeserializesPresentNull, which generates the generichost client and asserts the RequiredClass converter uses the null-tolerant read for the required+nullable enum while the non-nullable enum keeps the guard.
1 parent da8c31c commit 417840c

38 files changed

Lines changed: 154 additions & 220 deletions

File tree

modules/openapi-generator/src/main/resources/csharp/libraries/generichost/JsonConverter.mustache

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -230,13 +230,24 @@
230230
{{/isNumeric}}
231231
{{^isNumeric}}
232232
string{{nrt?}} {{#lambda.camelcase_sanitize_param}}{{name}}{{/lambda.camelcase_sanitize_param}}RawValue = utf8JsonReader.GetString();
233+
{{! A nullable enum may be explicitly null in the payload; still mark the Option as set so a present-but-null value is not rejected as missing. }}
233234
{{^isInnerEnum}}
235+
{{#isNullable}}
236+
{{#lambda.camelcase_sanitize_param}}{{name}}{{/lambda.camelcase_sanitize_param}} = {{>OptionProperty}}{{#lambda.camelcase_sanitize_param}}{{name}}{{/lambda.camelcase_sanitize_param}}RawValue == null ? null : {{{datatypeWithEnum}}}ValueConverter.FromStringOrDefault({{#lambda.camelcase_sanitize_param}}{{name}}{{/lambda.camelcase_sanitize_param}}RawValue));
237+
{{/isNullable}}
238+
{{^isNullable}}
234239
if ({{#lambda.camelcase_sanitize_param}}{{name}}{{/lambda.camelcase_sanitize_param}}RawValue != null)
235240
{{#lambda.camelcase_sanitize_param}}{{name}}{{/lambda.camelcase_sanitize_param}} = {{>OptionProperty}}{{{datatypeWithEnum}}}ValueConverter.FromStringOrDefault({{#lambda.camelcase_sanitize_param}}{{name}}{{/lambda.camelcase_sanitize_param}}RawValue));
241+
{{/isNullable}}
236242
{{/isInnerEnum}}
237243
{{#isInnerEnum}}
244+
{{#isNullable}}
245+
{{#lambda.camelcase_sanitize_param}}{{name}}{{/lambda.camelcase_sanitize_param}} = {{>OptionProperty}}{{#lambda.camelcase_sanitize_param}}{{name}}{{/lambda.camelcase_sanitize_param}}RawValue == null ? null : {{classname}}.{{{datatypeWithEnum}}}FromStringOrDefault({{#lambda.camelcase_sanitize_param}}{{name}}{{/lambda.camelcase_sanitize_param}}RawValue));
246+
{{/isNullable}}
247+
{{^isNullable}}
238248
if ({{#lambda.camelcase_sanitize_param}}{{name}}{{/lambda.camelcase_sanitize_param}}RawValue != null)
239249
{{#lambda.camelcase_sanitize_param}}{{name}}{{/lambda.camelcase_sanitize_param}} = {{>OptionProperty}}{{classname}}.{{{datatypeWithEnum}}}FromStringOrDefault({{#lambda.camelcase_sanitize_param}}{{name}}{{/lambda.camelcase_sanitize_param}}RawValue));
250+
{{/isNullable}}
240251
{{/isInnerEnum}}
241252
{{/isNumeric}}
242253
{{/isMap}}

modules/openapi-generator/src/test/java/org/openapitools/codegen/csharpnetcore/CSharpClientCodegenTest.java

Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -40,6 +40,39 @@
4040

4141
public class CSharpClientCodegenTest {
4242

43+
@Test
44+
public void testGenericHostNullableEnumDeserializesPresentNull() throws IOException {
45+
// For a required + nullable enum, the generated generichost JsonConverter must assign
46+
// the backing Option even when the JSON value is null; otherwise the required-property
47+
// check rejects a present-but-null value with "Property is required". Non-nullable enums
48+
// keep the original non-null guard, so their generated output is unchanged.
49+
File output = Files.createTempDirectory("test").toFile().getCanonicalFile();
50+
output.deleteOnExit();
51+
final OpenAPI openAPI = TestUtils.parseFlattenSpec(
52+
"src/test/resources/3_0/csharp/petstore-with-fake-endpoints-models-for-testing-with-http-signature.yaml");
53+
final DefaultGenerator defaultGenerator = new DefaultGenerator();
54+
final ClientOptInput clientOptInput = new ClientOptInput();
55+
clientOptInput.openAPI(openAPI);
56+
CSharpClientCodegen cSharpClientCodegen = new CSharpClientCodegen();
57+
cSharpClientCodegen.setLibrary("generichost");
58+
cSharpClientCodegen.setOutputDir(output.getAbsolutePath());
59+
clientOptInput.config(cSharpClientCodegen);
60+
defaultGenerator.opts(clientOptInput);
61+
62+
Map<String, File> files = defaultGenerator.generate().stream()
63+
.collect(Collectors.toMap(File::getPath, Function.identity()));
64+
65+
File requiredClass = files.get(Paths.get(output.getAbsolutePath(),
66+
"src", "Org.OpenAPITools", "Model", "RequiredClass.cs").toString());
67+
assertNotNull(requiredClass);
68+
// required + nullable enum: a null raw value still sets the Option (mapped to null)
69+
assertFileContains(requiredClass.toPath(),
70+
"requiredNullableEnumString = new Option<RequiredClass.RequiredNullableEnumStringEnum?>(requiredNullableEnumStringRawValue == null ? null :");
71+
// required + non-nullable enum: keeps the original guard (unchanged behavior)
72+
assertFileContains(requiredClass.toPath(),
73+
"if (requiredNotnullableEnumStringRawValue != null)");
74+
}
75+
4376
@Test
4477
public void testToEnumVarName() {
4578
final CSharpClientCodegen codegen = new CSharpClientCodegen();

samples/client/petstore/csharp/generichost/latest/UseDateTimeOffset/src/Org.OpenAPITools/Model/EnumTest.cs

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -753,8 +753,7 @@ public override EnumTest Read(ref Utf8JsonReader utf8JsonReader, Type typeToConv
753753
break;
754754
case "outerEnum":
755755
string? outerEnumRawValue = utf8JsonReader.GetString();
756-
if (outerEnumRawValue != null)
757-
outerEnum = new Option<OuterEnum?>(OuterEnumValueConverter.FromStringOrDefault(outerEnumRawValue));
756+
outerEnum = new Option<OuterEnum?>(outerEnumRawValue == null ? null : OuterEnumValueConverter.FromStringOrDefault(outerEnumRawValue));
758757
break;
759758
case "outerEnumDefaultValue":
760759
string? outerEnumDefaultValueRawValue = utf8JsonReader.GetString();

samples/client/petstore/csharp/generichost/latest/UseDateTimeOffset/src/Org.OpenAPITools/Model/RequiredClass.cs

Lines changed: 4 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1915,13 +1915,11 @@ public override RequiredClass Read(ref Utf8JsonReader utf8JsonReader, Type typeT
19151915
break;
19161916
case "notrequired_nullable_enum_string":
19171917
string? notrequiredNullableEnumStringRawValue = utf8JsonReader.GetString();
1918-
if (notrequiredNullableEnumStringRawValue != null)
1919-
notrequiredNullableEnumString = new Option<RequiredClass.NotrequiredNullableEnumStringEnum?>(RequiredClass.NotrequiredNullableEnumStringEnumFromStringOrDefault(notrequiredNullableEnumStringRawValue));
1918+
notrequiredNullableEnumString = new Option<RequiredClass.NotrequiredNullableEnumStringEnum?>(notrequiredNullableEnumStringRawValue == null ? null : RequiredClass.NotrequiredNullableEnumStringEnumFromStringOrDefault(notrequiredNullableEnumStringRawValue));
19201919
break;
19211920
case "notrequired_nullable_outerEnumDefaultValue":
19221921
string? notrequiredNullableOuterEnumDefaultValueRawValue = utf8JsonReader.GetString();
1923-
if (notrequiredNullableOuterEnumDefaultValueRawValue != null)
1924-
notrequiredNullableOuterEnumDefaultValue = new Option<OuterEnumDefaultValue?>(OuterEnumDefaultValueValueConverter.FromStringOrDefault(notrequiredNullableOuterEnumDefaultValueRawValue));
1922+
notrequiredNullableOuterEnumDefaultValue = new Option<OuterEnumDefaultValue?>(notrequiredNullableOuterEnumDefaultValueRawValue == null ? null : OuterEnumDefaultValueValueConverter.FromStringOrDefault(notrequiredNullableOuterEnumDefaultValueRawValue));
19251923
break;
19261924
case "notrequired_nullable_string_prop":
19271925
notrequiredNullableStringProp = new Option<string?>(utf8JsonReader.GetString());
@@ -1949,16 +1947,14 @@ public override RequiredClass Read(ref Utf8JsonReader utf8JsonReader, Type typeT
19491947
break;
19501948
case "required_nullable_enum_string":
19511949
string? requiredNullableEnumStringRawValue = utf8JsonReader.GetString();
1952-
if (requiredNullableEnumStringRawValue != null)
1953-
requiredNullableEnumString = new Option<RequiredClass.RequiredNullableEnumStringEnum?>(RequiredClass.RequiredNullableEnumStringEnumFromStringOrDefault(requiredNullableEnumStringRawValue));
1950+
requiredNullableEnumString = new Option<RequiredClass.RequiredNullableEnumStringEnum?>(requiredNullableEnumStringRawValue == null ? null : RequiredClass.RequiredNullableEnumStringEnumFromStringOrDefault(requiredNullableEnumStringRawValue));
19541951
break;
19551952
case "required_nullable_integer_prop":
19561953
requiredNullableIntegerProp = new Option<int?>(utf8JsonReader.TokenType == JsonTokenType.Null ? (int?)null : utf8JsonReader.GetInt32());
19571954
break;
19581955
case "required_nullable_outerEnumDefaultValue":
19591956
string? requiredNullableOuterEnumDefaultValueRawValue = utf8JsonReader.GetString();
1960-
if (requiredNullableOuterEnumDefaultValueRawValue != null)
1961-
requiredNullableOuterEnumDefaultValue = new Option<OuterEnumDefaultValue?>(OuterEnumDefaultValueValueConverter.FromStringOrDefault(requiredNullableOuterEnumDefaultValueRawValue));
1957+
requiredNullableOuterEnumDefaultValue = new Option<OuterEnumDefaultValue?>(requiredNullableOuterEnumDefaultValueRawValue == null ? null : OuterEnumDefaultValueValueConverter.FromStringOrDefault(requiredNullableOuterEnumDefaultValueRawValue));
19621958
break;
19631959
case "required_nullable_string_prop":
19641960
requiredNullableStringProp = new Option<string?>(utf8JsonReader.GetString());

samples/client/petstore/csharp/generichost/net10/FormModels/src/Org.OpenAPITools/Model/EnumTest.cs

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -281,8 +281,7 @@ public override EnumTest Read(ref Utf8JsonReader utf8JsonReader, Type typeToConv
281281
break;
282282
case "outerEnum":
283283
string outerEnumRawValue = utf8JsonReader.GetString();
284-
if (outerEnumRawValue != null)
285-
outerEnum = new Option<OuterEnum?>(OuterEnumValueConverter.FromStringOrDefault(outerEnumRawValue));
284+
outerEnum = new Option<OuterEnum?>(outerEnumRawValue == null ? null : OuterEnumValueConverter.FromStringOrDefault(outerEnumRawValue));
286285
break;
287286
case "outerEnumDefaultValue":
288287
string outerEnumDefaultValueRawValue = utf8JsonReader.GetString();

samples/client/petstore/csharp/generichost/net10/FormModels/src/Org.OpenAPITools/Model/RequiredClass.cs

Lines changed: 8 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -845,23 +845,19 @@ public override RequiredClass Read(ref Utf8JsonReader utf8JsonReader, Type typeT
845845
break;
846846
case "notrequired_nullable_enum_integer":
847847
string notrequiredNullableEnumIntegerRawValue = utf8JsonReader.GetString();
848-
if (notrequiredNullableEnumIntegerRawValue != null)
849-
notrequiredNullableEnumInteger = new Option<RequiredClassRequiredNullableEnumInteger?>(RequiredClassRequiredNullableEnumIntegerValueConverter.FromStringOrDefault(notrequiredNullableEnumIntegerRawValue));
848+
notrequiredNullableEnumInteger = new Option<RequiredClassRequiredNullableEnumInteger?>(notrequiredNullableEnumIntegerRawValue == null ? null : RequiredClassRequiredNullableEnumIntegerValueConverter.FromStringOrDefault(notrequiredNullableEnumIntegerRawValue));
850849
break;
851850
case "notrequired_nullable_enum_integer_only":
852851
string notrequiredNullableEnumIntegerOnlyRawValue = utf8JsonReader.GetString();
853-
if (notrequiredNullableEnumIntegerOnlyRawValue != null)
854-
notrequiredNullableEnumIntegerOnly = new Option<RequiredClassRequiredNullableEnumIntegerOnly?>(RequiredClassRequiredNullableEnumIntegerOnlyValueConverter.FromStringOrDefault(notrequiredNullableEnumIntegerOnlyRawValue));
852+
notrequiredNullableEnumIntegerOnly = new Option<RequiredClassRequiredNullableEnumIntegerOnly?>(notrequiredNullableEnumIntegerOnlyRawValue == null ? null : RequiredClassRequiredNullableEnumIntegerOnlyValueConverter.FromStringOrDefault(notrequiredNullableEnumIntegerOnlyRawValue));
855853
break;
856854
case "notrequired_nullable_enum_string":
857855
string notrequiredNullableEnumStringRawValue = utf8JsonReader.GetString();
858-
if (notrequiredNullableEnumStringRawValue != null)
859-
notrequiredNullableEnumString = new Option<RequiredClassRequiredNullableEnumString?>(RequiredClassRequiredNullableEnumStringValueConverter.FromStringOrDefault(notrequiredNullableEnumStringRawValue));
856+
notrequiredNullableEnumString = new Option<RequiredClassRequiredNullableEnumString?>(notrequiredNullableEnumStringRawValue == null ? null : RequiredClassRequiredNullableEnumStringValueConverter.FromStringOrDefault(notrequiredNullableEnumStringRawValue));
860857
break;
861858
case "notrequired_nullable_outerEnumDefaultValue":
862859
string notrequiredNullableOuterEnumDefaultValueRawValue = utf8JsonReader.GetString();
863-
if (notrequiredNullableOuterEnumDefaultValueRawValue != null)
864-
notrequiredNullableOuterEnumDefaultValue = new Option<OuterEnumDefaultValue?>(OuterEnumDefaultValueValueConverter.FromStringOrDefault(notrequiredNullableOuterEnumDefaultValueRawValue));
860+
notrequiredNullableOuterEnumDefaultValue = new Option<OuterEnumDefaultValue?>(notrequiredNullableOuterEnumDefaultValueRawValue == null ? null : OuterEnumDefaultValueValueConverter.FromStringOrDefault(notrequiredNullableOuterEnumDefaultValueRawValue));
865861
break;
866862
case "notrequired_nullable_string_prop":
867863
notrequiredNullableStringProp = new Option<string>(utf8JsonReader.GetString());
@@ -883,26 +879,22 @@ public override RequiredClass Read(ref Utf8JsonReader utf8JsonReader, Type typeT
883879
break;
884880
case "required_nullable_enum_integer":
885881
string requiredNullableEnumIntegerRawValue = utf8JsonReader.GetString();
886-
if (requiredNullableEnumIntegerRawValue != null)
887-
requiredNullableEnumInteger = new Option<RequiredClassRequiredNullableEnumInteger?>(RequiredClassRequiredNullableEnumIntegerValueConverter.FromStringOrDefault(requiredNullableEnumIntegerRawValue));
882+
requiredNullableEnumInteger = new Option<RequiredClassRequiredNullableEnumInteger?>(requiredNullableEnumIntegerRawValue == null ? null : RequiredClassRequiredNullableEnumIntegerValueConverter.FromStringOrDefault(requiredNullableEnumIntegerRawValue));
888883
break;
889884
case "required_nullable_enum_integer_only":
890885
string requiredNullableEnumIntegerOnlyRawValue = utf8JsonReader.GetString();
891-
if (requiredNullableEnumIntegerOnlyRawValue != null)
892-
requiredNullableEnumIntegerOnly = new Option<RequiredClassRequiredNullableEnumIntegerOnly?>(RequiredClassRequiredNullableEnumIntegerOnlyValueConverter.FromStringOrDefault(requiredNullableEnumIntegerOnlyRawValue));
886+
requiredNullableEnumIntegerOnly = new Option<RequiredClassRequiredNullableEnumIntegerOnly?>(requiredNullableEnumIntegerOnlyRawValue == null ? null : RequiredClassRequiredNullableEnumIntegerOnlyValueConverter.FromStringOrDefault(requiredNullableEnumIntegerOnlyRawValue));
893887
break;
894888
case "required_nullable_enum_string":
895889
string requiredNullableEnumStringRawValue = utf8JsonReader.GetString();
896-
if (requiredNullableEnumStringRawValue != null)
897-
requiredNullableEnumString = new Option<RequiredClassRequiredNullableEnumString?>(RequiredClassRequiredNullableEnumStringValueConverter.FromStringOrDefault(requiredNullableEnumStringRawValue));
890+
requiredNullableEnumString = new Option<RequiredClassRequiredNullableEnumString?>(requiredNullableEnumStringRawValue == null ? null : RequiredClassRequiredNullableEnumStringValueConverter.FromStringOrDefault(requiredNullableEnumStringRawValue));
898891
break;
899892
case "required_nullable_integer_prop":
900893
requiredNullableIntegerProp = new Option<int?>(utf8JsonReader.TokenType == JsonTokenType.Null ? (int?)null : utf8JsonReader.GetInt32());
901894
break;
902895
case "required_nullable_outerEnumDefaultValue":
903896
string requiredNullableOuterEnumDefaultValueRawValue = utf8JsonReader.GetString();
904-
if (requiredNullableOuterEnumDefaultValueRawValue != null)
905-
requiredNullableOuterEnumDefaultValue = new Option<OuterEnumDefaultValue?>(OuterEnumDefaultValueValueConverter.FromStringOrDefault(requiredNullableOuterEnumDefaultValueRawValue));
897+
requiredNullableOuterEnumDefaultValue = new Option<OuterEnumDefaultValue?>(requiredNullableOuterEnumDefaultValueRawValue == null ? null : OuterEnumDefaultValueValueConverter.FromStringOrDefault(requiredNullableOuterEnumDefaultValueRawValue));
906898
break;
907899
case "required_nullable_string_prop":
908900
requiredNullableStringProp = new Option<string>(utf8JsonReader.GetString());

samples/client/petstore/csharp/generichost/net10/NullReferenceTypes/src/Org.OpenAPITools/Model/EnumTest.cs

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -761,8 +761,7 @@ public override EnumTest Read(ref Utf8JsonReader utf8JsonReader, Type typeToConv
761761
break;
762762
case "outerEnum":
763763
string? outerEnumRawValue = utf8JsonReader.GetString();
764-
if (outerEnumRawValue != null)
765-
outerEnum = new Option<OuterEnum?>(OuterEnumValueConverter.FromStringOrDefault(outerEnumRawValue));
764+
outerEnum = new Option<OuterEnum?>(outerEnumRawValue == null ? null : OuterEnumValueConverter.FromStringOrDefault(outerEnumRawValue));
766765
break;
767766
case "outerEnumDefaultValue":
768767
string? outerEnumDefaultValueRawValue = utf8JsonReader.GetString();

0 commit comments

Comments
 (0)