Skip to content

Multi-value nullable unions emit an object variant for an explicit null-only branch #414

Closed

Description

@HavenDV

Regression

OpenAI's 3.1 schema replaces a legacy nullable: true on a two-value union with a third type: null branch. AutoSDK generates a catch-all object as an additional CLR union variant instead of preserving the nullable union over the two non-null variants.

Seen in tryAGI/OpenAI#416 (head eaefd98ae2c87a719658472ab5a671921f849ae9), for RealtimeSessionCreateRequestGA.Tracing, its Realtime projection, CLI command and serializer contexts. All 11 changed files were inspected. The PR has no package/workflow changes; normal build CI succeeds, so compilation alone does not establish nullability/schema fidelity. The existing two-variant collapse from #390 does not cover this shape.

Minimal reproduction

openapi: 3.1.0
info: {title: Null Union Reproduction, version: 1.0.0}
paths: {}
components:
  schemas:
    Request:
      type: object
      properties:
        tracing:
          oneOf:
            - type: string
              enum: [auto]
            - type: object
              properties:
                workflow_name: {type: string}
            - type: 'null'

Generate with autosdk generate openapi.yaml --namespace NullUnionRepro --output Generated.

Confirmed with CLI 0.35.0-preview.2.67+83957f817c7b8e3fb764c3809111911ac810d84b; the current organization regeneration also emits the same shape in OpenAI #416. The current generator's IsNullableUnion helper requires exactly two variants.

Observed generated property:

OneOf<RequestTracingEnum?, RequestTracingEnum2, object>? Tracing

Expected: a nullable union of the enum and the typed configuration, with no catch-all object branch introduced by the null-only schema. The additional generic argument also changes the public request/method signature for an equivalent wire schema.

Required fix and validation

  • Exclude exact null-only branches from multi-value oneOf and anyOf CLR variant lists while retaining outer nullability.
  • Preserve actual unconstrained object alternatives, named/reference unions, branch naming/order and multiple concrete-value variants; do not collapse a true multi-value union to its first branch.
  • Cover legacy nullable versus explicit null equivalence, two and three-plus concrete variants, enum/object/reference choices, null-only edge cases, round trips and invalid primitive inputs.
  • Validate AutoSDK units/snapshots and compiled serializer/NativeAOT behavior, regenerate OpenAI from its spec inputs, and wait for Publish/trimming before calling the regression fixed. Generated files must not be edited by hand.

Default reproduction is local and key-free; no provider call is necessary.

Compiled local runtime evidence

The generated minimal reproduction compiles and runs on .NET10 with its source-generated serializer context and no external packages. Observations:

{"tracing":true} => ArgumentOutOfRangeException
{"tracing":"unexpected"} => introduced_object=True
{"tracing":null} => introduced_object=False
{"tracing":"auto"} => introduced_object=False
{"tracing":{"workflow_name":"test"}} => introduced_object=False

The invalid string taking the newly introduced object branch confirms the schema-widening behavior. The boolean exception is a separate existing enum-converter fallback/error-shape concern, not a successful invalid-input round trip. No provider request was made.

OpenAI#416 was merged by the existing auto-merge workflow as 5bb64663a6d446210fce811628d5e29ca2cfd76f at2026-10-04T03:26:48Z while an attempted draft conversion raced it. The draft conversion command succeeded, but the final PR state is merged; this is not a prevention guarantee. PR Test succeeded; Publish is monitored separately. This issue remains open until the generator fix and regenerated SDK validation are complete.

Activity

  1. HavenDV commented on Oct 4, 2026

    @HavenDV
    CollaboratorAuthor

    Resolved on main by 1ab99a2, with shard-compatible integration test names in 40bbf26 and regenerated corpus snapshots in 2d9e3ab.

    Explicit null-only branches are excluded from multi-value anyOf/oneOf alternatives while outer nullability and concrete alternative ordering are retained. Type/helper arity now comes from the retained alternatives. Actual unconstrained object alternatives remain present. Single-concrete nullable wrappers retain their existing mapping; all-null unions are not collapsed to an empty union.

    Validation:

    • 19 focused unit scenarios, including named unions, nullable array items, variant order and unconstrained object preservation.
    • Compiled source-generated JSON round trips for anyOf/oneOf with reflection serialization disabled, plus native compilation and execution of both fixtures.
    • Final main Publish run 37178921876 succeeded: 37 successful jobs, 2 conditional skipped jobs; its build/test job includes the unit and snapshot corpus. A separate local snapshot run passed all 295 cases without auto-acceptance enabled.
    • OpenAI regenerated in e91af534c99870bd90c8bc1c037dbd2fc106639a; core and wire tracing round trips pass. OpenAI main 65ad1a5e73aaeac0196efb17bb652c03c2f9fdc8 consumes CSharpToJsonSchema 3.10.6; its Publish run 37179302365 succeeded.

    This change does not add strict closed-enum/union validation to legacy converters. Unknown-value behavior remains a separate concern. A separately launched fully rooted OpenAI native compilation is still running, so it is not being reported as a successful SDK native execution.

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

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions