Migrating from OpenAPI 3.0 to 3.1 and later

OpenAPI 3.1 rewrites the shape of several common constructs without changing what they mean on the wire. A naive diff tool reads the rewrites as schema changes and floods you with false positives. oasdiff produces the migrated spec in one command and, in CI, tells the dialect rewrites apart from real breaking changes automatically.

What actually changes between 3.0 and modern 3.x

3.03.1+
nullable: truetype: ["string", "null"]
exclusiveMinimum: true + minimum: 0exclusiveMinimum: 0 (numeric)
example: 7examples: [7]
JSON Schema draft-04-ishJSON Schema 2020-12 (fully)
no pathItems in componentspathItems allowed in components
no webhooksfirst-class webhooks

The first three are the ones that bite during a diff: they look like schema changes to a naive diff tool, but they're equivalent on the wire. A 3.0 client sending the same request to a 3.1+ schema works unchanged.

Producing the migrated spec

From the CLI:

oasdiff upgrade old-spec.yaml > new-spec.yaml

The transforms are semantic-preserving and idempotent: running upgrade on a spec that's already on a modern 3.x is a no-op aside from a possible version-string bump. See the note on 3.2 below for how to pin the output to a specific 3.x.

Try it

Specs are transformed and returned in the same request; nothing is stored server-side.

In CI: the migration PR

The migration PR is sensitive. You want the same audit trail every other PR gets: a clean run if nothing broke, an explicit list if something did. The default breaking-change check can't give you either, because it reads every dialect rewrite (nullable → type array, exclusiveMinimum numeric, exampleexamples) as a schema change. Real findings drown in dialect noise.

Set auto-upgrade: true in an .oasdiff.yaml next to your spec. oasdiff canonicalises both base and revision to the latest 3.x before diffing, so the dialect rewrites disappear and only genuine schema-level changes remain.

# .oasdiff.yaml
fail-on: ERR
auto-upgrade: true

Every oasdiff GitHub Action (free breaking, changelog, diff, and Pro pr-comment) picks the flag up automatically:

# .github/workflows/oasdiff.yaml
- uses: oasdiff/oasdiff-action/breaking@v0
  with:
    base: 'origin/${{ github.base_ref }}:openapi.yaml'
    revision: 'HEAD:openapi.yaml'

Keep the migration PR clean

We recommend the migration PR contain only the dialect bump, nothing else. Schema changes are easier to review when they don't share a PR with this kind of edit. If something does slip in, oasdiff catches it: auto-upgrade filters the dialect rewrites, not the real changes underneath.

Once the migration ships and the spec is canonically 3.1+, the flag is a no-op on every subsequent PR. Safe to leave on permanently or to remove, same outcome.


Why the target is 3.2 (and how to pin to 3.1)

By default, oasdiff upgrade emits the latest 3.x version it knows about (currently 3.2.0). The OpenAPI Initiative guarantees strict compatibility within 3.x going forward (3.2.x, 3.3.x, ...), so a consumer that correctly reads 3.1 reads 3.2 too. Skipping intermediate versions means one migration covers the next few years.

The structural rewrites upgrade applies (nullable → type array, exclusiveMinimum numeric, example examples, JSON Schema 2020-12 keywords) are the same regardless of which 3.x version the output is tagged as. The only difference is the openapi: field.

What's new in OpenAPI 3.2

upgrade never adds anything only 3.2 can express. It applies the dialect rewrites above and sets the version line, so what it writes is a 3.1-shaped document tagged 3.2. The tag still means something, though: 3.2 adds constructs 3.1 has no way to express, and being tagged 3.2 makes them available to you, none of them required:

New in 3.2What it lets you describe
query and additionalOperationsOperations under the QUERY method, or under any custom HTTP method your API uses
itemSchemaStreaming and sequential responses: server-sent events, JSON Lines, JSON text sequences, multipart
in: querystringThe whole query string as a single value, for APIs whose query syntax a per-parameter model cannot capture
$selfThe document's own URI, so relative $refs across a multi-file spec resolve predictably
Tag parent, kind, summaryNested tags, so large APIs get a real navigation hierarchy instead of one flat list
XML nodeTypeElement, attribute, text or CDATA directly, replacing the attribute and wrapped booleans
dataValue and serializedValueAn example's structured value and its serialized form, separately
deviceAuthorizationUrlThe OAuth 2.0 device authorization flow

oasdiff reads 3.2 documents, and support for the new constructs is arriving one at a time, so oasdiff validate may still report the newest of them as unrecognised fields. That affects specs already using 3.2 features, not the migration itself: upgrade rewrites schema constructs and the version string, and never introduces any of the above.

When this matters

If a downstream tool (codegen, validator, gateway) does a strict equality check on openapi: 3.1.0, it rejects a 3.2.0 spec even though every keyword behaves identically. The fix lives in that tool: modern 3.x consumers should accept the 3.x range, and most maintainers will treat it as a bug if you file one.

If you need a stopgap until the upstream fix lands, the version string is a single line in the output:

oasdiff upgrade old-spec.yaml | sed 's/^openapi: 3.2.0$/openapi: 3.1.0/'

That's it. Worth filing the upstream bug at the same time so the stopgap doesn't become permanent.