# OpenAPI Schemas — Routebase

> Objects, arrays and primitives, the difference between allOf, oneOf and anyOf, enums that carry meaning, formats, nullability in both dialects, and examples that stay true.

Canonical page: https://routebase.dev/guides/openapi/openapi-schemas/
Chapter 4 of 10 · OpenAPI · Last reviewed 2026-09-13 · The Routebase Team

Most of the effort in an OpenAPI document goes into schemas, and most of the disappointment with generated output traces back to them. A schema that says `type: object` and stops produces documentation that says nothing and a mock that returns nothing.

## The four shapes a schema takes

Every schema is one of four things, whatever the tooling calls them.

An **object** has named properties and a list of which ones are required. A **primitive** is a string, a number, an integer or a boolean, optionally narrowed by an enum or by bounds. An **array** is an ordered list with an item schema. A **composition** is built from other schemas with `allOf`, `oneOf` or `anyOf`.

The one rule worth memorising is that an array must define its items. A schema with `type: array` and nothing else is valid in the loosest sense and useless to everything downstream.

## Objects and required

Properties and required are separate lists, which surprises people who expect a flag on each property.

```yaml
Order:
  type: object
  required: [id, status, total]
  properties:
    id:
      type: string
      format: uuid
    status:
      type: string
      enum: [PENDING, PAID, SHIPPED, CANCELLED]
    total:
      type: number
      format: double
    note:
      type: string
      description: Free-text note the customer added at checkout.
```

Required means the property must be present, not that it must be non-empty and not that it is always useful. A property that is absent from `required` is optional for a request and, more importantly, optional for a response, which means every client has to handle it being missing. Marking response fields required is how you promise they will always be there.

## Composition without the confusion

The three combining keywords differ only in how many members must match, and picking the wrong one produces types nobody can use.

**`allOf` means every member matches.** It is how you express shared structure, so a `PaidOrder` can be an `Order` plus a payment block. Most code generators turn it into inheritance or into a merged type.

**`oneOf` means exactly one member matches.** It is the right choice for a value that is genuinely one thing or another, such as a card payment or a bank transfer. Generators turn it into a union or a discriminated type.

**`anyOf` means at least one member matches.** It is rarely what you mean. If two members can both match, no generator can decide which type to produce, so the result tends to be an untyped blob.

For `oneOf` and `anyOf` a `discriminator` names the property that says which member applies, which is what turns a union into something a generator can switch on.

```yaml
Payment:
  oneOf:
    - $ref: "#/components/schemas/CardPayment"
    - $ref: "#/components/schemas/BankTransfer"
  discriminator:
    propertyName: method
    mapping:
      card: "#/components/schemas/CardPayment"
      transfer: "#/components/schemas/BankTransfer"
```

## Enums that mean something

An enum lists the allowed values and says nothing about what they mean. A reader who sees `PARTIALLY_FULFILLED` learns the spelling and nothing else.

Put the meaning next to the values, because the alternative is that every consumer guesses. A short description that names each value and its consequence is enough, and it is the difference between a status field a client can act on and one they have to ask about.

Two extension fields are widely understood for this. `x-enumNames` carries a readable label per value and `x-enumDescriptions` carries an explanation, and generators that know them produce named constants instead of bare strings.

## Nullability depends on the dialect

This is the one schema question where the specification version changes the answer.

In OpenAPI 3.0 you set `nullable` to true, and it only takes effect when `type` is explicitly set in the same schema. In OpenAPI 3.1 the keyword does not exist, and you give the property two types instead.

```yaml
# OpenAPI 3.0
cancelledAt:
  type: string
  format: date-time
  nullable: true
```

```yaml
# OpenAPI 3.1
cancelledAt:
  type: [string, "null"]
  format: date-time
```

Quoting `"null"` in YAML matters, because an unquoted null is the null value rather than the string naming the type. The [version chapter](/guides/openapi/openapi-3-0-3-1-3-2/) covers the rest of the differences.

## Formats and real constraints

Format is a hint, and whether it is enforced depends on the validator. Treat `format: uuid` as documentation for the reader and the generator, and add a `pattern` or explicit bounds where the value genuinely has to match.

The formats worth using are the ones tooling recognises, such as `date`, `date-time`, `uuid`, `email`, `uri`, `int32`, `int64` and `double`. Inventing your own is allowed and achieves nothing unless you also control the tool that reads it.

## Examples that stay true

An example is the part of a schema a reader trusts most and the part most likely to be wrong, because nothing forces it to match the schema it sits under.

Two habits keep them honest. Generate the example from the schema rather than typing it, so a structural change is reflected automatically. And validate examples as part of linting, since an example that no longer matches its schema is a defect a machine can find. Both are covered in [validating and linting](/guides/openapi/validating-and-linting-openapi/).

## In Routebase

Schemas are edited as structures rather than as YAML, with the kind selector deciding whether you get a property table, a primitive editor, an item type or a composition operator.

_Screenshot: The kind selector at the top switches between object, primitive, array and composition, and the table below it is the properties block of the schema._

Enum values carry an optional label and description that export as `x-enumNames` and `x-enumDescriptions`, so the meaning travels with the file rather than living only in the workspace. The example panel generates a sample payload from the structure and can be regenerated after a change, and a usage count on every schema tells you what a change would touch before you make it. The [Schemas guide](https://docs.routebase.dev/schemas/) covers all four kinds in detail.

## Frequently asked questions

### What is a schema in OpenAPI?

A schema describes the shape of a piece of data, meaning its type, its properties, which of them are required and what values they may take. Schemas are used for request bodies, for responses and for parameter values. In OpenAPI 3.1 a schema is JSON Schema 2020-12, and in 3.0 it is a modified subset of an older draft.

### What is the difference between allOf, oneOf and anyOf?

All three combine schemas and they differ in how many members have to match. With allOf the data must satisfy every member, which is how composition and inheritance are expressed. With oneOf it must satisfy exactly one, which is how you describe a value that is either this or that. With anyOf it must satisfy at least one, which is the loosest and the least useful for generated code.

### How do you document what an enum value means?

The enum keyword only carries the allowed values, so the meaning has to go somewhere else. The usual place is the schema description, written as a short list of the values and what each one implies. Some tooling also reads the extension fields x-enumNames and x-enumDescriptions, which keep the label and the explanation attached to the value itself.

### What does format do in OpenAPI?

Format refines a type with a hint such as uuid, date-time, email or int32. Validators may or may not enforce it, so it is best understood as a signal to generators and readers rather than a constraint you can rely on. Where a value really must match a shape, add a pattern or explicit bounds next to the format.

---

[Routebase](https://routebase.dev/) — [Sign up](https://app.routebase.dev/): Every account starts with a 14-day Pro trial — no credit card required.
