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.0.47
  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 the walker 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.

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.