[csharp][generichost] Skip unset optional string enums and write null enums
Fixes #25267
WriteProperties writes inline string enums without the Option.IsSet check that other optional properties get, and it reads .Value on nullable enums without checking for null first. Serializing a model throws InvalidOperationException: Nullable object must have a value when:
- an optional enum is not set (#25267)
- an optional nullable enum is not set, or is set to
null - a required nullable enum is
null
Since #23912 the reader turns a JSON null into a set Option holding null, so the last two cases also break round-tripping a response such as {"kind": null}.
The fix wraps optional enums in if (...Option.IsSet), and for nullable enums writes null when the value is null instead of calling .Value. Required non-nullable enums are unchanged. Numeric enums and enums that reference a schema already go through other branches that handle this, so they are unchanged too.
Spec
openapi: 3.0.3
info: { title: repro, version: 1.0.0 }
paths: {}
components:
schemas:
Pet:
type: object
required: [name, kind]
properties:
name: { type: string }
status: { type: string, enum: [available, sold] }
mode: { type: string, nullable: true, enum: [auto, manual] }
kind: { type: string, nullable: true, enum: [cat, dog] }
Before
var statusRawValue = Pet.StatusEnumToJsonValue(pet.StatusOption.Value!.Value); // throws when unset
writer.WriteString("status", statusRawValue);
var modeRawValue = Pet.ModeEnumToJsonValue(pet.ModeOption.Value!.Value); // throws when unset or null
if (modeRawValue != null)
writer.WriteString("mode", modeRawValue);
else
writer.WriteNull("mode");
var kindRawValue = Pet.KindEnumToJsonValue(pet.Kind!.Value); // throws when null
After
if (pet.StatusOption.IsSet)
{
var statusRawValue = Pet.StatusEnumToJsonValue(pet.StatusOption.Value!.Value);
writer.WriteString("status", statusRawValue);
}
if (pet.ModeOption.IsSet)
{
if (pet.ModeOption.Value == null)
writer.WriteNull("mode");
else
{
var modeRawValue = Pet.ModeEnumToJsonValue(pet.ModeOption.Value!.Value);
writer.WriteString("mode", modeRawValue);
}
}
if (pet.Kind == null)
writer.WriteNull("kind");
else
{
var kindRawValue = Pet.KindEnumToJsonValue(pet.Kind!.Value);
writer.WriteString("kind", kindRawValue);
}
Deserializing and then serializing each of these, with nullableReferenceTypes on and off:
| Input | Before | After |
|---|---|---|
{"name":"Rex","kind":"cat"} | throws | {"name":"Rex","kind":"cat"} |
{"name":"Rex","kind":"cat","status":"sold","mode":null} | throws | {"name":"Rex","status":"sold","mode":null,"kind":"cat"} |
{"name":"Rex","kind":null,"status":"sold","mode":"auto"} | throws | {"name":"Rex","status":"sold","mode":"auto","kind":null} |
| all fields set | same output | same output |
Testing
./mvnw -DskipTests -pl modules/openapi-generator-cli -am package./bin/generate-samples.sh bin/configs/csharp-generichost-*.yaml(50 configs): 91 model files change (EnumArrays,EnumTest,Order,Pet,RequiredClass,Zebra,ZeroBasedEnumClass), +663/−299 lines. Regenerating these configs on master first produced no changes.- Added
CSharpClientCodegenTest#testGenericHostWritesUnsetAndNullInnerStringEnums. It fails without the template change and passes with it.CSharpClientCodegenTest: 24 tests, 0 failures. - Generated a net8.0 client from the spec above and ran the round trips in the table with the .NET 9 SDK.
PR checklist
- Read the contribution guidelines.
- Run the following to build the project and update samples:
(For Windows users, please run the script in WSL)./mvnw clean package || exit ./bin/generate-samples.sh ./bin/configs/*.yaml || exit ./bin/utils/export_docs_generators.sh || exit
Commit all changed files.
This is important, as CI jobs will verify all generator outputs of your HEAD commit as it would merge with master.
These must match the expectations made by your contribution.
You may regenerate an individual generator by passing the relevant config(s) as an argument to the script, for example./bin/generate-samples.sh bin/configs/java*.
IMPORTANT: Do NOT purge/delete any folders/files (e.g. tests) when regenerating the samples as manually written tests may be removed.
(Ran the build and regenerated thecsharp-generichost-*configs, the only ones that use this template; not all configs.) - If your PR is targeting a particular programming language, @mention the technical committee members, so they are more likely to review the pull request.
@mandrean @shibayan @Blackclaws @lucamazzanti @devhl-labs
Summary by cubic
Fixes C# generichost serialization so models with inline string enums no longer throw Nullable object must have a value.
WriteProperties in the JsonConverter.mustache template was missing the Option.IsSet check that other optional properties get, and it read .Value on nullable enums without checking for null first. Optional string enums that are unset are now skipped, and null nullable enums are written as JSON null. Previously, serializing threw when an optional enum was unset, an optional nullable enum was unset or explicitly null, or a required nullable enum was null.
Bug Fixes
- Wraps optional string enum serialization in an
Option.IsSetcheck. - Writes
nullfor null nullable enums instead of reading.Value. - Adds a codegen test that asserts set enums are serialized as well, and regenerates the
csharp-generichost-*samples.