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 blockType or type) 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).
  • Underlying schema differences cannot be unified.
    • e.g., A video block requires an embedUrl and provider, whereas an image block requires url, altText, and dimensions. Combining them into one model dilutes validation.
  • 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 altText on a heading block).
  • You want automated, strongly-typed code generation.
    • OpenAPI tooling uses the discriminator to generate polymorphism in client libraries (e.g., interface inheritance or sealed classes).

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 /articles vs POST /videos), prefer separate endpoints over a polymorphic endpoint.
  • 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, and provider are all optional properties on a single object.
    • Drawback: The contract cannot enforce that level is required when text is present, or that embedUrl is forbidden on images.

Polymorphic Schema Approach

  • GET /pages/{pageId}/blocks
    • Returns an array of objects matching a base schema contract, validated strictly against HeadingBlock, ImageBlock, or VideoBlock.
    • The object’s blockType field directs the client parser directly to the appropriate schema variant.
[
  {
    "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 oneOf without an explicit discriminator forces 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 anyOf allows a payload to validate successfully against multiple schemas simultaneously. Use oneOf alongside a discriminator to 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}/blocks endpoint.
  • Usage of oneOf to define concrete schema variants.
  • A discriminator block referencing blockType with 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