Handling oneOf Discriminators in Generated SDKs

Polymorphic responses are where generated SDKs most often fall apart: the API returns one of several object shapes, and without a discriminator the SDK cannot decide which concrete type a payload is. The result is a loose TypeScript union you have to narrow by hand, or a Java Object you have to cast. This guide, part of OpenAPI Generator and the broader SDK Generation & Changelog Automation section, shows how to wire up discriminator.propertyName so generated models deserialize oneOf/anyOf correctly across both TypeScript and Java.

Discriminator selecting the concrete subtype A JSON payload's discriminator field, petType, is read and used to deserialize into either the Cat or Dog subtype. JSON payload petType: "Cat" name, huntsMice Read discriminator propertyName: petType mapping lookup Cat instance huntsMice: true Dog instance packSize: int The discriminator value picks the concrete subtype at deserialization time

Problem & Context

Consider an endpoint that returns a Pet that is either a Cat or a Dog. Modeled as a bare oneOf, the spec is technically valid but tells the generator nothing about how to distinguish the variants at runtime:

# WRONG — no discriminator. The generator cannot pick a subtype.
Pet:
  oneOf:
    - $ref: '#/components/schemas/Cat'
    - $ref: '#/components/schemas/Dog'

What you get downstream:

  • TypeScript (typescript-axios): type Pet = Cat | Dog; — a union with no type guard. You must write if ('huntsMice' in pet) by hand, and any structural overlap between Cat and Dog makes that fragile.
  • Java (java): a deserialization helper that tries each schema until one validates, or falls back to a base type. Ambiguous payloads throw, and overlapping fields silently pick the wrong subtype.

The fix is to give the generator a field it can switch on: a discriminator with a propertyName and an explicit mapping.

Step-by-Step Solution

1. Add a shared discriminator property to every variant

Each subtype must carry the same field, and it must be required. Here petType is the discriminator.

Cat:
  type: object
  required: [petType, name]
  properties:
    petType: { type: string }   # discriminator field — present in every variant
    name: { type: string }
    huntsMice: { type: boolean }
Dog:
  type: object
  required: [petType, name]
  properties:
    petType: { type: string }
    name: { type: string }
    packSize: { type: integer }

2. Declare the discriminator on the parent schema

Add discriminator.propertyName and an explicit mapping from the wire value to the component $ref. The explicit mapping decouples the JSON value ("Cat") from your component name, so renaming the schema later does not change the API contract.

Pet:
  oneOf:
    - $ref: '#/components/schemas/Cat'
    - $ref: '#/components/schemas/Dog'
  discriminator:
    propertyName: petType        # the field deserializers switch on
    mapping:                     # wire value -> schema ref
      Cat: '#/components/schemas/Cat'
      Dog: '#/components/schemas/Dog'

3. Generate the SDK

openapi-generator-cli version-manager set 7.6.0
openapi-generator-cli generate \
  -i openapi.yaml \
  -g typescript-axios \
  -o ./sdk-ts

Expected output (truncated):

[main] INFO  o.o.codegen.TemplateManager - writing file ./sdk-ts/models/pet.ts
[main] INFO  o.o.codegen.TemplateManager - writing file ./sdk-ts/models/cat.ts
[main] INFO  o.o.codegen.TemplateManager - writing file ./sdk-ts/models/dog.ts

The generated models/pet.ts now carries the discriminator metadata instead of a bare union:

// ./sdk-ts/models/pet.ts (generated)
export type Pet = { petType: 'Cat' } & Cat | { petType: 'Dog' } & Dog;

For Java, the generated Pet is an interface implemented by Cat and Dog, annotated with Jackson’s @JsonTypeInfo / @JsonSubTypes so deserialization routes on petType:

openapi-generator-cli generate -i openapi.yaml -g java \
  --additional-properties=library=okhttp-gson,serializationLibrary=gson -o ./sdk-java

4. Verify polymorphic deserialization

Deserialize one payload per variant and assert the concrete type. TypeScript:

// verify.ts
import { Pet } from './sdk-ts/models';

const raw = JSON.parse('{"petType":"Cat","name":"Tom","huntsMice":true}') as Pet;
if (raw.petType === 'Cat') {
  console.log('Cat narrowed:', raw.huntsMice); // type guard works on petType
}

Expected output:

Cat narrowed: true

Complete Working Example

A single self-contained OpenAPI 3.1 document with a discriminated oneOf that generates cleanly for both TypeScript and Java.

# openapi.yaml — discriminated polymorphic response, OpenAPI 3.1.0
openapi: 3.1.0
info:
  title: Pet Store
  version: 1.0.0
paths:
  /pets/{id}:
    get:
      operationId: getPet
      parameters:
        - { name: id, in: path, required: true, schema: { type: string } }
      responses:
        '200':
          description: A pet, polymorphic by petType
          content:
            application/json:
              schema: { $ref: '#/components/schemas/Pet' }
components:
  schemas:
    Pet:
      oneOf:
        - $ref: '#/components/schemas/Cat'
        - $ref: '#/components/schemas/Dog'
      discriminator:
        propertyName: petType        # field every variant must supply
        mapping:                     # decouple wire value from component name
          Cat: '#/components/schemas/Cat'
          Dog: '#/components/schemas/Dog'
    Cat:
      type: object
      required: [petType, name]
      properties:
        petType: { type: string }    # MUST be required in every variant
        name: { type: string }
        huntsMice: { type: boolean }
    Dog:
      type: object
      required: [petType, name]
      properties:
        petType: { type: string }
        name: { type: string }
        packSize: { type: integer, minimum: 1 }

Gotchas & Edge Cases

anyOf with a discriminator behaves like oneOf for code generation. The discriminator narrows to exactly one subtype regardless of whether you used oneOf or anyOf, because the generator routes on the single propertyName value. If you genuinely need multiple shapes to match at once, a discriminator is the wrong tool — model it as allOf composition instead.

A missing or unmapped discriminator value throws in Java but slips through in TypeScript. Generated Gson/Jackson Java throws when petType is absent or its value is not in the mapping, while the TypeScript union simply produces an object that no branch narrows. Validate the spec so the discriminator field is required in every variant, and keep the mapping exhaustive.

Renaming a schema breaks deserialization if you relied on the implicit mapping. Without an explicit mapping, the generator uses the schema component name as the discriminator value, so renaming Cat to FelineComponent changes the expected wire value to FelineComponent and old payloads stop deserializing. Always write the mapping block to pin the wire values.

FAQ

Why does my generated SDK deserialize oneOf into a useless union or Object?

Without a discriminator the generator cannot tell the variants apart, so it emits a loose union in TypeScript or a base type or Object in Java. Adding discriminator.propertyName plus a mapping gives the generator the field it needs to pick the concrete subtype at runtime.

Does the discriminator property need to be in the required list?

Yes. The discriminator field must be present in every variant and listed under required, otherwise the deserializer has nothing reliable to switch on. Generated Java in particular throws if the discriminator value is missing or not in the mapping.

Should the discriminator mapping keys match the schema names?

Use an explicit mapping so the wire value is decoupled from your schema component names. Without an explicit mapping the generator falls back to using the schema name as the discriminator value, which breaks the moment you rename a component.