OpenAPI 3.0 vs 3.1: What Changed and Should You Upgrade

OpenAPI 3.0 vs 3.1: nullable, exclusiveMinimum, examples, webhooks and JSON Schema 2020-12, with before/after YAML and a checklist to upgrade 3.0 to 3.1.

On this page

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

AreaOpenAPI 3.0OpenAPI 3.1
Version fieldopenapi: 3.0.3 (or 3.0.4)openapi: 3.1.0 (or 3.1.1)
Schema languageExtended subset of JSON Schema Wright draft 00JSON Schema draft 2020-12, plus the OpenAPI base vocabulary
Null valuesnullable: truetype: [string, "null"] or a "null" branch in oneOf/anyOf
Exclusive boundsexclusiveMinimum: true next to minimumexclusiveMinimum: 0 (the bound itself)
Schema examplesexample: single valueexamples: array of values (example is deprecated)
$ref siblingsIgnoredAllowed in schemas; summary and description allowed on other references
Binary uploadstype: string, format: binarycontentMediaType, or no schema for a raw body
Base64 stringstype: string, format: bytetype: string, contentEncoding: base64
WebhooksNot available (callbacks only)Top-level webhooks map
Required root fieldsopenapi, info, pathsopenapi, info, and at least one of paths, components, webhooks
Default schema dialectFixedjsonSchemaDialect at the root
info and licensetitle, description, license.urlAdds info.summary and license.identifier (SPDX)
Reusable path itemsNot in componentscomponents.pathItems
Security schemesapiKey, http, oauth2, openIdConnectAdds mutualTLS
Changes from OpenAPI 3.0.x to 3.1.x. Operations, parameters, responses, servers and security requirements keep the same shape.

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.

YAMLorders.openapi31.yaml
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]
jsonSchemaDialect sets the default $schema for every Schema Object. The value shown is the OpenAPI default, so you only need the field when you use a different dialect. if/then makes address required only for courier shipments, which 3.0 cannot express.

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.

YAML3.0
nickname:
  type: string
  nullable: true
status:
  type: string
  enum: [active, paused]
  nullable: true
OpenAPI 3.0: nullable on the schema.
YAML3.1
nickname:
  type: [string, "null"]
status:
  type: [string, "null"]
  enum: [active, paused, null]
OpenAPI 3.1: "null" in the type array. Quote it in YAML, or the parser reads a real null value.

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:

YAML3.1
billingAddress:
  oneOf:
    - $ref: "#/components/schemas/Address"
    - type: "null"
A property that is either an Address object or 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.

YAML3.0
quantity:
  type: integer
  minimum: 0
  exclusiveMinimum: true
discount:
  type: number
  maximum: 1
  exclusiveMaximum: true
YAML3.1
quantity:
  type: integer
  exclusiveMinimum: 0
discount:
  type: number
  exclusiveMaximum: 1
quantity must be greater than 0 and discount less than 1 in both versions.

A 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.

YAML3.0
Order:
  type: object
  properties:
    id:
      type: string
      example: ord_8f2k1
    total:
      type: number
      example: 49.5
YAML3.1
Order:
  type: object
  properties:
    id:
      type: string
      examples: [ord_8f2k1, ord_77xq0]
    total:
      type: number
      examples: [49.5]
Each value in the array is a complete example of that schema.

$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.

YAML3.0
customer:
  allOf:
    - $ref: "#/components/schemas/Customer"
  description: The customer who placed the order.
The allOf wrapper exists only so the description is not ignored.
YAML3.1
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.

YAML3.0
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: byte
YAML3.1
requestBody:
  content:
    application/octet-stream: {}
    multipart/form-data:
      schema:
        type: object
        properties:
          avatar:
            type: string
            contentMediaType: image/png
          thumbnail:
            type: string
            contentEncoding: base64
An empty Media Type Object accepts any bytes. contentMediaType names the file type of a multipart part.

New 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.
YAMLwebhooks.openapi31.yaml
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 event
A 3.1 document can describe outgoing webhooks with or without any paths.

Should 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?

ScenarioRecommendationAlternative
Every tool in your pipeline documents 3.1 supportUpgrade. 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.0Stay 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 webhooksUse 3.1. The webhooks field exists only in 3.1 and later.—
You share schemas between OpenAPI and a JSON Schema validator in your backendUse 3.1, so both read the same draft 2020-12 schemas without an adapter.—
Your framework generates the spec from codeUse 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.

Frequently asked questions

No. Boolean exclusiveMinimum and exclusiveMaximum fail 3.1 validation, and nullable is ignored, so fields stop accepting null. The structure of paths, operations and components is the same, so most files need only schema edits plus the new version string.

Add "null" to the type array, as in type: [string, "null"]. For a $ref, use oneOf with the reference and type: "null". If the schema has an enum, add null to the enum list too.

Inside a Schema Object, yes: use examples, which takes an array. The example field and examples map on parameters and media types are unchanged.

Start on 3.1 if your docs renderer, code generator and gateway support it. If any of them only reads 3.0, write 3.0.3 or 3.0.4 and convert later with the checklist in this guide.

Sources

  1. OpenAPI Specification 3.1.1OpenAPI Initiative
  2. OpenAPI Specification 3.0.4OpenAPI Initiative
  3. Upgrading from OpenAPI 3.0 to 3.1OpenAPI Initiative
  4. JSON Schema draft 2020-12JSON Schema
  5. OpenAPI Specification 3.2.0OpenAPI Initiative