Skip to content

feat: Handle oneOf/anyOf with inline object variants - #150

Open
nikcio wants to merge 11 commits into
mainfrom
feature/oneof-anyof-inline-variants
Open

feat: Handle oneOf/anyOf with inline object variants#150
nikcio wants to merge 11 commits into
mainfrom
feature/oneof-anyof-inline-variants

Conversation

@nikcio

@nikcio nikcio commented Aug 26, 2026

Copy link
Copy Markdown
Contributor

Changes

Previously, oneOf/anyOf unions with inline (non-ref) object variants degraded to object, losing type information. Now, inline object variants are hoisted to named records, and the union is emitted as an abstract record with [JsonDerivedType] attributes referencing both ref variants and hoisted inline objects.

Before

value:
  oneOf:
    - $ref: A                    # ref variant
    - type: object                # inline variant → degraded to object
        properties:
          x: { type: string }

Property typed as object? — no type safety, no JSON polymorphism.

After

Property typed as MyRecordValue? — an abstract record with:

  • [JsonDerivedType(typeof(A), "A")]
  • [JsonDerivedType(typeof(MyRecordValueVariant2), "MyRecordValueVariant2")]

The inline object is hoisted to MyRecordValueVariant2 record.

Implementation

  • Extend IsInlineRefUnion to IsInlineUnion: accepts inline objects alongside ref variants
  • In TryHoistPropertySchema, hoist inline object variants before hoisting the union itself
  • In DiscoverInlineObjects, scan oneOf/anyOf variants for component-level union schemas
  • In EmitSimpleUnion, resolve inline object variants to their hoisted type names
  • Add TypeResolver.GetInlineObjectTypeName for looking up hoisted names

Limitation

Unions with primitive variants (e.g. oneOf: [{type: string}, {type: integer}]) still degrade to object since primitives can't be hoisted to records. This could be addressed in a future PR with a custom JsonConverter.

Testing

All 264 tests pass (260 existing + 4 new). New tests include compilation with TreatWarningsAsErrors.

nikcio added 11 commits August 27, 2026 00:44
Previously, oneOf/anyOf unions with inline (non-$ref$) object variants
degraded to 'object', losing type information. Now, inline object
variants are hoisted to named records, and the union is emitted as an
abstract record with [JsonDerivedType] attributes referencing both
$ref$ variants and hoisted inline objects.

- Extend IsInlineRefUnion to IsInlineUnion: accepts inline objects
  alongside $ref$ variants
- In TryHoistPropertySchema, hoist inline object variants before
  hoisting the union itself
- In DiscoverInlineObjects, scan oneOf/anyOf variants for component-level
  union schemas
- In EmitSimpleUnion, resolve inline object variants to their hoisted
  type names for [JsonDerivedType] attributes
- Add TypeResolver.GetInlineObjectTypeName for looking up hoisted names
- Add 4 tests: mixed ref+inline, all-inline, component-level, compilation
…ples

- Add escalation oneOf (mixed ref + inline object) to showcase-openapi.yaml
- Document the new case in the examples README
- Regenerate all example outputs from merged main
- Extend EmitDiscriminatedUnion to emit [JsonDerivedType] for hoisted
  inline object variants not covered by the discriminator mapping
- Add doc comment noting synthetic discriminator for inline variants
- Fix xUnit1051 warnings: use TestContext.Current.CancellationToken
- Add tests: anyOf with inline, nested inline objects, same-shaped
  inline variants, discriminated oneOf with inline object
- Regenerate all example outputs
…iminated unions

Inline object variants in discriminated unions now use the wire
discriminator value extracted from the variant's discriminator property
(e.g. "dog" from petType: { enum: ["dog"] }) instead of the synthesized
type name. Union variants now inherit from the discriminated union base
type and the discriminator property is skipped on derived types so that
System.TextJson polymorphic deserialization works correctly.

Also fixes orphan hoisted types when a schema has both direct properties
and oneOf/anyOf.
…ission

- Extract discriminator value from discProp.Const (OpenAPI 3.1) in
  TryGetDiscriminatorValue, falling back to discProp.Enum (OpenAPI 3.0)
- Skip discriminator properties of union variants in ResolveInlineEnums
  to avoid emitting orphaned inline enum types
- Merge duplicate <remarks> doc comment blocks in EmitSimpleUnion
…riants, and sequential variant naming

- Guard against inline variant discriminator value overwriting explicit
  mapping entries in EmitDiscriminatedUnion
- Populate _unionVariantBaseTypes for property-level discriminated unions
  so both  and inline variants inherit from the union base type
- Filter null-type variants from anyOf before numbering to produce
  sequential Variant1, Variant2 names without gaps
- Add 4 tests covering all three fixes including a round-trip test
- Regenerate examples
… redundant condition

- Filter null-type variants in both oneOf and anyOf hoisting paths
  (DiscoverInlineObjects and TryHoistPropertySchema) so variant
  numbering stays sequential, consistent with IsInlineUnion.
- Remove redundant inner  in EmitSimpleUnion
  that was always true due to the outer guard.
- Add oneOf equivalent of the anyOf null-variant sequential naming test.
…ulti-union ref variants

Non-discriminated unions with inline object variants now resolve to
object instead of hoisting abstract records that System.Text.Json
cannot deserialize without a discriminator. This restores the
pre-branch behavior for these properties and eliminates ~10% code
bloat in the github-api output.

Discriminated unions with a $ref variant assigned to a different
union base are now skipped in EmitDiscriminatedUnion, preventing
invalid [JsonDerivedType] attributes that would cause runtime
polymorphic configuration errors (C# single inheritance).
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.

1 participant