16. Pattern: Polymorphic Schema
The Polymorphic Schema pattern is used when an API request or response payload must dynamically adapt its structural shape based on underlying domain differences that cannot be unified into a single schema.
Instead of defining an overly broad schema filled with optional fields or using unstructured key-value objects, you expose a flexible payload contract using OpenAPI polymorphism keywords (oneOf, anyOf, allOf) paired with an explicit discriminator field.
16.1. Overview
Polymorphic Schema enables APIs to model heterogeneous collections and varying resource representations cleanly within a single API endpoint contract.
Typical examples:
GET /pages/{pageId}/blocks(Returns an array of heading, image, and video layout blocks)POST /payments(Accepts credit card, bank transfer, or crypto payload structures)POST /notifications(Dispatches distinct payloads for email, SMS, or push notifications)
These operations:
- Enforce strict structural validation for each individual concrete subtype
- Provide SDK generators and client code with clear type hints to instantiate concrete classes or interfaces
- Use a designated discriminator property (such as
blockTypeortype) to tell parsers which schema to inspect
16.2. When to Use Polymorphic Schema
Use Polymorphic Schema when:
- The request or response payload is genuinely heterogeneous.
- e.g., A CMS page rendering an ordered stream of distinct content block types (
heading,image,video).
- e.g., A CMS page rendering an ordered stream of distinct content block types (
- Underlying schema differences cannot be unified.
- e.g., A video block requires an
embedUrlandprovider, whereas an image block requiresurl,altText, and dimensions. Combining them into one model dilutes validation.
- e.g., A video block requires an
- You need strict schema validation for every distinct payload variant.
- You want the API validator to reject invalid fields specific to a variant (e.g., rejecting
altTexton aheadingblock).
- You want the API validator to reject invalid fields specific to a variant (e.g., rejecting
- You want automated, strongly-typed code generation.
- OpenAPI tooling uses the
discriminatorto generate polymorphism in client libraries (e.g., interface inheritance or sealed classes).
- OpenAPI tooling uses the
16.3. When NOT to Use Polymorphic Schema
Avoid Polymorphic Schema when:
- The schemas are 90% identical.
- If variants only differ by a single optional field, standard schema composition or standard optional properties are simpler to maintain.
- Endpoints can be split into distinct resources.
- If a client interacts with only one resource type at a time (e.g.,
POST /articlesvsPOST /videos), prefer separate endpoints over a polymorphic endpoint.
- If a client interacts with only one resource type at a time (e.g.,
- The client does not need to handle variants generically.
- If the client always knows the exact subtype upfront, forced polymorphism adds unnecessary indirection.
16.4. What the Pattern Looks Like
The Polymorphic Schema pattern uses oneOf to declare mutually exclusive schemas and a discriminator property to steer validation and parsing.
Unified / Overly Broad Attempt (Problematic)
GET /pages/{pageId}/blocks- Returns a generic block where
text,level,url,altText,embedUrl, andproviderare all optional properties on a single object. - Drawback: The contract cannot enforce that
levelis required whentextis present, or thatembedUrlis forbidden on images.
- Returns a generic block where
Polymorphic Schema Approach
GET /pages/{pageId}/blocks- Returns an array of objects matching a base schema contract, validated strictly against
HeadingBlock,ImageBlock, orVideoBlock. - The object’s
blockTypefield directs the client parser directly to the appropriate schema variant.
- Returns an array of objects matching a base schema contract, validated strictly against
[
{
"id": "blk_101",
"blockType": "heading",
"text": "Welcome to the Platform",
"level": 1
},
{
"id": "blk_102",
"blockType": "image",
"url": "https://assets.example.com/banner.png",
"altText": "Dashboard preview",
"width": 1200,
"height": 600
},
{
"id": "blk_103",
"blockType": "video",
"embedUrl": "https://www.youtube.com/embed/dQw4w9WgXcQ",
"provider": "youtube",
"autoplay": false
}
]
16.5. Anti-Patterns to Avoid
1. Amalgamating distinct schemas into a single object
{
"id": "blk_101",
"blockType": "image",
"text": null,
"level": null,
"url": "https://assets.example.com/banner.png",
"altText": "Dashboard preview",
"embedUrl": null
}
- Mixing non-unifiable attributes into a flat object filled with nulls or optional fields ruins contract validation and exposes an unclear API interface to consumers.
2. Omitting the OpenAPI discriminator field
oneOf:
- $ref: '#/components/schemas/HeadingBlock'
- $ref: '#/components/schemas/ImageBlock'
- Defining
oneOfwithout an explicitdiscriminatorforces client SDKs and API gateways to attempt structural matching (duck typing). If schemas share similar properties, validation becomes slow, ambiguous, and fragile.
3. Using anyOf when schemas are mutually exclusive
- Using
anyOfallows a payload to validate successfully against multiple schemas simultaneously. UseoneOfalongside adiscriminatorto enforce that each payload matches exactly one concrete subtype.
16.6. OpenAPI Example
A lean but complete OpenAPI 3.0.3 document showing:
- A polymorphic
GET /pages/{pageId}/blocksendpoint. - Usage of
oneOfto define concrete schema variants. - A
discriminatorblock referencingblockTypewith explicit mappings.
openapi: 3.0.3
info:
title: Content Management API - Polymorphic Schema Example
version: 1.0.0
servers:
- url: https://api.example.com
paths:
/pages/{pageId}/blocks:
get:
summary: Retrieve layout blocks for a page
description: Returns an array of polymorphic content blocks driving a page's layout.
tags: [Page Content]
parameters:
- in: path
name: pageId
required: true
schema:
type: string
example: page_9921
responses:
'200':
description: A list of ordered content blocks
content:
application/json:
schema:
type: array
items:
$ref: '#/components/schemas/ContentBlock'
components:
schemas:
ContentBlock:
type: object
required:
- id
- blockType
properties:
id:
type: string
example: blk_101
blockType:
type: string
description: Discriminator field defining the concrete block schema.
discriminator:
propertyName: blockType
mapping:
heading: '#/components/schemas/HeadingBlock'
image: '#/components/schemas/ImageBlock'
video: '#/components/schemas/VideoBlock'
oneOf:
- $ref: '#/components/schemas/HeadingBlock'
- $ref: '#/components/schemas/ImageBlock'
- $ref: '#/components/schemas/VideoBlock'
HeadingBlock:
type: object
required:
- text
- level
properties:
text:
type: string
example: "Welcome to the Platform"
level:
type: integer
minimum: 1
maximum: 6
example: 1
ImageBlock:
type: object
required:
- url
- altText
properties:
url:
type: string
format: uri
example: "https://assets.example.com/banner.png"
altText:
type: string
example: "Dashboard preview"
width:
type: integer
example: 1200
height:
type: integer
example: 600
VideoBlock:
type: object
required:
- embedUrl
- provider
properties:
embedUrl:
type: string
format: uri
example: "https://www.youtube.com/embed/dQw4w9WgXcQ"
provider:
type: string
enum: [youtube, vimeo, custom]
example: "youtube"
autoplay:
type: boolean
default: false
16.7. Visualizing Polymorphic Schema (Mermaid Diagram)
sequenceDiagram
participant Client as Client Application / SDK
participant API as API Gateway / Router
participant Service as Page Service
Client->>API: GET /pages/page_9921/blocks
API->>Service: Query blocks for page_9921
Service-->>API: Return heterogeneous array [Heading, Image, Video]
Note over API,Client: Payload Serialization with "blockType" Discriminator
API-->>Client: 200 OK [ { blockType: "heading", ... }, { blockType: "image", ... } ]
Note over Client: Client SDK inspects "blockType"
alt blockType == "heading"
Client->>Client: Deserialize payload as HeadingBlock schema
else blockType == "image"
Client->>Client: Deserialize payload as ImageBlock schema
else blockType == "video"
Client->>Client: Deserialize payload as VideoBlock schema
end