Skip to content

Fix C# codegen for runtime schema unions - #2656

Merged
SteveSandersonMS merged 2 commits into
mainfrom
fix/csharp-codegen-single-variant-unions
Sep 14, 2026
Merged

SteveSandersonMS merged 2 commits into
mainfrom
fix/csharp-codegen-single-variant-unions

Conversation

@SteveSandersonMS

@SteveSandersonMS SteveSandersonMS commented Sep 14, 2026

Copy link
Copy Markdown
Contributor

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.

  • Fix the single-variant anyOf/oneOf failure identified in feat: Add typed structured outputs for Node and .NET #2590. The action originally failed on ResponseFormat, a named anyOf containing just one object. Generate the existing STJ polymorphic hierarchy even for one variant, rather than unwrapping it into a plain class.
  • Handle the next failure exposed by that fix: ProtocolSystemMessageConfig uses 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.
  • Add regression coverage to the existing Node.js codegen test suite, including one-to-two-variant API compatibility, required/optional properties, nullable unions, named types, nested unions, and serializer registrations.

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 ResponseFormat definition contains an anyOf with 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):

[JsonPolymorphic(
    TypeDiscriminatorPropertyName = "type",
    UnknownDerivedTypeHandling = JsonUnknownDerivedTypeHandling.FallBackToBaseType)]
[JsonDerivedType(typeof(ResponseFormatJsonSchema), "json_schema")]
public partial class ResponseFormat
{
    [JsonPropertyName("type")]
    public virtual string Type { get; set; } = string.Empty;
}

public partial class ResponseFormatJsonSchema : ResponseFormat
{
    [JsonIgnore]
    public override string Type => "json_schema";

    [JsonPropertyName("jsonSchema")]
    public required JsonSchemaResponseFormat JsonSchema { get; set; }
}

Adding a second variant later adds another subtype and [JsonDerivedType] registration without changing the existing base or ResponseFormatJsonSchema class declarations. Regression tests compare the one-variant and two-variant output for both anyOf and oneOf, with and without null.

JsonSchemaResponseFormat is also generated as a typed class; only its explicitly opaque schema property becomes JsonElement. 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 on SystemMessage. Unlike an inline const discriminator, mode refers to a named enum. Excerpt from the runtime's definitions:

{
  "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 require mode; 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:

public sealed partial class ProtocolSystemMessageConfig
{
    public ProtocolSystemMessageAppendConfig? ProtocolSystemMessageAppendConfig { get; }
    public ProtocolSystemMessageReplaceConfig? ProtocolSystemMessageReplaceConfig { get; }
    public ProtocolSystemMessageCustomizeConfig? ProtocolSystemMessageCustomizeConfig { get; }

    // Constructors, implicit conversions, and the generated JSON converter omitted.
}

The generated converter selects append for an omitted mode or "append", replace for "replace", and customize for "customize". Unknown or non-string modes throw JsonException instead 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.

Generator Result with 1.0.84-6 schemas
C# Both generators succeed after this fix; generated SDK builds with zero warnings/errors
TypeScript Generation and SDK typecheck succeed without generator changes
Python Generation succeeds without generator changes
Go Generation succeeds without generator changes
Rust Generation and rustfmt succeed without generator changes
Java Generation succeeds without generator changes
  • 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.cs and Rpc.cs, and leaves the working tree clean. This was also confirmed using the default pinned-schema lookup, with no schema-path override:

    cd scripts/codegen
    npm run generate:csharp
    cd ../..
    git diff --exit-code -- dotnet/src/Generated
    git status --short

    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.

SteveSandersonMS and others added 2 commits September 14, 2026 19:23
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>
@SteveSandersonMS
SteveSandersonMS marked this pull request as ready for review September 14, 2026 19:55
@SteveSandersonMS
SteveSandersonMS requested a review from a team as a code owner September 14, 2026 19:55
Copilot AI balanced review requested due to automatic review settings September 14, 2026 19:55
@SteveSandersonMS
SteveSandersonMS merged commit 8d1545d into main Sep 14, 2026
43 of 53 checks passed
@SteveSandersonMS
SteveSandersonMS deleted the fix/csharp-codegen-single-variant-unions branch September 14, 2026 19:58

Copilot AI left a comment

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.

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/oneOf schemas.
  • 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.

@github-actions

Copy link
Copy Markdown
Contributor

SDK Consistency Review

Reviewed PR #2656 against the authoritative file list (get_files) and diff (get_diff):

  • scripts/codegen/csharp.ts (modified)
  • nodejs/test/csharp-codegen.test.ts (added)

Findings: No cross-SDK consistency issues.

This PR is scoped entirely to the internal C#/.NET code generator (scripts/codegen/csharp.ts), which fixes how discriminated JSON unions are matched for RPC-level types (adding a matchExpression/getRpcUnionMatchExpression so an optional discriminator property doesn't incorrectly swallow another union variant), and improves handling of oneOf/anyOf union resolution and $ref title propagation. It also exports generateRpcCode so the new unit test (nodejs/test/csharp-codegen.test.ts) can exercise the generator directly.

No generated output files (e.g. dotnet/src/generated) were regenerated in this PR, and no public SDK API surface changed in any language — this is purely an internal fix/improvement to the C#-specific code-generation tooling, analogous to the language-specific handling of anyOf/oneOf already present independently in python.ts, go.ts, rust.ts, and typescript.ts. Since each generator implements its own union-resolution logic tailored to its target language's type system, this fix does not need to be mirrored elsewhere, and the addition of a matching unit test keeps the change well-covered.

No inline review comments are being added since there are no consistency gaps to flag.

Generated by SDK Consistency Review Agent for #2656 · copilot · sonnet50 · 41.4 AIC · ⌖ 12.1 AIC · ⊞ 8.3K ·

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants