Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -237,40 +237,28 @@
{{^isMap}}
{{#isInnerEnum}}
{{#vendorExtensions.x-enum-byte}}
if (utf8JsonReader.TokenType == JsonTokenType.Null)
{{#lambda.camelcase_sanitize_param}}{{name}}{{/lambda.camelcase_sanitize_param}} = {{>OptionProperty}}null);
else
{
string {{#lambda.camelcase_sanitize_param}}{{name}}{{/lambda.camelcase_sanitize_param}}RawValue = utf8JsonReader.{{>EnumJsonReaderMethod}}().ToString(System.Globalization.CultureInfo.InvariantCulture);
{{classname}}.{{{datatypeWithEnum}}}? {{#lambda.camelcase_sanitize_param}}{{name}}{{/lambda.camelcase_sanitize_param}}Value = {{classname}}.{{{datatypeWithEnum}}}FromStringOrDefault({{#lambda.camelcase_sanitize_param}}{{name}}{{/lambda.camelcase_sanitize_param}}RawValue);
if ({{#lambda.camelcase_sanitize_param}}{{name}}{{/lambda.camelcase_sanitize_param}}Value == null)
throw new JsonException();
{{#lambda.camelcase_sanitize_param}}{{name}}{{/lambda.camelcase_sanitize_param}} = {{>OptionProperty}}{{#lambda.camelcase_sanitize_param}}{{name}}{{/lambda.camelcase_sanitize_param}}Value);
}
{{>NumericInnerEnumJsonRead}}
{{/vendorExtensions.x-enum-byte}}
{{^vendorExtensions.x-enum-byte}}
{{#isNumeric}}
if (utf8JsonReader.TokenType == JsonTokenType.Null)
{{#lambda.camelcase_sanitize_param}}{{name}}{{/lambda.camelcase_sanitize_param}} = {{>OptionProperty}}null);
else
{
string {{#lambda.camelcase_sanitize_param}}{{name}}{{/lambda.camelcase_sanitize_param}}RawValue = utf8JsonReader.{{>EnumJsonReaderMethod}}().ToString(System.Globalization.CultureInfo.InvariantCulture);
{{classname}}.{{{datatypeWithEnum}}}? {{#lambda.camelcase_sanitize_param}}{{name}}{{/lambda.camelcase_sanitize_param}}Value = {{classname}}.{{{datatypeWithEnum}}}FromStringOrDefault({{#lambda.camelcase_sanitize_param}}{{name}}{{/lambda.camelcase_sanitize_param}}RawValue);
if ({{#lambda.camelcase_sanitize_param}}{{name}}{{/lambda.camelcase_sanitize_param}}Value == null)
throw new JsonException();
{{#lambda.camelcase_sanitize_param}}{{name}}{{/lambda.camelcase_sanitize_param}} = {{>OptionProperty}}{{#lambda.camelcase_sanitize_param}}{{name}}{{/lambda.camelcase_sanitize_param}}Value);
}
{{>NumericInnerEnumJsonRead}}
{{/isNumeric}}
{{^isNumeric}}
string{{nrt?}} {{#lambda.camelcase_sanitize_param}}{{name}}{{/lambda.camelcase_sanitize_param}}RawValue = utf8JsonReader.GetString();
{{! 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. }}
{{#isNullable}}
{{#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));
if ({{#lambda.camelcase_sanitize_param}}{{name}}{{/lambda.camelcase_sanitize_param}}RawValue == null)
{{#lambda.camelcase_sanitize_param}}{{name}}{{/lambda.camelcase_sanitize_param}} = {{>OptionProperty}}null);
else
{{/isNullable}}
{{^isNullable}}
if ({{#lambda.camelcase_sanitize_param}}{{name}}{{/lambda.camelcase_sanitize_param}}RawValue != null)
{{#lambda.camelcase_sanitize_param}}{{name}}{{/lambda.camelcase_sanitize_param}} = {{>OptionProperty}}{{classname}}.{{{datatypeWithEnum}}}FromStringOrDefault({{#lambda.camelcase_sanitize_param}}{{name}}{{/lambda.camelcase_sanitize_param}}RawValue));
{{/isNullable}}
{
{{classname}}.{{{datatypeWithEnum}}}? {{#lambda.camelcase_sanitize_param}}{{name}}{{/lambda.camelcase_sanitize_param}}Value = {{classname}}.{{{datatypeWithEnum}}}FromStringOrDefault({{#lambda.camelcase_sanitize_param}}{{name}}{{/lambda.camelcase_sanitize_param}}RawValue);
if ({{#lambda.camelcase_sanitize_param}}{{name}}{{/lambda.camelcase_sanitize_param}}Value == null)
throw new JsonException();

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: Unknown nullable string enum values now raise JsonException instead of deserializing as null, regressing the existing inner-enum behavior and making this path inconsistent with the nullable handling described for numeric enums. The null result should be assigned to the optional property rather than treated as an exception for nullable fields.

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/resources/csharp/libraries/generichost/JsonConverter.mustache, line 259:

<comment>Unknown nullable string enum values now raise `JsonException` instead of deserializing as null, regressing the existing inner-enum behavior and making this path inconsistent with the nullable handling described for numeric enums. The null result should be assigned to the optional property rather than treated as an exception for nullable fields.</comment>

<file context>
@@ -245,14 +245,20 @@
+                            {
+                                {{classname}}.{{{datatypeWithEnum}}}? {{#lambda.camelcase_sanitize_param}}{{name}}{{/lambda.camelcase_sanitize_param}}Value = {{classname}}.{{{datatypeWithEnum}}}FromStringOrDefault({{#lambda.camelcase_sanitize_param}}{{name}}{{/lambda.camelcase_sanitize_param}}RawValue);
+                                if ({{#lambda.camelcase_sanitize_param}}{{name}}{{/lambda.camelcase_sanitize_param}}Value == null)
+                                    throw new JsonException();
+                                {{#lambda.camelcase_sanitize_param}}{{name}}{{/lambda.camelcase_sanitize_param}} = {{>OptionProperty}}{{#lambda.camelcase_sanitize_param}}{{name}}{{/lambda.camelcase_sanitize_param}}Value);
+                            }
</file context>

{{#lambda.camelcase_sanitize_param}}{{name}}{{/lambda.camelcase_sanitize_param}} = {{>OptionProperty}}{{#lambda.camelcase_sanitize_param}}{{name}}{{/lambda.camelcase_sanitize_param}}Value);
}
{{/isNumeric}}
{{/vendorExtensions.x-enum-byte}}
{{/isInnerEnum}}
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
if (utf8JsonReader.TokenType == JsonTokenType.Null)
{{#lambda.camelcase_sanitize_param}}{{name}}{{/lambda.camelcase_sanitize_param}} = {{>OptionProperty}}null);
Comment thread
cubic-dev-ai[bot] marked this conversation as resolved.
else
{
string {{#lambda.camelcase_sanitize_param}}{{name}}{{/lambda.camelcase_sanitize_param}}RawValue = utf8JsonReader.{{>EnumJsonReaderMethod}}().ToString(System.Globalization.CultureInfo.InvariantCulture);
{{classname}}.{{{datatypeWithEnum}}}? {{#lambda.camelcase_sanitize_param}}{{name}}{{/lambda.camelcase_sanitize_param}}Value = {{classname}}.{{{datatypeWithEnum}}}FromStringOrDefault({{#lambda.camelcase_sanitize_param}}{{name}}{{/lambda.camelcase_sanitize_param}}RawValue);
if ({{#lambda.camelcase_sanitize_param}}{{name}}{{/lambda.camelcase_sanitize_param}}Value == 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: The throw new JsonException() for unrecognized numeric enum values contradicts the PR's stated behavior. For nullable enums, unknown values should be deserialized as null (not throw). For non-nullable enums, unknown values should leave the option unset. Either restore the nullable-aware branching from the old template, or update the PR description to reflect the breaking change.

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/resources/csharp/libraries/generichost/NumericInnerEnumJsonRead.mustache, line 7:

<comment>The throw new JsonException() for unrecognized numeric enum values contradicts the PR's stated behavior. For nullable enums, unknown values should be deserialized as null (not throw). For non-nullable enums, unknown values should leave the option unset. Either restore the nullable-aware branching from the old template, or update the PR description to reflect the breaking change.</comment>

<file context>
@@ -1,11 +1,10 @@
-    if ({{#lambda.camelcase_sanitize_param}}{{name}}{{/lambda.camelcase_sanitize_param}}Value != null)
-        {{#lambda.camelcase_sanitize_param}}{{name}}{{/lambda.camelcase_sanitize_param}} = {{>OptionProperty}}{{#lambda.camelcase_sanitize_param}}{{name}}{{/lambda.camelcase_sanitize_param}}Value);{{/isNullable}}
+    {{classname}}.{{{datatypeWithEnum}}}? {{#lambda.camelcase_sanitize_param}}{{name}}{{/lambda.camelcase_sanitize_param}}Value = {{classname}}.{{{datatypeWithEnum}}}FromStringOrDefault({{#lambda.camelcase_sanitize_param}}{{name}}{{/lambda.camelcase_sanitize_param}}RawValue);
+    if ({{#lambda.camelcase_sanitize_param}}{{name}}{{/lambda.camelcase_sanitize_param}}Value == null)
+        throw new JsonException();
+    {{#lambda.camelcase_sanitize_param}}{{name}}{{/lambda.camelcase_sanitize_param}} = {{>OptionProperty}}{{#lambda.camelcase_sanitize_param}}{{name}}{{/lambda.camelcase_sanitize_param}}Value);
</file context>

throw new JsonException();
{{#lambda.camelcase_sanitize_param}}{{name}}{{/lambda.camelcase_sanitize_param}} = {{>OptionProperty}}{{#lambda.camelcase_sanitize_param}}{{name}}{{/lambda.camelcase_sanitize_param}}Value);
}
Original file line number Diff line number Diff line change
Expand Up @@ -41,11 +41,10 @@
public class CSharpClientCodegenTest {

@Test
public void testGenericHostNullableEnumDeserializesPresentNull() throws IOException {
// For a required + nullable enum, the generated generichost JsonConverter must assign
// the backing Option even when the JSON value is null; otherwise the required-property
// check rejects a present-but-null value with "Property is required". Non-nullable enums
// keep the original non-null guard, so their generated output is unchanged.
public void testGenericHostInnerStringEnumUnknownHandlingPreservesNullBehavior() throws IOException {
// Unknown enum values must throw for both nullable and non-nullable properties.
// Preserve the existing null-token behavior: nullable properties record a present
// null, while non-nullable string properties retain their existing null guard.
File output = Files.createTempDirectory("test").toFile().getCanonicalFile();
output.deleteOnExit();
final OpenAPI openAPI = TestUtils.parseFlattenSpec(
Expand All @@ -65,12 +64,23 @@ public void testGenericHostNullableEnumDeserializesPresentNull() throws IOExcept
File requiredClass = files.get(Paths.get(output.getAbsolutePath(),
"src", "Org.OpenAPITools", "Model", "RequiredClass.cs").toString());
assertNotNull(requiredClass);
// required + nullable enum: a null raw value still sets the Option (mapped to null)
assertFileContains(requiredClass.toPath(),
"requiredNullableEnumString = new Option<RequiredClass.RequiredNullableEnumStringEnum?>(requiredNullableEnumStringRawValue == null ? null :");
// required + non-nullable enum: keeps the original guard (unchanged behavior)
assertFileContains(requiredClass.toPath(),
"if (requiredNotnullableEnumStringRawValue != null)");
String requiredClassContents = Files.readString(requiredClass.toPath());
assertThat(requiredClassContents)
.containsPattern(
"if \\(requiredNullableEnumStringRawValue == null\\)\\s+" +
"requiredNullableEnumString = new Option<RequiredClass.RequiredNullableEnumStringEnum\\?>\\(null\\);")
.containsPattern(
"if \\(requiredNotnullableEnumStringRawValue != null\\)\\s+\\{");
assertThat(requiredClassContents).contains(
"if (requiredNullableEnumStringValue == null)\n" +
" throw new JsonException();",
"if (requiredNotnullableEnumStringValue == null)\n" +
" throw new JsonException();",
"if (!requiredNullableEnumString.IsSet)\n" +
" throw new ArgumentException(\"Property is required for class RequiredClass.\", nameof(requiredNullableEnumString));",
"if (!requiredNotnullableEnumString.IsSet)\n" +
" throw new ArgumentException(\"Property is required for class RequiredClass.\", nameof(requiredNotnullableEnumString));"
);
}

@Test
Expand Down Expand Up @@ -331,6 +341,67 @@ public void testNumericEnumJsonConverterUsesNumericOperations() throws IOExcepti
": byte"
);

File modelWithEnumPropertiesFile = files.get(Paths
.get(output.getAbsolutePath(), "src", "Org.OpenAPITools", "Model", "ModelWithEnumProperties.cs")
.toString()
);
assertNotNull(modelWithEnumPropertiesFile, "Could not find file for model: ModelWithEnumProperties");
String modelWithEnumProperties = Files.readString(modelWithEnumPropertiesFile.toPath());

// Unknown non-null enum values are rejected consistently for nullable and non-nullable
// string, numeric, and byte-backed inner enums.
assertThat(modelWithEnumProperties).contains(
"if (inlineIntEnumValue == null)\n" +
" throw new JsonException();",
"if (nullableInlineIntEnumValue == null)\n" +
" throw new JsonException();",
"if (inlineStringEnumValue == null)\n" +
" throw new JsonException();",
"if (nullableInlineStringEnumValue == null)\n" +
" throw new JsonException();",
"if (inlineByteEnumValue == null)\n" +
" throw new JsonException();",
"if (nullableInlineByteEnumValue == null)\n" +
" throw new JsonException();",
"inlineIntEnum = new Option<ModelWithEnumProperties.InlineIntEnumEnum?>(inlineIntEnumValue);",
"ModelWithEnumProperties.NullableInlineIntEnumEnum? nullableInlineIntEnumValue = " +
"ModelWithEnumProperties.NullableInlineIntEnumEnumFromStringOrDefault(nullableInlineIntEnumRawValue);",
"nullableInlineIntEnum = new Option<ModelWithEnumProperties.NullableInlineIntEnumEnum?>(" +
"nullableInlineIntEnumValue);",
"inlineStringEnum = new Option<ModelWithEnumProperties.InlineStringEnumEnum?>(inlineStringEnumValue);",
"nullableInlineStringEnum = new Option<ModelWithEnumProperties.NullableInlineStringEnumEnum?>(" +
"nullableInlineStringEnumValue);"
);

// Numeric and byte-backed enums record an explicit JSON null as present so existing
// nullability validation can distinguish it from a missing property.
assertThat(modelWithEnumProperties)
.containsPattern(
"if \\(utf8JsonReader.TokenType == JsonTokenType.Null\\)\\s+" +
"inlineIntEnum = new Option<ModelWithEnumProperties.InlineIntEnumEnum\\?>\\(null\\);")
.containsPattern(
"if \\(utf8JsonReader.TokenType == JsonTokenType.Null\\)\\s+" +
"nullableInlineIntEnum = new Option<ModelWithEnumProperties.NullableInlineIntEnumEnum\\?>\\(null\\);")
.containsPattern(
"if \\(inlineStringEnumRawValue != null\\)\\s+\\{")
.containsPattern(
"if \\(nullableInlineStringEnumRawValue == null\\)\\s+" +
"nullableInlineStringEnum = new Option<ModelWithEnumProperties.NullableInlineStringEnumEnum\\?>\\(null\\);")
.containsPattern(
"if \\(utf8JsonReader.TokenType == JsonTokenType.Null\\)\\s+" +
"inlineByteEnum = new Option<ModelWithEnumProperties.InlineByteEnumEnum\\?>\\(null\\);")
.containsPattern(
"if \\(utf8JsonReader.TokenType == JsonTokenType.Null\\)\\s+" +
"nullableInlineByteEnum = new Option<ModelWithEnumProperties.NullableInlineByteEnumEnum\\?>\\(null\\);");

// Required numeric checks distinguish a missing property from a present null.
assertThat(modelWithEnumProperties).contains(
"if (!requiredInlineIntEnum.IsSet)\n" +
" throw new ArgumentException(\"Property is required for class ModelWithEnumProperties.\", nameof(requiredInlineIntEnum));",
"if (requiredInlineIntEnum.IsSet && requiredInlineIntEnum.Value == null)\n" +
" throw new ArgumentNullException(nameof(requiredInlineIntEnum), \"Property is not nullable for class ModelWithEnumProperties.\");"
);

// Verify long enum uses int64 reader with validation and actual int64 values
File longEnumFile = files.get(Paths
.get(output.getAbsolutePath(), "src", "Org.OpenAPITools", "Model", "LongEnum.cs")
Expand Down Expand Up @@ -426,6 +497,9 @@ public void testNumericEnumJsonConverterUsesNumericOperations() throws IOExcepti
"inlineLongEnumRawValue = utf8JsonReader.GetInt64().ToString(System.Globalization.CultureInfo.InvariantCulture)",
"inlineDoubleEnumRawValue = utf8JsonReader.GetDouble().ToString(System.Globalization.CultureInfo.InvariantCulture)",
"inlineByteEnumRawValue = utf8JsonReader.GetByte().ToString(System.Globalization.CultureInfo.InvariantCulture)",
"nullableInlineByteEnumRawValue = utf8JsonReader.GetByte().ToString(System.Globalization.CultureInfo.InvariantCulture)",
"inlineStringEnumRawValue = utf8JsonReader.GetString()",
"nullableInlineStringEnumRawValue = utf8JsonReader.GetString()",
"InlineLongEnumEnum : long",
"InlineByteEnumEnum : byte",
"return 1.1d;",
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -72,6 +72,8 @@ components:
- Max
ModelWithEnumProperties:
type: object
required:
- requiredInlineIntEnum
properties:
intEnum:
$ref: '#/components/schemas/IntegerEnum'
Expand All @@ -94,6 +96,42 @@ components:
x-enum-varnames:
- None
- Some
nullableInlineIntEnum:
type: integer
format: int32
nullable: true
enum:
- 0
- 1
x-enum-varnames:
- None
- Some
requiredInlineIntEnum:
type: integer
format: int32
enum:
- 0
- 1
x-enum-varnames:
- None
- Some
inlineStringEnum:
type: string
enum:
- none
- some
x-enum-varnames:
- None
- Some
nullableInlineStringEnum:
type: string
nullable: true
enum:
- none
- some
x-enum-varnames:
- None
- Some
inlineLongEnum:
type: integer
format: int64
Expand Down Expand Up @@ -121,3 +159,13 @@ components:
x-enum-varnames:
- Min
- Max
nullableInlineByteEnum:
type: integer
format: byte
nullable: true
enum:
- 0
- 255
x-enum-varnames:
- Min
- Max
Original file line number Diff line number Diff line change
Expand Up @@ -187,7 +187,12 @@ public override ChildCat Read(ref Utf8JsonReader utf8JsonReader, Type typeToConv
case "pet_type":
string? petTypeRawValue = utf8JsonReader.GetString();
if (petTypeRawValue != null)
petType = new Option<ChildCat.PetTypeEnum?>(ChildCat.PetTypeEnumFromStringOrDefault(petTypeRawValue));
{
ChildCat.PetTypeEnum? petTypeValue = ChildCat.PetTypeEnumFromStringOrDefault(petTypeRawValue);
if (petTypeValue == null)
throw new JsonException();
petType = new Option<ChildCat.PetTypeEnum?>(petTypeValue);
}
break;
default:
break;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -180,7 +180,12 @@ public override CopyActivity Read(ref Utf8JsonReader utf8JsonReader, Type typeTo
case "$schema":
string? schemaRawValue = utf8JsonReader.GetString();
if (schemaRawValue != null)
schema = new Option<CopyActivity.SchemaEnum?>(CopyActivity.SchemaEnumFromStringOrDefault(schemaRawValue));
{
CopyActivity.SchemaEnum? schemaValue = CopyActivity.SchemaEnumFromStringOrDefault(schemaRawValue);
if (schemaValue == null)
throw new JsonException();
schema = new Option<CopyActivity.SchemaEnum?>(schemaValue);
}
break;
default:
break;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -125,7 +125,12 @@ public override Descendant1 Read(ref Utf8JsonReader utf8JsonReader, Type typeToC
case "objectType":
string? objectTypeRawValue = utf8JsonReader.GetString();
if (objectTypeRawValue != null)
objectType = new Option<Descendant1.ObjectTypeEnum?>(Descendant1.ObjectTypeEnumFromStringOrDefault(objectTypeRawValue));
{
Descendant1.ObjectTypeEnum? objectTypeValue = Descendant1.ObjectTypeEnumFromStringOrDefault(objectTypeRawValue);
if (objectTypeValue == null)
throw new JsonException();
objectType = new Option<Descendant1.ObjectTypeEnum?>(objectTypeValue);
}
break;
default:
break;
Expand Down
Loading
Loading