>_ Analyst Engineering

OpenAPI 3.1 and 3.2 for Analysts: What Changed and What to Check on Upgrade

Written by Ahmed at Analyst Engineering, a Senior Technical Business Analyst with 10+ years in banking and payments delivery.

Cover for OpenAPI 3.1 and 3.2 for analysts, showing nullable replaced by a type array, the webhooks object, and the 3.2 QUERY method and itemSchema.

Key takeaways

  • OpenAPI 3.1 made the Schema Object a superset of JSON Schema Draft 2020-12, which is why nullable: true disappeared and a nullable string is now written as type: [string, "null"].
  • The 3.0 to 3.1 upgrade is breaking only inside schemas: nullable, boolean exclusiveMinimum and exclusiveMaximum, the singular example keyword, and file upload formats all change meaning or form.
  • OpenAPI 3.2.0, dated 19 September 2025 in the specification's revision history, is backward compatible with 3.1: every valid 3.1 document is valid 3.2 after changing the version number.
  • OpenAPI 3.2 adds hierarchical tags with parent and kind, the QUERY method, additionalOperations for other HTTP methods, an in: querystring parameter, itemSchema for streamed media types such as application/jsonl and text/event-stream, the OAuth device authorization flow, oauth2MetadataUrl, and $self.
  • A contract version upgrade is a tooling event before it is a design event: a validator or code generator that only understands 3.0 can silently drop the null in a type array and generate a client that rejects valid responses.

OpenAPI 3.1 made schemas real JSON Schema Draft 2020-12, so nullable: true became type: [string, "null"], exclusive bounds became numbers, and example gave way to examples. It also added a top-level webhooks object. OpenAPI 3.2, dated 19 September 2025, is backward compatible with 3.1 and adds hierarchical tags, the QUERY method, streaming media types with itemSchema, a querystring parameter, and the OAuth device flow. When a provider moves its contract to either version, the analyst’s job is to check that every tool downstream still reads it the same way.

The specification text is the source for every field named here: spec.openapis.org for 3.2.0, and the 3.1 and 3.0 texts linked from the same site. This sits in the advanced track of APIs for Analysts, after you can read a contract comfortably. If you cannot yet, start with reading an API contract and the first time I read an OpenAPI contract.

APIs for Analysts, advanced track. Builds on API design review and API versioning and breaking changes. Full learning path: APIs for Analysts.

Why does the OpenAPI version line matter to an analyst?

Because the first line of the contract, openapi: 3.0.3 or openapi: 3.1.1 or openapi: 3.2.0, decides how every schema below it is interpreted. The same YAML can mean different things under different versions, and the tools your team relies on read that line to decide which rules to apply.

Real providers sit on different versions today. Stripe’s public OpenAPI file for API version 2026-09-30.endive still declares openapi: 3.0.0 and uses nullable: true. GitHub publishes its REST API description in two folders: descriptions holds the 3.0 version and descriptions-next holds the 3.1 version. So in one integration programme you can easily be reading a 3.0 contract from one provider and a 3.1 contract from another, and the word “nullable” means different syntax in each.

I have seen a 3.0 to 3.1 upgrade go wrong in exactly one way, repeatedly: the contract was converted correctly, and a code generator two steps downstream did not understand type arrays. It generated a client where an optional, nullable failureCode became a required string, and every successful payment (where failureCode is null) failed deserialization in the consumer. Nothing in the contract diff looked dangerous.

What changed from OpenAPI 3.0 to 3.1?

The headline is JSON Schema alignment. In 3.1 the Schema Object is a superset of JSON Schema Draft 2020-12, and the OpenAPI Initiative’s own upgrade guide says the breaking changes are confined to the Schema Object. Everything else in 3.1 is additive.

AreaOpenAPI 3.0OpenAPI 3.1
Nullabletype: string plus nullable: truetype: [string, "null"]
Exclusive boundsminimum: 0 plus exclusiveMinimum: trueexclusiveMinimum: 0
Schema examplesexample: pay_123 (single value)examples: [pay_123] (array); example deprecated
Binary uploadformat: binarycontentMediaType, or an empty schema for raw bodies
Base64 contentformat: base64contentEncoding: base64
$ref with siblingsSiblings ignoredAllowed in schemas; Reference Objects may override summary and description
Required top-levelpaths requiredAt least one of paths, components, or webhooks
WebhooksOnly callbacks tied to an operationTop-level webhooks map of Path Items
Infotitle, description, versionAdds info.summary
Licensename, urlAdds identifier, an SPDX expression, mutually exclusive with url
ComponentsNo reusable path itemsAdds components.pathItems
Schema dialectFixedjsonSchemaDialect at the root, $schema per schema
Security schemesapiKey, http, oauth2, openIdConnectAdds mutualTLS

