skip to content

What does an OpenAPI discriminator do, and what must be true for it to work?

level: seniorimportance: should knowfreq 42%

answer

  1. A tag that names the variant
  2. One required key, one optional map
  3. Every branch must carry the tag
  4. A hint, not an assertion
  5. New variants surprise old clients

basics

~20 s

An OpenAPI discriminator names a property whose value tells tooling which subschema a polymorphic payload belongs to. It requires propertyName, that property must be present and required in every variant, and it is a serialization hint — it does not replace oneOf validation.

solid answer

~50 s

A `discriminator` sits next to a `oneOf`, `anyOf` or an inheritance-style `allOf` and holds a required `propertyName` plus an optional `mapping`. The named property carries a value — `"card"`, `"bank_transfer"` — that identifies which variant the payload is. Without `mapping`, the value is matched against the *component schema name*; with `mapping`, you control the value-to-schema association explicitly, which you almost always want because wire values rarely equal your schema names. For it to work, the discriminating property must exist in every variant, must be a string, and must be listed in `required`. The point interviewers push on: a discriminator is a hint for serializers and generators, not an extra validation rule. A payload must still satisfy the underlying `oneOf` on its own; the discriminator only spares a deserializer from trying every branch, and it is what makes generated polymorphic clients usable.

code

yaml · 29 lines
yaml
components:
  schemas:
    PaymentMethod:
      oneOf:
        - $ref: '#/components/schemas/CardPayment'
        - $ref: '#/components/schemas/BankTransfer'
      discriminator:
        propertyName: kind
        mapping:
          card: '#/components/schemas/CardPayment'
          bank_transfer: '#/components/schemas/BankTransfer'

    CardPayment:
      type: object
      required: [kind, last4]
      properties:
        kind:
          type: string
          enum: [card]
        last4: { type: string }

    BankTransfer:
      type: object
      required: [kind, iban]
      properties:
        kind:
          type: string
          enum: [bank_transfer]
        iban: { type: string }

go deeper

for a junior

Know that a discriminator names a property whose value identifies which variant of a polymorphic payload you are looking at.

for a middle

Explain propertyName and mapping, the implicit schema-name fallback, and the requirement that the tag property be a required string in every variant.

for a senior

Demonstrate that it is a serialization hint rather than a validation rule, and pair it with per-branch enum or const so the oneOf and the discriminator can never disagree.

for a principal

Own the evolution policy for polymorphic payloads — how new variants roll out without breaking sealed generated clients, and whether the oneOf or allOf shape is the house standard given your generators.

## The problem it solves Suppose a payment method is either a card or a bank transfer: ``` PaymentMethod: oneOf: - $ref: '#/components/schemas/CardPayment' - $ref: '#/components/schemas/BankTransfer' ``` A validator can handle that by trying each branch. A *deserializer* cannot: it must decide which class to instantiate before it knows whether the data fits. Trying every branch and keeping the one that parses is slow, ambiguous when branches overlap, and produces terrible error messages. The `discriminator` gives it a direct answer. ## The object ``` PaymentMethod: oneOf: - $ref: '#/components/schemas/CardPayment' - $ref: '#/components/schemas/BankTransfer' discriminator: propertyName: kind mapping: card: '#/components/schemas/CardPayment' bank_transfer: '#/components/schemas/BankTransfer' ``` - `propertyName` — REQUIRED. The name of the property in the payload whose value selects the variant. - `mapping` — optional map from the property's *value* to either a schema name in `components.schemas` or an explicit `$ref`. Without `mapping`, the implicit rule is that the property's value must equal the component schema's name. That works only if your wire values happen to match your schema names, which for `bank_transfer` versus `BankTransfer` they do not — so an explicit `mapping` is the norm. ## The preconditions The discriminator only functions if all of the following hold: 1. **The property exists in every variant.** Each subschema must declare `kind` among its `properties`. 2. **It is required.** Each variant must list it in `required`. A discriminator over an absent property has nothing to read. 3. **It is a string.** The specification constrains the discriminating property to a string value. 4. **It sits on the composed schema.** `discriminator` is placed next to the `oneOf`/`anyOf` (or on the parent schema in the `allOf` inheritance pattern), not inside a branch. A good practice on top of the spec's requirements is to pin the value per variant with a single-value `enum` (3.0) or `const` (3.1) — `enum: [card]` inside `CardPayment`. That is what makes the `oneOf` branches genuinely disjoint, so validation and discrimination agree. ## The semantics people get wrong The discriminator does **not** add validation. It is described by the specification as a hint to aid serialization and deserialization. Two consequences: - A payload whose `kind` is `"card"` but whose body does not satisfy `CardPayment` is invalid because of the `oneOf`, not because of the discriminator. - A validator that ignores `discriminator` entirely is still conforming. So you cannot rely on it to reject an unknown `kind` value; if you need that rejection, constrain the property with an `enum` in each branch. ## The allOf inheritance pattern The other place a discriminator appears is the parent of an inheritance-style hierarchy: `Pet` declares `discriminator: {propertyName: petType}` and `Cat`/`Dog` each use `allOf` with a `$ref` to `Pet`. Here the discriminator lives on the *parent*, and tooling walks it downward to find the concrete type. Support for this shape varies more between generators than the `oneOf` shape does, so if you have a choice, `oneOf` plus an explicit `mapping` is the more portable modelling. ## What generators do with it Given a discriminator, generators emit type-tagged polymorphic deserialization: a sealed class or interface with concrete subtypes, plus the wiring that reads the tag property and dispatches. Without one, most generators produce a wrapper type whose deserializer attempts each branch — workable for two simple variants, painful for six. This is why discriminators show up in interviews as a *production* question rather than a syntax one: polymorphic payloads are exactly where a spec and its generated clients diverge, and the discriminator is the mechanism that keeps them aligned. ## Evolution Adding a new variant means adding a schema, a `mapping` entry, and a new allowed value in the tag `enum`. Old generated clients do not know the new value: a sealed-type deserializer will typically throw. Plan for it — either version the endpoint, or document that consumers must tolerate an unknown tag, and consider a permissive fallback variant if your generator supports one. This is the same additive-compatibility problem enums have, concentrated at the point where it hurts most.

  • Does a discriminator make the oneOf validation unnecessary?
    No. The specification defines it as a serialization hint, and a conforming validator may ignore it entirely. The payload must still satisfy the underlying `oneOf` on its own terms — the discriminator only tells a deserializer which class to build.
  • What happens if you omit the mapping?
    Tooling falls back to the implicit rule: the property value must equal the name of the schema under `components.schemas`. That only works when your wire values match your schema names exactly, so `bank_transfer` against `BankTransfer` fails. An explicit mapping is the safe default.
  • How do you keep the oneOf branches disjoint so validation agrees with the discriminator?
    Pin the discriminating property to one value per branch — `enum: [card]` in OpenAPI 3.0, or `const: card` in 3.1 — and list it in each branch's `required`. Then exactly one branch can ever validate, so `oneOf` and the discriminator can never disagree.
  • What breaks in a generated client when you add a new variant?
    Older SDKs have a sealed set of subtypes and no case for the new tag value, so deserialization typically throws when the new variant first appears in a response. Treat adding a variant as a compatibility event: version it, or document that consumers must tolerate unknown tags.

saying these in an interview costs you the question

  • Says the discriminator validates the payload
  • Omits mapping and assumes any value works
  • Puts the discriminator inside a oneOf branch
  • Leaves the tag property out of required
  • Adds a variant and calls it backward compatible

context