Skip to content
Open
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();
{{#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)
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