Handling Polymorphic JSON in OpenAPI with the Discriminator Keyword
OpenAPI's discriminator keyword lets you model polymorphic payloads—like multiple payment methods—in a single schema. This post shows how to define the mapping, see it work in Swagger UI, and generate type-safe clients without custom validation code.
01 Sept 2025, 14:35 UTC

The Problem: One Endpoint, Many Payload Shapes
REST APIs often need to accept or return objects that share a common concept but differ in structure. A payments endpoint might receive a credit card, a PayPal account, or a bank transfer—each with its own fields. Without a standard way to describe this, developers resort to manual if/else checks on a type property, scattering validation logic across controllers and client code. The result is boilerplate that's easy to get wrong and hard to keep in sync with the API contract.
OpenAPI's Discriminator: A Contract-First Solution
OpenAPI 3.0 introduced the discriminator keyword to solve this at the schema level. You define a base schema that declares which property acts as the type tag, then map each possible value to a concrete schema. Tools like Swagger UI and OpenAPI Generator read this mapping and produce type-safe models, validation, and interactive documentation without custom code.
Defining the Base Schema and Mapping
Start with a base schema that uses oneOf to reference each concrete type. The discriminator object specifies the property name (e.g., type) and a mapping from literal values to schema references. Each concrete schema must include the discriminator property as a required field with an enum constraint matching its key.
components:
schemas:
PaymentMethod:
oneOf:
- $ref: '#/components/schemas/CreditCard'
- $ref: '#/components/schemas/PayPal'
- $ref: '#/components/schemas/BankTransfer'
discriminator:
propertyName: type
mapping:
credit_card: '#/components/schemas/CreditCard'
paypal: '#/components/schemas/PayPal'
bank_transfer: '#/components/schemas/BankTransfer'
CreditCard:
type: object
required: [type, number, expiry, cvv]
properties:
type:
type: string
enum: [credit_card]
number:
type: string
pattern: '^\d{13,19}$'
expiry:
type: string
pattern: '^\d{2}/\d{2}$'
cvv:
type: string
pattern: '^\d{3,4}$'
PayPal:
type: object
required: [type, email]
properties:
type:
type: string
enum: [paypal]
email:
type: string
format: email
BankTransfer:
type: object
required: [type, accountNumber, routingNumber]
properties:
type:
type: string
enum: [bank_transfer]
accountNumber:
type: string
routingNumber:
type: string
pattern: '^\d{9}$'
With this structure, any request body validated against PaymentMethod must include a type field whose value matches one of the mapping keys. The validator then applies the corresponding concrete schema.
Swagger UI: Interactive Type Selection
When you load this spec in Swagger UI, the Try it out panel for an endpoint accepting PaymentMethod shows a dropdown for the type field. Selecting credit_card reveals the number, expiry, and cvv fields; choosing paypal shows only email. This eliminates guesswork for API consumers and reduces malformed requests.
Worked Example: Generating a TypeScript Client
Run the OpenAPI Generator to produce a TypeScript client with discriminated unions. From a terminal with Node.js installed (no elevated permissions needed):
npx @openapitools/openapi-generator-cli generate \
-i ./openapi.yaml \
-g typescript-axios \
-o ./generated-client
The generated PaymentMethod type becomes a discriminated union:
export type PaymentMethod =
| { type: 'credit_card'; number: string; expiry: string; cvv: string }
| { type: 'paypal'; email: string }
| { type: 'bank_transfer'; accountNumber: string; routingNumber: string };
TypeScript narrows the type automatically when you check payment.type, giving you compile-time safety without runtime guards.
Trade-offs and Limitations
- Property must be present and literal. The discriminator only works when the distinguishing property exists in the payload and holds a plain string value. Conditional schemas (e.g., "if field A exists then type is X") require
anyOfwith custom validators. - Nested polymorphism needs extra care. If a concrete schema itself contains a polymorphic field, you must define a separate discriminator for that level; a single discriminator does not cascade.
- Language support varies. Generated code in languages without sealed unions (e.g., plain JavaScript, older Java) may still need runtime checks for unknown
typevalues.
Verify It Works
- Validate the spec:
swagger-cli bundle openapi.yaml --type yaml(install vianpm i -g swagger-cli). No errors means the discriminator syntax is correct. - Spin up Swagger UI locally:
docker run -p 8080:8080 -v $(pwd)/openapi.yaml:/openapi.yaml swaggerapi/swagger-ui. Openhttp://localhost:8080and confirm the type dropdown appears in the request body editor. - Send test requests with
curlfor each type and verify your server receives the expected JSON structure. Example for credit card:curl -X POST http://localhost:3000/payments \ -H 'Content-Type: application/json' \ -d '{"type":"credit_card","number":"4111111111111111","expiry":"12/28","cvv":"123"}'
Next Steps
Add the discriminator to your OpenAPI document, run the validation and UI checks above, then regenerate your client SDKs. You'll replace ad-hoc type checks with a single source of truth that stays consistent across documentation, validation, and generated code.
0 replies
A thoughtful contribution can make all the difference. Be the first to share one.