Schema Composition

By Phil Sturgeon
Last update on July 24, 2024

In OpenAPI v3.1 and JSON Schema, you can use oneOf, allOf, and anyOf keywords to handle composition, which is the concept of combining multiple schemas and subschemas in various ways to handle polymorphism, or “extending” other schemas to add more criteria.

What are oneOf, anyOf, and allOf? #

All of these keywords must be an array, where each item is a schema. Be careful with recursive schemas as they can exponentially increase processing times.

oneOf #

The oneOf keyword is used when you want to specify that a value should match one of the given schemas exactly. It’s useful when you have different possible data structures or types for a particular field, like accepting bank account or card payments, or having train tickets and tram tickets, which are similar but a little different.

The validation will pass if the value matches exactly one of the schemas defined in oneOf.

This can be done for a single value:

properties:
	timestamp:
    oneOf:
    - type: string
      format: date-time
      examples:
      - '2024-07-21T17:32:28Z'
    - type: integer
      examples: 
      - 1721820298

In this example the timestamp property could be either a RFC 3339 date time (e.g. 2017-07-21T17:32:28Z) or a unix timestamp (e.g. 1721820298).

That shows how it works for a single property, but oneOf can also be used with whole objects:

 properties:
    source:
      oneOf:
        - title: Card
          properties:
            number:
              type: string
            cvc:
              type: integer
            exp_month:
              type: integer
              format: int64
            exp_year:
              type: integer
              format: int64
          required:
            - number
            - cvc
            - exp_month
            - exp_year
        
        - title: Bank Account
          type: object
          properties:
            number:
              type: string
            sort_code:
              type: string
            account_type:
              type: string
              enum:
                - individual
                - company
          required:
            - number
            - account_type

In the above example, the source property will be an object either way, but the properties contained within can be in one of two combinations. Either number, cvc, exp_month, and exp_year are valid and in good form (a card payment), or number, sort_code, and account_type will be sent (a bank account). These are the only two outcomes which will return a valid result in a validator, because if neither subscheme match it will be invalid, and if multiple subschemas match that will also be invalid.

anyOf #

The anyOf keyword is very similar to oneOf but a little less restrictive. oneOf is more like a XOR in programming, where one or the other can match, but never both. anyOf is more like a regular OR, which allows one or another or both.

Just like oneOf, you can use anyOf when you have multiple valid options for a particular field. The validation will pass if the value matches one or more of the listed subschemas.

oneOf:
  - type: number
    multipleOf: 5
  - type: number
    multipleOf: 3

The values 1, 2, 4, 7, 8, 11, 13, 14 would all be rejected for not being multiples of either 3 or 5.

The values 3, 5, 6, 9, 10, 12 would be valid for being multiples of 3 and 5.

The value 15 would be rejected because it is multiples of both 3 and 5, and oneOf doesn’t like that.

allOf #

The allOf keyword is used when you want to specify that a value should match all of the given schemas. It is useful when you want to combine multiple schemas together. The validation will pass if the value matches all of the schemas defined in allOf.

allOf:
  - type: object
    properties:
      name:
        type: string
  - type: object
    properties:
      age:
        type: integer
        minimum: 0

In the above example, the value should be an object that has both a name property of type string and an age property of type integer with a minimum value of 0.

References in Composition #

All of these keywords can contain a list of subschemas that are defined directly inside them, or a $ref can point to a schema defined elsewhere.

schema:
  oneOf:
    - $ref: '#/components/schemas/Card'
    - $ref: '#/components/schemas/BankAccount'

This says that the schema can be either one of these schemas stored as shared components.

Due to the way allOf works, you can essentially reference multiple schemas and say “I want all of the validation rules and criteria from all of these schemas to apply here”, providing a sort of merge-like functionality.

schema:
  allOf:
    - $ref: '#/components/schemas/PaymentMethod'
    - $ref: '#/components/schemas/BankAccount'

This basically declares a schema which has a lot of generic payment fields, then adds specific fields from the bank account type, to avoid declaring generic fields like “name” and “number” in both.

Feel free to mix and match a $ref and an inline subschema, which is a handy way to pop some extra content into a generic shared schema like HATEOAS links:

content:
  application/json:
    schema:
      allOf:
        - $ref: '#/components/schemas/Booking'
        - properties:
            links:
              $ref: '#/components/schemas/Links-Self'

These schema composition keywords provide flexibility and allow you to define complex data structures and validation rules in OpenAPI v3.1 and JSON Schema, which becomes more useful as you start to improve reuse across one or more API.

Representing XML