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.
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.
| Area | OpenAPI 3.0 | OpenAPI 3.1 |
|---|---|---|
| Nullable | type: string plus nullable: true | type: [string, "null"] |
| Exclusive bounds | minimum: 0 plus exclusiveMinimum: true | exclusiveMinimum: 0 |
| Schema examples | example: pay_123 (single value) | examples: [pay_123] (array); example deprecated |
| Binary upload | format: binary | contentMediaType, or an empty schema for raw bodies |
| Base64 content | format: base64 | contentEncoding: base64 |
$ref with siblings | Siblings ignored | Allowed in schemas; Reference Objects may override summary and description |
| Required top-level | paths required | At least one of paths, components, or webhooks |
| Webhooks | Only callbacks tied to an operation | Top-level webhooks map of Path Items |
| Info | title, description, version | Adds info.summary |
| License | name, url | Adds identifier, an SPDX expression, mutually exclusive with url |
| Components | No reusable path items | Adds components.pathItems |
| Schema dialect | Fixed | jsonSchemaDialect at the root, $schema per schema |
| Security schemes | apiKey, http, oauth2, openIdConnect | Adds 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.
enumwithnull. JSON Schema checksenumindependently oftype. If the converter writestype: [string, "null"]but leaves the enum as[card_declined, insufficient_funds], a nullfailureCodenow fails validation. Every successful payment fails a strict validator.exclusiveMinimum: 0. In 3.0,exclusiveMinimum: truewas a modifier onminimum. 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.- Quoting
"null". In YAML, a barenullinside the type array can be parsed as an actual null value rather than the type name. Quote it. In the enum, a barenullis 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.
| Feature | Field | What it lets a contract say |
|---|---|---|
| Hierarchical tags | Tag Object summary, parent, kind | Refunds nest under Payments in the docs; kind such as nav, badge, or audience classifies tags |
| QUERY method | Path Item query | A safe, idempotent request with a body, for complex searches |
| Other methods | Path Item additionalOperations | Methods with no fixed field, keyed by the exact method name |
| Whole query string | Parameter in: querystring | The entire query string as one value described with content |
| Streaming | Media Type itemSchema | The schema of each item in application/jsonl, application/x-ndjson, application/json-seq, text/event-stream, or multipart/mixed |
| Multipart by position | Media Type prefixEncoding, itemEncoding | Encoding for multipart parts by position |
| OAuth device flow | OAuth Flows deviceAuthorization, deviceAuthorizationUrl | Sign-in on devices without a browser, such as a payment terminal |
| OAuth metadata | Security Scheme oauth2MetadataUrl | Where the RFC 8414 authorization server metadata lives |
| Retiring auth | Security Scheme deprecated | Marks a scheme consumers should stop using |
| Document identity | Root $self | The document’s own URI, used as its base URI for references |
| Server naming | Server name | A unique name for each host |
| Examples | Example dataValue, serializedValue | The in-memory value and the exact wire form, separately |
| Responses | Response summary | A short summary per response |
| Polymorphism | Discriminator defaultMapping | The schema to use when the discriminator is missing or unknown |
| XML | XML nodeType | Element, attribute, text, cdata, or none |
| Reuse | Components mediaTypes | Reusable 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:
| Tool | What its own docs or releases say |
|---|---|
| OpenAPI Generator | README lists spec compatibility as “3.0, 3.1 (beta support)“ |
| Spectral | README lists OpenAPI v3.2 among ready-to-use rulesets; release v6.17.0 (1 October 2026) fixed OAS 3.2 schema validation |
| Redocly | States full OpenAPI 3.2 support for linting, rendering, mock server, and Respect (March 2026 post) |
| Swagger UI | Open 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: truenow a type array including"null", with"null"quoted? - Does every nullable field with an
enumincludenullin the enum? - Are exclusive bounds numbers, and do they still express the same rule (zero amounts rejected, for example)?
- Were
examplevalues converted toexamplesarrays in schemas, and do they still validate against the schema? - Are file and binary fields expressed with
contentMediaTypeandcontentEncodingrather thanformat? - Did any
$refgain 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.
Related articles
- Reading an API Contract: OpenAPI Without a Developer How an analyst reads an API contract: endpoints, methods, request and response schemas, status codes, and OpenAPI structure, without asking a developer.
- API Design Review: The Analyst's Checklist Before the Contract Is Frozen How analysts review an API design before build: domain naming, state changes, money and dates, error model, pagination, idempotency, and a worked review.
- API Versioning and Breaking Changes: How Analysts Assess Impact Before a Release What counts as a breaking API change, versioning strategies, Deprecation and Sunset headers, detecting breaks with oasdiff, and consumer impact assessment.
- Contract Testing: Catch Breaking Changes Before They Ship What contract testing is, how it differs from integration testing, and how consumer-driven contracts catch breaking API and event changes before production.
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.