Two of these deserve an analyst’s attention more than the others.

The webhooks object. In 3.0, you could only describe an outbound call as a callback attached to an operation, such as “after you POST a subscription, we will call your URL”. Many providers send events that are not tied to any single request, so they documented webhooks in prose. In 3.1, webhooks is a top-level map: each entry is a Path Item that describes the request the provider will send to you. That makes webhook payloads contract material you can lint and test, which is the gap described in webhooks for analysts.

The two meanings of examples. Inside a Schema Object, examples is the JSON Schema keyword: an array of values. Inside a Media Type Object or a Parameter, examples is still a map of named Example Objects. Same word, two shapes, and the most common review comment I write on converted contracts.

What does a 3.0 to 3.1 upgrade look like on a payment schema?

Here is the diff on a realistic Payment schema, with an ISO 20022 endToEndId (Max35Text) that is null until the payment reaches the scheme.

-openapi: 3.0.3
+openapi: 3.1.1
 info:
   title: Merchant Payments API
+  summary: Create, capture, and refund card and SEPA payments.
   version: 2.4.0
   license:
     name: Apache 2.0
-    url: https://www.apache.org/licenses/LICENSE-2.0
+    identifier: Apache-2.0
 components:
   schemas:
     Payment:
       type: object
       required: [id, amount, currency, status]
       properties:
         id:
           type: string
-          example: pay_8f3k2LmQ9xT4vB1c
+          examples: [pay_8f3k2LmQ9xT4vB1c]
         amount:
           type: integer
           description: Minor units. 1250 means 12.50.
-          minimum: 0
-          exclusiveMinimum: true
+          exclusiveMinimum: 0
         currency:
           type: string
           enum: [EUR, GBP, USD]
         status:
           type: string
           enum: [pending, succeeded, failed]
         failureCode:
-          type: string
-          nullable: true
-          enum: [card_declined, insufficient_funds]
+          type: [string, "null"]
+          enum: [card_declined, insufficient_funds, null]
         endToEndId:
-          type: string
-          nullable: true
+          type: [string, "null"]
           maxLength: 35
+webhooks:
+  paymentSettled:
+    post:
+      summary: Sent to the merchant when a payment settles.
+      requestBody:
+        required: true
+        content:
+          application/json:
+            schema:
+              $ref: '#/components/schemas/PaymentSettledEvent'
+      responses:
+        '200':
+          description: Acknowledge within 10 seconds or the event is retried.

Three lines in that diff are where real defects come from.

  1. enum with null. JSON Schema checks enum independently of type. If the converter writes type: [string, "null"] but leaves the enum as [card_declined, insufficient_funds], a null failureCode now fails validation. Every successful payment fails a strict validator.
  2. exclusiveMinimum: 0. In 3.0, exclusiveMinimum: true was a modifier on minimum. In 3.1 it is a number. A tool reading 3.1 with 3.0 rules sees a number where it expected a boolean and either errors or ignores the bound, so a zero-amount payment stops being rejected in your mock server.
  3. Quoting "null". In YAML, a bare null inside the type array can be parsed as an actual null value rather than the type name. Quote it. In the enum, a bare null is correct, because there you mean the null value.

What is new in OpenAPI 3.2?

OpenAPI 3.2.0 is dated 19 September 2025 in the specification’s revision history; the OpenAPI Initiative announced it on 23 September 2025. The official upgrade guide says 3.2 introduces no breaking changes: a 3.1 document becomes a valid 3.2 document by changing the version number. Every addition is opt-in. The current text is 3.2.1, a patch release dated 10 September 2026; under the OpenAPI versioning policy a patch release clarifies and corrects the specification without adding features, so openapi: 3.2.0 and openapi: 3.2.1 describe the same feature set.

