Fix C# codegen for runtime schema unions - #2656
Conversation
Extract single-variant union handling from #2590 and support referenced enum and nested unions exposed by CLI 1.0.84-6. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Use the existing STJ hierarchy for tagged unions regardless of variant count. Cover one-to-two-variant compatibility and preserve existing nullable reference output. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
There was a problem hiding this comment.
Copilot review overview
🟢 Approval recommended
The implementation addresses both reported generator failures with comprehensive targeted coverage and no identified regressions.
Review tier: Balanced
Findings: None
What changed in this PR
Fixes C# RPC code generation for runtime schema unions that blocked the Copilot CLI update workflow.
Changes:
- Preserves polymorphic types for single-variant
anyOf/oneOfschemas. - Adds discriminator-aware converters for referenced enums and nested unions.
- Adds regression tests covering generated API stability and serialization metadata.
| File | Description |
|---|---|
scripts/codegen/csharp.ts |
Extends C# union detection, matching, and generation. |
nodejs/test/csharp-codegen.test.ts |
Adds focused C# codegen regression coverage. |
💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.
SDK Consistency ReviewReviewed PR #2656 against the authoritative file list (
Findings: No cross-SDK consistency issues. This PR is scoped entirely to the internal C#/.NET code generator ( No generated output files (e.g. No inline review comments are being added since there are no consistency gaps to flag.
|
Summary
Fix the C# codegenerator failures blocking the Copilot CLI 1.0.84-6 update action, without updating the runtime pin or committing generated SDK changes.
anyOf/oneOffailure identified in feat: Add typed structured outputs for Node and .NET #2590. The action originally failed onResponseFormat, a namedanyOfcontaining just one object. Generate the existing STJ polymorphic hierarchy even for one variant, rather than unwrapping it into a plain class.ProtocolSystemMessageConfiguses referenced enum discriminators, and its section overrides contain nested unions. Reuse the existing typed JSON-union converter, matching discriminator values before deserialization so an omitted append-mode discriminator cannot swallow replace/customize variants.The broader structured-output APIs from #2590 are deliberately excluded. Its singleton-unwrapping approach is not retained: changing from a plain class to a hierarchy when a second variant arrives would break consumers. The additional referenced-enum/nested-union handling is also necessary to get past the next generation failure.
Examples: problematic schemas and fixed output
Single-variant union (the original failure)
The runtime's
ResponseFormatdefinition contains ananyOfwith only one object. Schema excerpt, with descriptions omitted:{ "title": "ResponseFormat", "anyOf": [ { "type": "object", "properties": { "jsonSchema": { "$ref": "#/definitions/JsonSchemaResponseFormat" }, "type": { "type": "string", "const": "json_schema" } }, "required": ["type", "jsonSchema"], "additionalProperties": false } ] }Previously this threw
cannot map schema to an idiomatic C# type (unknown/missing type (propName=ResponseFormat)). With the fix, the generator uses the existing STJ polymorphism path regardless of whether there is one tagged variant or several. It emits the following C# (documentation and experimental attributes omitted):Adding a second variant later adds another subtype and
[JsonDerivedType]registration without changing the existing base orResponseFormatJsonSchemaclass declarations. Regression tests compare the one-variant and two-variant output for bothanyOfandoneOf, with and withoutnull.JsonSchemaResponseFormatis also generated as a typed class; only its explicitly opaqueschemaproperty becomesJsonElement. The union itself does not fall back to untyped JSON.Referenced enum discriminators (the next failure)
After fixing
ResponseFormat, generation reached this union and failed onSystemMessage. Unlike an inlineconstdiscriminator,moderefers to a named enum. Excerpt from the runtime'sdefinitions:{ "ProtocolSystemMessageConfig": { "anyOf": [ { "$ref": "#/definitions/ProtocolSystemMessageAppendConfig" }, { "$ref": "#/definitions/ProtocolSystemMessageReplaceConfig" }, { "$ref": "#/definitions/ProtocolSystemMessageCustomizeConfig" } ] }, "ProtocolSystemMessageAppendConfig": { "type": "object", "properties": { "mode": { "$ref": "#/definitions/ProtocolAppendMode" }, "content": { "type": "string" } }, "additionalProperties": false }, "ProtocolAppendMode": { "type": "string", "enum": ["append"] } }The replace/customize definitions similarly reference
["replace"]/["customize"]enums, but requiremode; append permits it to be omitted.The fix generates a typed union wrapper with constructors and implicit conversions for each variant. Its public value properties are:
The generated converter selects append for an omitted
modeor"append", replace for"replace", and customize for"customize". Unknown or non-string modes throwJsonExceptioninstead of being silently accepted as append. Serialization writes the selected variant directly, without an extra wrapper object. The same handling supports the nested section-override unions.Validation
Used the actual, checksum-verified schemas from CLI 1.0.84-6, not the unreleased runtime used by #2590.
24 targeted C#/session-event/shared-codegen tests pass, including four one-to-two-variant compatibility cases that fail with the original singleton-unwrapping implementation.
A local round-trip test using the SDK's actual source-generated serializer configuration confirms that the single-variant STJ hierarchy serializes one discriminator and deserializes back to
ResponseFormatJsonSchema. The test ran on the installed .NET 10 runtime.The earlier referenced-enum/nested-union converter change also passed 14 local serialization round-trip/rejection cases.
Existing Java generator tests, Node.js typechecks, and targeted lint/format checks pass.
Regenerating C# with the unchanged 1.0.84-5 pin produces zero diff in both
SessionEvents.csandRpc.cs, and leaves the working tree clean. This was also confirmed using the default pinned-schema lookup, with no schema-path override:C# is the only generator changed by this PR; no generated-file update is needed before the runtime bump.
The GitHub Actions workflow itself has not been redispatched. After this fix lands, the runtime update action can be rerun for 1.0.84-6.