openapijson-schemaapi-specswaggerwebhooksapi

OpenAPI 3.1 vs 3.0: What Changed and How to Migrate

·9 min read·APIs

The version bump that fixed JSON Schema

For years OpenAPI had an awkward secret: its "JSON Schema" wasn't really JSON Schema. OpenAPI 3.0 was based on an old JSON Schema draft (Draft 05) with its own dialect of tweaks, so schemas you wrote for validation didn't drop cleanly into an OpenAPI file, and vice versa. OpenAPI 3.1 fixes this at the root: it is fully aligned with [JSON Schema 2020-12](/blog/json-schema-2020-12-guide). By early 2026 the major gateways, docs platforms, and codegen tools default to 3.1, and 3.0-only stacks are starting to hit compatibility walls.

If you maintain an API contract — or generate one from code — this is the migration worth understanding. Here's exactly what changed and how to move.

Change 1: `nullable` is gone — use a type array

This is the change that breaks the most specs. In 3.0 you marked a field nullable with a dedicated keyword:

json
{ "type": "string", "nullable": true }

JSON Schema 2020-12 has no nullable keyword — instead type can be an *array*. So 3.1 expresses the same thing the standard way:

json
{ "type": ["string", "null"] }

Any tool that reads 3.1 ignores nullable, so leaving it in silently drops your null handling. This is the single most important find-and-fix of the migration.

Change 2: `example` becomes `examples`

3.0 used a singular example inside schema objects. 2020-12 uses a plural examples array:

json
{ "type": "string", "format": "email", "examples": ["ada@example.com"] }

(The example keyword still exists on media types and parameters — only the *schema-level* keyword changed to the array form.)

Change 3: `$ref` can stand on its own

In 3.0, a $ref had to appear alone; sibling keywords next to it were ignored. 2020-12 lets $ref coexist with other keywords, so you can reference a shared schema *and* add a description or override in the same object:

json
{
  "$ref": "#/components/schemas/Address",
  "description": "Billing address; overrides the shared description."
}

This makes component reuse far less painful and removes a whole class of "why is my description missing?" surprises.

Change 4: real webhooks

Before 3.1, an OpenAPI file could only describe requests a client *sends to you*. It had no way to describe calls *your API sends out* — the webhooks you POST to a subscriber. 3.1 adds a top-level webhooks object that describes those outbound event payloads with the same schema machinery as regular operations:

json
{
  "webhooks": {
    "orderShipped": {
      "post": {
        "requestBody": {
          "content": {
            "application/json": {
              "schema": { "$ref": "#/components/schemas/OrderShippedEvent" }
            }
          }
        }
      }
    }
  }
}

This pairs directly with webhook payload design — now the contract lives in the same file as the rest of your API.

Change 5: modern schema keywords unlocked

Because 3.1 speaks full 2020-12, you get expressive keywords 3.0 lacked:

  • `if` / `then` / `else` for conditional validation, instead of contorted nested allOf/oneOf.
  • Tuple validation via prefixItems for fixed-position arrays.
  • `const`, $defs, unevaluatedProperties, and the rest of the 2020-12 vocabulary.

A practical migration checklist

  1. Bump the version string to "openapi": "3.1.0".
  2. Replace every `nullable: true` with a ["type", "null"] type array. This is the one that changes behavior — do it first and grep for stragglers.
  3. Rename schema-level `example` to `examples` and wrap the value in an array.
  4. Collapse `$ref` workarounds — you can now put siblings next to $ref, so unwind any allOf-with-single-ref hacks you added just to attach a description.
  5. Move outbound events into the new webhooks object if you document them.
  6. Re-run codegen and linting against 3.1-aware tooling; older generators may not understand type arrays yet.
  7. Validate the result. Generate a schema for sample payloads and validate them against your components so the migration didn't quietly loosen a constraint. Format the spec to review large diffs.

Should you migrate now?

Yes, if your tooling supports it — and in 2026 most does. The payoff is that your API schemas and your standalone JSON Schema validators finally speak the same language, so a type defined once can validate requests, generate clients, and drive documentation without translation. The main risk is a stale generator or validator that hasn't caught up to 2020-12; check your toolchain before committing. For the schema fundamentals see the JSON Schema 2020-12 guide and OpenAPI and JSON Schema.

Frequently asked questions

OpenAPI 3.1 is fully aligned with JSON Schema Draft 2020-12, while 3.0 used an older, incompatible dialect based on Draft 05. That alignment removes OpenAPI-specific keywords like nullable and unlocks modern JSON Schema features such as if/then/else and tuple validation.

Use a type array instead of the old nullable keyword: { "type": ["string", "null"] }. The nullable keyword no longer exists in 3.1 and is ignored by conforming tools.

Yes. It adds a top-level webhooks object that lets you describe the outbound event payloads your API sends to subscribers, using the same schema and operation structure as normal endpoints.

It can be. Most of the spec is compatible, but nullable, the schema-level example keyword, and some $ref behaviors change. The nullable conversion in particular alters validation, so migrate deliberately and re-validate your payloads.

Not immediately, but it's recommended. As of 2026 major gateways, documentation tools, and generators default to 3.1, and staying on 3.0 increasingly means missing features and running into tooling that assumes 3.1.

Try JSONKit — JSON Developer Tools

Format, validate, convert and diff JSON. Instant, accurate and 100% private.