FeatureFieldWhat it lets a contract say
Hierarchical tagsTag Object summary, parent, kindRefunds nest under Payments in the docs; kind such as nav, badge, or audience classifies tags
QUERY methodPath Item queryA safe, idempotent request with a body, for complex searches
Other methodsPath Item additionalOperationsMethods with no fixed field, keyed by the exact method name
Whole query stringParameter in: querystringThe entire query string as one value described with content
StreamingMedia Type itemSchemaThe schema of each item in application/jsonl, application/x-ndjson, application/json-seq, text/event-stream, or multipart/mixed
Multipart by positionMedia Type prefixEncoding, itemEncodingEncoding for multipart parts by position
OAuth device flowOAuth Flows deviceAuthorization, deviceAuthorizationUrlSign-in on devices without a browser, such as a payment terminal
OAuth metadataSecurity Scheme oauth2MetadataUrlWhere the RFC 8414 authorization server metadata lives
Retiring authSecurity Scheme deprecatedMarks a scheme consumers should stop using
Document identityRoot $selfThe document’s own URI, used as its base URI for references
Server namingServer nameA unique name for each host
ExamplesExample dataValue, serializedValueThe in-memory value and the exact wire form, separately
ResponsesResponse summaryA short summary per response
PolymorphismDiscriminator defaultMappingThe schema to use when the discriminator is missing or unknown
XMLXML nodeTypeElement, attribute, text, cdata, or none
ReuseComponents mediaTypesReusable Media Type Objects

Here is what the payment API gains by using a few of them:

openapi: 3.2.0
$self: https://api.example.com/openapi/payments.yaml
info:
  title: Merchant Payments API
  version: 2.5.0
tags:
  - name: payments
    summary: Payments
    kind: nav
  - name: refunds
    summary: Refunds
    parent: payments
    kind: nav
paths:
  /payments/search:
    query:
      operationId: searchPayments
      tags: [payments]
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/PaymentSearch'
      responses:
        '200':
          description: Matching payments.
  /payments/export:
    get:
      operationId: exportPayments
      tags: [payments]
      responses:
        '200':
          description: One payment per line, streamed until the export completes.
          content:
            application/jsonl:
              itemSchema:
                $ref: '#/components/schemas/Payment'
components:
  securitySchemes:
    terminalAuth:
      type: oauth2
      oauth2MetadataUrl: https://auth.example.com/.well-known/oauth-authorization-server
      flows:
        deviceAuthorization:
          deviceAuthorizationUrl: https://auth.example.com/oauth/device
          tokenUrl: https://auth.example.com/oauth/token
          scopes:
            payments:read: Read payments

Two of these change how you write tests, not just docs.

itemSchema makes streams testable. Before 3.2, a reconciliation export served as JSON Lines had no way to say what each line looked like; the contract described the response as a string. With itemSchema, every line is validated against Payment, so a test can assert that line 40,000 is as well-formed as line 1. In payments, a truncated or malformed line in the middle of a settlement export is the defect that costs a day of reconciliation.

QUERY separates “search” from “create”. Teams have long used POST /payments/search for searches with large filters, which makes a read look like a write to every gateway, cache, and retry policy in the path. The QUERY method, still an IETF draft (draft-ietf-httpbis-safe-method-w-body) when 3.2 shipped, is defined as safe and idempotent. For an analyst, the review question is whether your gateway, WAF, and client libraries actually pass QUERY through, because a contract can describe a method the infrastructure rejects.

Which tools lag behind, and how do you check?

A contract version upgrade is a tooling event before it is a design event. Current as of October 2026, from each project’s own documentation and release notes:

ToolWhat its own docs or releases say
OpenAPI GeneratorREADME lists spec compatibility as “3.0, 3.1 (beta support)“
SpectralREADME lists OpenAPI v3.2 among ready-to-use rulesets; release v6.17.0 (1 October 2026) fixed OAS 3.2 schema validation
RedoclyStates full OpenAPI 3.2 support for linting, rendering, mock server, and Respect (March 2026 post)
Swagger UIOpen issues for rendering 3.2 tag fields (summary, kind, parent) and itemSchema

The pattern that matters: linters move first, renderers next, code generators last. A provider can be on 3.2 while the SDK your developers generate understands 3.1 in beta. So before anyone says “the contract upgraded, nothing changed”, walk the chain:

# 1. Validate the new contract with the linter your pipeline uses
npx @stoplight/spectral-cli lint openapi.yaml

# 2. Diff old against new (confirm your diff tool reads both versions);
#    anything flagged is a conversation, not a pass
oasdiff breaking openapi-3.0.yaml openapi-3.1.yaml

# 3. Regenerate the client and diff the generated models, not just the contract
git diff --stat generated/

