Skip to content

Write schemas generated for oneOf types override type discriminator values #3293

@Marsunpaisti

Description

@Marsunpaisti

Description

When @hey-api/openapi-ts generates TypeScript types from an OpenAPI spec containing polymorphic types with readOnly properties, it creates "Writable" variants with incorrect typeDiscriminator values.

The generated types use a PascalCase discriminator derived from the type name (e.g., 'DogPayloadWritable') instead of preserving the discriminator value from the OpenAPI spec (e.g., 'dog').

This causes deserialization failures in the backend that still expects the original type discriminators it has defined.

Interestingly enough, this issue wasn't present in @hey-api/openapi-ts 0.84.4, which I suppose might have been due to writable types not being generated properly with polymorphic types so this bug never surfaced itself because of that.

Reproducible example or configuration

Environment

  • @hey-api/openapi-ts: v0.90.10
  • Backend: ASP.NET Core with System.Text.Json
  • OpenAPI generator: Swashbuckle

Reproduction

.NET models

[JsonPolymorphic(TypeDiscriminatorPropertyName = "typeDiscriminator")]
[JsonDerivedType(typeof(DogPayload), "dog")]
[JsonDerivedType(typeof(CatPayload), "cat")]
public abstract record AnimalPayload;

public record DogPayload : AnimalPayload
{
    public required string Breed { get; init; }
    public required bool CanFetch { get; init; }

    /// Computed property - marked as readOnly in OpenAPI schema.
    /// This triggers hey-api to generate a Writable variant.
    public string DisplayBreed => $"{Breed} (Dog)";
}

public record CatPayload : AnimalPayload
{
    public required string Breed { get; init; }
    public required int LivesRemaining { get; init; }

    /// Computed property - marked as readOnly in OpenAPI schema.
    /// This triggers hey-api to generate a Writable variant.
    public string DisplayBreed => $"{Breed} (Cat)";
}

public record CreatePetRequest
{
    public required string Name { get; init; }
    public required AnimalPayload Animal { get; init; }
}

Generated TS Read types (correct)

export type DogPayload = Omit<AnimalPayload, 'typeDiscriminator'> & {
  breed: string
  canFetch: boolean
  readonly displayBreed: string
  typeDiscriminator: 'dog'  // Correct - matches OpenAPI spec
}

export type CatPayload = Omit<AnimalPayload, 'typeDiscriminator'> & {
  breed: string
  livesRemaining: number
  readonly displayBreed: string
  typeDiscriminator: 'cat'  // Correct - matches OpenAPI spec
}

Generated TS Writable types (incorrect):

export type DogPayloadWritable = Omit<AnimalPayload, 'typeDiscriminator'> & {
  breed: string
  canFetch: boolean
  typeDiscriminator: 'DogPayloadWritable'  // should be 'dog'
}

export type CatPayloadWritable = Omit<AnimalPayload, 'typeDiscriminator'> & {
  breed: string
  livesRemaining: number
  typeDiscriminator: 'CatPayloadWritable'  // should be 'cat'
}

export type CreatePetRequestWritable = {
  name: string
  animal: DogPayloadWritable | CatPayloadWritable  // Uses wrong discriminators
}

OpenAPI specification (optional)

OpenAPI Schema (Backend)

components:
  schemas:
    AnimalPayload:
      type: object
      required: [typeDiscriminator]
      properties:
        typeDiscriminator:
          type: string
      discriminator:
        propertyName: typeDiscriminator
        mapping:
          dog: '#/components/schemas/DogPayload'
          cat: '#/components/schemas/CatPayload'

    DogPayload:
      allOf:
        - $ref: '#/components/schemas/AnimalPayload'
        - type: object
          required: [breed, canFetch, displayBreed]
          properties:
            breed:
              type: string
            canFetch:
              type: boolean
            displayBreed:
              type: string
              readOnly: true  # <-- This triggers Writable variant generation

    CatPayload:
      allOf:
        - $ref: '#/components/schemas/AnimalPayload'
        - type: object
          required: [breed, livesRemaining, displayBreed]
          properties:
            breed:
              type: string
            livesRemaining:
              type: integer
            displayBreed:
              type: string
              readOnly: true  # <-- This triggers Writable variant generation

    CreatePetRequest:
      type: object
      required: [name, animal]
      properties:
        name:
          type: string
        animal:
          oneOf:
            - $ref: '#/components/schemas/DogPayload'
            - $ref: '#/components/schemas/CatPayload'

System information (optional)

No response

Metadata

Metadata

Labels

bug 🔥Broken or incorrect behavior.

Type

No type

Projects

No projects

Milestone

No milestone

Relationships

None yet

Development

No branches or pull requests

Issue actions