feat(csharp): add bodyVariantOverloads option for union request bodies
Why
When an operation's request body is a union, the generated C# method only accepts the union type:
Task<ChatResponse> ChatAsync(ChatRequest request, CancellationToken? cancellationToken = null);
Callers must wrap the value in a factory (ChatRequest.CreateChatModelRequest(model)). Even with unionImplicitConversions, completion and XML docs only show the union type, never the member types.
What changed
New opt-in csharp option bodyVariantOverloads (default false). The name and intent match pythonv2's bodyVariantOverloads; pythonv2 flattens each member's fields into keyword arguments, while C# takes the member value. When an operation's request body is a required union and a method parameter, each union member gets an overload on the SDK class and its interface. The overload takes the member value and forwards to the main method through the member's Create* factory:
public async Task<ChatResponse> ChatAsync(
ChatModelRequest chatModelRequest,
CancellationToken? cancellationToken = null
)
{
if (chatModelRequest == null) throw new ArgumentNullException(nameof(chatModelRequest));
return await ChatAsync(ChatRequest.CreateChatModelRequest(chatModelRequest), cancellationToken);
}
-
The overload keeps the other parameters, their order and their defaults, and repeats the method's XML docs (from the same source as the main method on both the root SDK and sub-SDK interfaces). The
<param>text for the member parameter comes from the member schema's summary or description. For discriminated unions whose factory sets the discriminator, the doc says that the passed instance is modified. -
The member parameter name is the camel-cased member name. If it clashes with another parameter, it gets a numeric suffix (
alpha→alpha2). -
Members are selected with the same rules as
unionImplicitConversions. Skipped:- members whose C# type occurs more than once (such overloads would be ambiguous);
object/dynamic/anymembers, and event-stream, request or response members;- the union itself;
- unions of errors.
-
Members of nested unions get no overload of their own. The nested union type gets one.
-
Optional request bodies get no overloads. The member parameter would be required and would sit among optional parameters (CS1737).
-
Request bodies wrapped in a request object get no overloads: operations that are not flattened, or that exceed
maxMethodParams. -
Overloads are
async, like the main methods: a null reference member throwsArgumentNullExceptionfrom the returned task. -
Turning the option on, or adding a union member later, can make existing call sites ambiguous (CS0121). The option description lists them:
nullordefaultpassed as the body;- an argument that is not exactly a member type but converts to two of them, e.g. an
intwhen the union hasBigIntegerand another numeric member, or a literal0with an enum and an integral member; - a method group without a target type (CS8917).
Skipping such members would require reproducing C#'s better-conversion rules for every numeric pair, so they are documented instead.
Review SDK: bodyVariantOverloads is enabled. The zSDKs/sdk-csharp diff is two ChatAsync overloads on ISDK/SDK.
Shared fixture and test IDs
The first commit prepares tests/specs/fragments/primary/body-variant-overloads.yaml for every target that implements bodyVariantOverloads (pythonv2 today, C# here):
- its operations move from the
requestBodiestag tounions, so they share one sub-SDK; - new operations cover reserved field names, subset variants, disjoint field types, renamed parameters and single-value enum discriminators;
bodyVariantOverloadsDiscriminatedalso takes an array query parameter; - shared test IDs
unions-body-variant-overloads-{discriminated,non-discriminated,request-body,non-body-arguments,renamed-argument}are registered. pythonv2 keeps them in its skip list until #189 records them; - the pythonv2 and cli primary tests follow the tag change.
#189 rebases onto this PR and adds a nullable query parameter to bodyVariantOverloadsDiscriminated, together with the pythonv2 fix for nullable parameters in overloads.
Testing
- Fixture:
tests/specs/fragments/primary/body-variant-overloads.yaml(see above). The C# commit adds four operations:bodyVariantOverloadsParameterNameClash: a path parameter named like a union member, which forces a rename;bodyVariantOverloadsOptional: the discriminated union as an optional body;bodyVariantOverloadsServerUrl: an operation with its ownservers;bodyVariantOverloadsOpenEnum: a union of an open enum and a number.
primary(net8.0, withunionImplicitConversions),secondary(responseFormat: flat) andquaternary(netstandard2.0) enable the option. In primary it adds overloads to 57 union-body operations from the shared spec, including primitive, array, date-time, big-integer and nested-union members. The generated SDK compiles alongside the implicit conversions. Insecondary, operations without a response body returnTask, so their overloadsawaitthe main method withoutreturn.UnionRequestBodyOverloadsAdditionalShouldinprimary(10 tests) records the shared IDsunions-body-variant-overloads-{discriminated,non-discriminated,request-body,non-body-arguments,renamed-argument}and the C#-only IDsunions-body-variant-overloads-{nested-union,open-enum}. It checks:- overload counts on the class and the interface, and that an optional body gets none;
- that a required nullable body, a union whose members all share a C# type, and an
objectmember get no overloads; - the renamed member parameter when it clashes with a path parameter;
- the non-discriminated (associated types) path;
- that a request sent through an overload and through the factory carries the same URL, body and renamed header, and that the passed member is not left with an invalid discriminator;
- the
ArgumentNullExceptionparameter name for a null member, including an open-enum member, raised from the returned task; - an open-enum member sent as its raw value;
- a value-type (
long) member overload, bound through an exact-signature delegate so the test cannot fall back to the main method via implicit conversions; - a nested-union member binding the nested union's overload rather than the outer method;
serverUrlforwarding through an overload, bound the same way.
- Count, forwarding and null-member checks use
bodyVariantOverloadsRenamedParams. UnionRequestBodyOverloadsAdditionalShouldinsecondary(2 tests, no implicit conversions) binds aTask-returning overload through an exact-signature delegate, checks it sends the same request as the factory call, and checks that a null member is raised from the returned task.TARGET=primary make test-csharp: 622 passed / 34 skipped, 0 failed, before the exclusion and secondary tests were added. The added tests pass against the generatedprimary(10/10) andsecondary(2/2) SDKs.secondaryandquaternary: generated SDKs build. Their test suites were not rerun after the fixture move; before it they passed (175 / 35 skipped and 191 / 51 skipped).review: thezSDKs/sdk-csharpdiff is unchanged (twoChatAsyncoverloads).- With the option off, generated code is unchanged.
make check-template-csharpandmake lintpass.
Public-safety check
- This change contains no credentials, customer documents, private repository URLs, private filesystem paths, or unredacted private logs.
- Title, body, comments, and commit messages name no customers or customer-derived identifiers, private paths or trackers, or workflow provenance, and are understandable without private context (
.claude/skills/public-repo-communication/SKILL.md). - Generated fixtures and review SDK changes are public-safe.
- I reviewed
git diff --check.
Summary by cubic
Adds an opt-in bodyVariantOverloads option for C# generation that emits one method overload per union member for operations with a required union request body. Callers can now pass a union variant directly instead of wrapping it in a Create* factory, and the variant types show up in completion and XML docs.
New Features
- Generates overloads on SDK classes and interfaces that forward to the main method via the member's
Create*factory, preserving other parameters, defaults, and docs. - Skips members with duplicate C# types,
object/anymembers, nested-union members, optional request bodies, and non-flattened operations. - Enabling the option can make existing calls ambiguous (CS0121) when passing
null/default, an argument convertible to multiple members, or a method group without a target type.
Bug Fixes
- Open-enum members now null-check their variant parameter, so passing
nullthrowsArgumentNullExceptioninstead of wrapping the value in an empty union that fails during serialization.