Step three is the one teams skip. Diffing generated models after an upgrade is how you catch a nullable field that became required in the client, which no contract diff will show, because the contract is correct. The broader method for detecting breaks with oasdiff and rerunning collections is in API versioning and breaking changes, and the consumer-side guard is contract testing.

Writing a contract that survives version upgrades, with examples and descriptions consumers can act on, is the core of API Documentation from Scratch, and the regression testing behind it is in API Testing and QA Mastery for BAs.

What should an analyst check when a provider upgrades the contract version?

Use this as the review checklist. Each line is a question with a yes or no answer.

Version and tooling

  • Does every tool in our chain (linter, mock server, docs renderer, code generator, contract test runner, API gateway import) support the new version, confirmed from its release notes rather than assumed?
  • Have we regenerated clients and diffed the generated models against the previous generation?
  • Is the old contract kept in version control so the diff is reproducible?

Schema conversion (3.0 to 3.1)

  • Is every former nullable: true now a type array including "null", with "null" quoted?
  • Does every nullable field with an enum include null in the enum?
  • Are exclusive bounds numbers, and do they still express the same rule (zero amounts rejected, for example)?
  • Were example values converted to examples arrays in schemas, and do they still validate against the schema?
  • Are file and binary fields expressed with contentMediaType and contentEncoding rather than format?
  • Did any $ref gain sibling keywords that now take effect where they were ignored before?

New capabilities

  • Are webhooks now in webhooks, and does each one have the same payload and acknowledgement rules the prose used to describe?
  • If QUERY is used, does our gateway and client tooling accept the method?
  • If a streamed response declares itemSchema, do our tests validate every item, not only the first?
  • If a security scheme is marked deprecated, do we know the replacement and the date?

Behavior

  • Rerun yesterday’s collection unchanged against the new contract’s mock and sandbox. Any failure is either an intended change or a conversion defect.

These checks map directly onto the Versioning (C10) and Evidence (C12) checks of the Review Scorecard, the twelve-check rubric used in the Stripe PaymentIntents teardown. A provider that upgrades its OpenAPI version without a changelog entry is a C10 finding on its own. To practise reading a 3.1 contract end to end, the free Analyze the Payments API mission uses a 3.1 file with a webhooks section.

When should you not upgrade?

Upgrading your own contract is not free, and “the latest version” is not a requirement. Stay on 3.0 when a code generator your consumers depend on only has beta support for 3.1, and nothing in 3.1 solves a problem you have. Move to 3.1 when you need webhooks as contract material, real JSON Schema features such as const, if/then/else, or unevaluatedProperties, or a single schema you can share with a JSON Schema validator elsewhere in the estate. Move to 3.2 when you have a stream, a QUERY search, or a device-flow client to describe, and your renderer and linter have confirmed support.

For a team that both publishes and consumes, I recommend writing the decision down in one line in the API’s README: which OpenAPI version, why, and which tool versions were confirmed. That line ends a recurring argument.

The takeaway

OpenAPI 3.1 turned schemas into JSON Schema Draft 2020-12, which is why nullable became a type array, exclusive bounds became numbers, and examples became an array; it also made webhooks first-class contract material. OpenAPI 3.2 is a backward-compatible minor release that adds hierarchical tags, QUERY, additionalOperations, querystring, itemSchema for streams, the OAuth device flow, oauth2MetadataUrl, and $self. The risk in either upgrade sits in tools, not in the specification: check every linter, renderer, and generator against its release notes, convert nullable enums with null included, diff the generated code as well as the contract, and rerun the old collection before anyone calls the upgrade safe.

Ahmed is a Senior Technical Business Analyst with 10+ years in banking and payments. He builds practical guides and tools for analysts at The Tech BA Toolkit.

Tags: OpenAPI, OpenAPI 3.1, OpenAPI 3.2, API Contracts, JSON Schema, Systems Analysis

About the author

Analyst Engineering is written by Ahmed, a Senior Technical Business Analyst with 10+ years of banking and payments delivery experience: ISO 20022 and SWIFT messaging, payments API integration, Kafka event validation, and production support. Every article comes from real delivery work, and each one is reviewed and updated as tools and standards change.

Go deeper on this

Not ready to buy? The free downloads are a no-cost place to start, and every article here stays free.

Free account

Practice on the Labs, keep your progress

A free account, no password: an email link signs you in. It saves your steps and self-assessments on the Labs, shows your missions on a dashboard, unlocks the solutions, and, if you tick the box, sends you new missions and articles when they ship.

Your email is used to sign you in. Nothing else, unless you ask. Privacy.