OpenAPI 3.1 keeps the 3.0 document structure and replaces the schema language. In 3.0, a Schema Object is an extended subset of JSON Schema with OpenAPI-only keywords such as nullable. In 3.1, schemas are full JSON Schema draft 2020-12. That one decision drives most of the edits you will make: nullable: true becomes a type array, exclusiveMinimum takes a number, example becomes examples, and binary uploads use contentMediaType. 3.1 also adds top-level webhooks, makes paths optional, and allows siblings next to $ref. Upgrade when your code generator, gateway and docs renderer all read 3.1, or when you need webhooks or plain JSON Schema validators. If one tool in the pipeline only reads 3.0, stay on 3.0.3 or 3.0.4 for now; the conversion is a short, mechanical list you can run later.
This guide covers each change with the 3.0 version and the 3.1 version side by side, then gives a migration checklist. If you are still deciding between Swagger 2.0 and OpenAPI 3, start with Swagger vs OpenAPI, which covers the naming and the 2.0 to 3.0 move.
Every change in one table
| Area | OpenAPI 3.0 | OpenAPI 3.1 |
|---|---|---|
| Version field | openapi: 3.0.3 (or 3.0.4) | openapi: 3.1.0 (or 3.1.1) |
| Schema language | Extended subset of JSON Schema Wright draft 00 | JSON Schema draft 2020-12, plus the OpenAPI base vocabulary |
| Null values | nullable: true | type: [string, "null"] or a "null" branch in oneOf/anyOf |
| Exclusive bounds | exclusiveMinimum: true next to minimum | exclusiveMinimum: 0 (the bound itself) |
| Schema examples | example: single value | examples: array of values (example is deprecated) |
| $ref siblings | Ignored | Allowed in schemas; summary and description allowed on other references |
| Binary uploads | type: string, format: binary | contentMediaType, or no schema for a raw body |
| Base64 strings | type: string, format: byte | type: string, contentEncoding: base64 |
| Webhooks | Not available (callbacks only) | Top-level webhooks map |
| Required root fields | openapi, info, paths | openapi, info, and at least one of paths, components, webhooks |
| Default schema dialect | Fixed | jsonSchemaDialect at the root |
| info and license | title, description, license.url | Adds info.summary and license.identifier (SPDX) |
| Reusable path items | Not in components | components.pathItems |
| Security schemes | apiKey, http, oauth2, openIdConnect | Adds mutualTLS |
One note on version numbers. 3.1 is a minor release that breaks compatibility, and the 3.1 specification says so directly: the OpenAPI Initiative may make non-backwards-compatible changes in minor versions when the benefit outweighs the impact. A 3.0 file with a boolean exclusiveMinimum fails 3.1 validation, and a leftover nullable: true is silently ignored, so changing the version string alone is a bug.
Schemas are now JSON Schema 2020-12
In 3.0, the Schema Object borrowed keywords from an old JSON Schema draft, dropped some, and changed the meaning of others. type had to be a single string. There was no const, no if/then/else, no prefixItems. A standard JSON Schema validator could not read a 3.0 schema without an adapter, because nullable, and the boolean form of exclusiveMinimum, meant something it did not know.
In 3.1, any keyword from JSON Schema 2020-12 is valid inside a Schema Object. That includes const, if, then and else, dependentRequired, unevaluatedProperties, prefixItems, contentMediaType and contentEncoding, and $schema, $id and $anchor. The OpenAPI-specific keywords that remain, such as discriminator, xml and externalDocs, live in the OpenAPI base vocabulary. Reuse is the practical gain. A schema you already maintain for request validation in your backend can be referenced from the OpenAPI document without a second copy in a different dialect.
openapi: 3.1.1
jsonSchemaDialect: https://spec.openapis.org/oas/3.1/dialect/base
info:
title: Orders API
version: 2.0.0
components:
schemas:
Shipment:
type: object
required: [method]
properties:
method:
enum: [pickup, courier]
address:
$ref: "#/components/schemas/Address"
if:
properties:
method:
const: courier
then:
required: [address]Replace nullable with a type array
This is the change that touches the most files. OpenAPI 3.1 has no nullable keyword at all. JSON Schema skips keywords it does not know, so most 3.1 tools ignore a leftover nullable: true without an error, and the field quietly stops accepting null. In JSON Schema, null is a type of its own, and a property that accepts a string or null lists both types.
nickname:
type: string
nullable: true
status:
type: string
enum: [active, paused]
nullable: truenickname:
type: [string, "null"]
status:
type: [string, "null"]
enum: [active, paused, null]Two details trip people up. First, enum still restricts the value on its own, so a nullable enum needs null in the enum list as well as in type, written unquoted because it is the null value itself. Leave it out and null fails validation. Second, the 3.0 workaround for a nullable reference, allOf with one $ref plus nullable: true, has a direct 3.1 replacement that says what it means:
billingAddress:
oneOf:
- $ref: "#/components/schemas/Address"
- type: "null"exclusiveMinimum and exclusiveMaximum take numbers
In 3.0, exclusiveMinimum and exclusiveMaximum are booleans that change how minimum and maximum behave. In 3.1 they hold the bound itself, as JSON Schema defines them. A schema with both minimum and exclusiveMinimum is still valid in 3.1, but the two keywords now apply independently, so the old pair turns into one keyword.
quantity:
type: integer
minimum: 0
exclusiveMinimum: true
discount:
type: number
maximum: 1
exclusiveMaximum: truequantity:
type: integer
exclusiveMinimum: 0
discount:
type: number
exclusiveMaximum: 1A converted file that still says exclusiveMinimum: true is an invalid 3.1 schema, because 2020-12 requires a number there.
example becomes examples inside schemas
JSON Schema has an examples keyword that takes an array, and 3.1 schemas use it. The singular example on a Schema Object still parses in 3.1 but is deprecated. This only applies to schemas. The example field and the examples map on Parameter and Media Type Objects are unchanged, so leave those alone during the migration.
Order:
type: object
properties:
id:
type: string
example: ord_8f2k1
total:
type: number
example: 49.5Order:
type: object
properties:
id:
type: string
examples: [ord_8f2k1, ord_77xq0]
total:
type: number
examples: [49.5]$ref can have siblings
In 3.0, any field next to $ref is ignored. Writing a description beside a reference was common and did nothing, which is why many 3.0 files wrap a single $ref in allOf just to attach a description. In 3.1, a $ref inside a Schema Object is an ordinary JSON Schema keyword, so sibling keywords apply alongside the referenced schema. Outside schemas, a Reference Object may carry summary and description, which override the referenced component's own text. Other fields on a Reference Object are still ignored.
customer:
allOf:
- $ref: "#/components/schemas/Customer"
description: The customer who placed the order.customer:
$ref: "#/components/schemas/Customer"
description: The customer who placed the order.File uploads use contentMediaType and contentEncoding
3.0 described binary data as a string with format: binary, and base64 text as format: byte. 3.1 uses the JSON Schema content keywords. For a raw binary request body, the media type key already says what the bytes are, so the schema can be left out entirely.
requestBody:
content:
application/octet-stream:
schema:
type: string
format: binary
multipart/form-data:
schema:
type: object
properties:
avatar:
type: string
format: binary
thumbnail:
type: string
format: byterequestBody:
content:
application/octet-stream: {}
multipart/form-data:
schema:
type: object
properties:
avatar:
type: string
contentMediaType: image/png
thumbnail:
type: string
contentEncoding: base64New document-level fields
The rest of the changes add fields. None of them force an edit to an existing file, but a few are worth adopting during the upgrade.
- webhooks is a top-level map of requests your API sends to subscribers. Each entry is a Path Item, so it has operations, a request body and expected responses. In 3.0 the closest option was callbacks, which must hang off an operation that registers the URL.
- paths is optional. A valid 3.1 document needs openapi, info and at least one of paths, components or webhooks. That makes a components-only file, such as a shared schema library, a valid document.
- info.summary is a one-line description of the API, separate from the longer info.description.
- license.identifier takes an SPDX expression such as Apache-2.0. It is mutually exclusive with license.url, so use one or the other.
- components.pathItems stores reusable Path Item Objects that paths and webhooks can reference.
- mutualTLS is a new security scheme type for APIs that authenticate clients with certificates.
openapi: 3.1.1
info:
title: Orders API
version: 2.0.0
summary: Place and track orders.
license:
name: Apache 2.0
identifier: Apache-2.0
webhooks:
orderShipped:
post:
summary: Sent when an order leaves the warehouse
requestBody:
required: true
content:
application/json:
schema:
$ref: "#/components/schemas/Order"
responses:
"204":
description: Subscriber accepted the eventShould you upgrade from 3.0 to 3.1?
Tool support decides this one. Most teams run a spec through several programs: a linter, a docs renderer, a client or server code generator, a mock server, a gateway that imports routes. Every one of them has to read 3.1 before the upgrade pays off.
Upgrade 3.0 to 3.1?
| Scenario | Recommendation | Alternative |
|---|---|---|
| Every tool in your pipeline documents 3.1 support | Upgrade. You get standard JSON Schema, cleaner null handling and webhooks for a few hours of edits. | Upgrade on a branch first and diff the generated client code before merging. |
| Your code generator or API gateway only reads 3.0 | Stay on 3.0.3 or 3.0.4 and revisit when the tool adds 3.1. | Keep a 3.1 source and down-convert for that one tool, if you can maintain the extra step. |
| You need to describe outgoing webhooks | Use 3.1. The webhooks field exists only in 3.1 and later. | — |
| You share schemas between OpenAPI and a JSON Schema validator in your backend | Use 3.1, so both read the same draft 2020-12 schemas without an adapter. | — |
| Your framework generates the spec from code | Use the version it emits. Many frameworks expose a setting for the output version; change that setting and leave the generated file alone. | — |
After the upgrade, compare generated output before and after. A type array such as [string, "null"] should produce the same nullable type your 3.0 nullable: true produced. If the generated code changes shape, the generator's 3.1 support is the cause, and you have found it before your frontend did.
Migration checklist: OpenAPI 3.0 to 3.1
Upgrade OpenAPI 3.0 to 3.1
- Change the version field to openapi: 3.1.13.1.1 clarifies the 3.1.0 text without changing the format. Use 3.1.0 if a tool rejects 3.1.1.
- Replace every nullable: true with a type arraytype: string plus nullable: true becomes type: [string, "null"]. Add null to any enum list on the same schema.
- Rewrite nullable referencesallOf with one $ref plus nullable: true becomes oneOf with the $ref and type: "null".
- Convert boolean exclusiveMinimum and exclusiveMaximumMove the value from minimum or maximum into the exclusive keyword and delete the boolean.
- Change example to examples inside Schema ObjectsWrap each value in an array. Leave example on parameters and media types as it is.
- Replace format: binary and format: byteUse contentMediaType for binary parts, an empty media type for raw bodies, and contentEncoding: base64 for base64 strings.
- Remove allOf wrappers that only exist to attach a description to a $refOptional, but 3.1 reads siblings next to $ref directly.
- Add info.summary and license.identifierOptional. license.identifier replaces license.url; do not set both.
- Move outgoing events from extensions or prose into webhooksOptional. Useful if you documented events with x- extensions or in descriptions.
- Validate the result against the 3.1 schemaThen load it in each downstream tool and compare generated clients and rendered docs with the 3.0 output.
The spec validation guide shows how to run that last step in CI so a half-migrated file never merges. If the validator prints errors you do not recognize, common OpenAPI errors explains the usual messages.
What about OpenAPI 3.2?
The OpenAPI Initiative published OpenAPI 3.2.0 in September 2025. It keeps the 3.1 schema model, JSON Schema 2020-12, and adds features on top, such as hierarchical tags and support for streaming media types. Getting to 3.1 is the large step; moving from 3.1 to 3.2 later is mostly additive. Tool support for 3.2 is newer still, so check your pipeline before targeting it.
Check a 3.0 or 3.1 spec in APIForge
APIForge parses OpenAPI 3.0 and 3.1 documents, as well as Swagger 2.0, from a URL or a file. Load the 3.0 file, then the 3.1 version, in the OpenAPI validator and confirm both list the same operations, parameters and schemas. When you generate TypeScript types, APIForge maps both nullable: true from 3.0 and a type array that includes "null" from 3.1 to a union with null, so the generated types should match across the two files. APIForge does not convert between versions, so make the edits by hand or with a converter, and use APIForge to check that nothing was dropped.