The structural shift: from paths to requests
In OpenAPI 3.1, the top-level structure revolves around paths:
# OpenAPI 3.1, path-centric
openapi: "3.1.0"
paths:
/users/{userId}:
get:
operationId: getUser
parameters:
- name: userId
in: path
required: true
schema:
type: string
responses:
"200":
content:
application/json:
schema:
$ref: "#/components/schemas/User"
OpenAPI 4.0 reorganizes around requests and pathItems:
# OpenAPI 4.0, request-centric
openapi: "4.0.0"
info:
title: User Service
version: "2.0"
pathItems:
user:
parameters:
userId:
in: path
schema:
type: string
requests:
getUser:
method: get
outcomes:
success:
status: 200
contentType: application/json
body:
schema:
$ref: "#/components/schemas/User"
notFound:
status: 404
contentType: application/problem+json
body:
schema:
$ref: "#/components/schemas/Problem"
urls:
/users/{userId}: { $ref: "#/pathItems/user" }
/v2/users/{userId}: { $ref: "#/pathItems/user" }
Notice several things. First, pathItems are decoupled from URLs. The same user pathItem serves both /users/{userId} and /v2/users/{userId}. In 3.1, you'd duplicate the entire path definition or use $ref gymnastics. Second, responses are now outcomes, named semantic results rather than status-code-indexed entries. success and notFound are more meaningful than "200" and "404". Third, parameters belong to the pathItem, not repeated per operation.
Overlays: layering without forking
This is a long-awaited feature. Overlays let you take a base OpenAPI document and apply modifications without editing the original. Think of it like CSS overrides for your API spec.
# base.yaml, your canonical API spec
openapi: "4.0.0"
info:
title: Payment API
version: "1.0"
pathItems:
charge:
requests:
createCharge:
method: post
body:
schema:
$ref: "#/components/schemas/ChargeRequest"
outcomes:
created:
status: 201
body:
schema:
$ref: "#/components/schemas/Charge"
urls:
/charges: { $ref: "#/pathItems/charge" }
# overlay-staging.yaml, staging environment specifics
overlay: "1.0.0"
info:
title: Staging Overlay
extends: base.yaml
actions:
- target: "$.info"
update:
description: "STAGING ENVIRONMENT, test data only"
- target: "$.servers"
update:
- url: https://staging-api.example.com
description: Staging
- target: "$.pathItems.charge.requests.createCharge"
update:
security:
- apiKey: [] # Simpler auth in staging
# overlay-production.yaml
overlay: "1.0.0"
extends: base.yaml
actions:
- target: "$.servers"
update:
- url: https://api.example.com
description: Production US-East
- url: https://eu-api.example.com
description: Production EU-West
- target: "$.pathItems.charge.requests.createCharge"
update:
security:
- oauth2:
scopes: ["charges:write"]
rateLimit:
limit: 100
window: 60s
Build the final spec for each environment:
# Merge base + overlay
redocly bundle base.yaml --overlay overlay-production.yaml -o production-spec.yaml
redocly bundle base.yaml --overlay overlay-staging.yaml -o staging-spec.yaml
This eliminates the "copy the spec and change the server URL" pattern that plagues every multi-environment API. The base spec is your single source of truth. Overlays adapt it per environment, per partner, per version.
First-class webhooks
In 3.1, webhooks were bolted on as a top-level webhooks field, structurally identical to paths but semantically different. In 4.0, they're integrated into the request/outcome model:
webhooks:
paymentCompleted:
description: Fired when a payment is successfully processed
requests:
notify:
method: post
contentType: application/json
body:
schema:
type: object
required: [event, data]
properties:
event:
type: string
const: payment.completed
data:
$ref: "#/components/schemas/Payment"
timestamp:
type: string
format: date-time
outcomes:
acknowledged:
status: 200
description: Webhook received and acknowledged
retry:
status: [500, 502, 503]
description: Server error, webhook will be retried
delivery:
retryPolicy:
maxRetries: 5
backoff: exponential
initialDelay: 30s
signing:
algorithm: hmac-sha256
headerName: X-Webhook-Signature
The delivery section is new, it documents retry policies and signing mechanisms as part of the spec. In 3.1, this information lived in your docs' prose. Now it's machine-readable, and code generators can produce webhook verification code automatically.
Migration from 3.1: the practical path
A 2,400-line OpenAPI 3.1 spec migration to 4.0 follows this process:
# Step 1: Automated conversion
npx @redocly/cli transform openapi-3.1.yaml --to 4.0 -o openapi-4.0.yaml
# Step 2: Validate the output
npx @redocly/cli lint openapi-4.0.yaml --ruleset recommended
# Step 3: Manual cleanup, name your outcomes semantically
# The converter uses status codes as outcome names; rename them
The automated converter handles ~85% of the transformation. The remaining 15% is:
- Naming outcomes: The converter maps
"200"tooutcome200. Rename tosuccess,created,notFound, etc. - Extracting shared pathItems: If you had duplicated path definitions, consolidate them.
- Webhook delivery metadata: Add retry policies and signing info that didn't exist in 3.1.
- Parameter deduplication: Move shared parameters from operations to pathItems.
The total migration takes about 6 hours for a 2,400-line spec. Most of the time was spent on semantic naming.
Code generation reality check
Here's the honest state of tooling as of January 2026:
| Tool | 4.0 Support | Notes |
|---|---|---|
| Redocly CLI | Full | Linting, bundling, overlays |
| Swagger UI | Partial | Renders but misses outcomes |
| OpenAPI Generator | Beta | Java/Python/TypeScript clients |
| Stoplight | Full | Design-first editor |
| Prism (mock server) | Partial | Basic mocking works |
| Postman | Partial | Import works, some features missing |
If your workflow depends on a specific tool, check its 4.0 support before migrating. The spec is ahead of the tooling ecosystem by roughly 6 months, which is normal for a major version.
Real-world spec design patterns
After extended work with 4.0, these patterns emerge:
Pattern: Shared error outcomes
components:
outcomes:
validationError:
status: 422
contentType: application/problem+json
body:
schema:
$ref: "#/components/schemas/ValidationProblem"
unauthorized:
status: 401
contentType: application/problem+json
body:
schema:
$ref: "#/components/schemas/AuthProblem"
rateLimited:
status: 429
headers:
Retry-After:
schema:
type: integer
pathItems:
charge:
requests:
createCharge:
method: post
outcomes:
created:
status: 201
body:
schema:
$ref: "#/components/schemas/Charge"
validation:
$ref: "#/components/outcomes/validationError"
auth:
$ref: "#/components/outcomes/unauthorized"
throttled:
$ref: "#/components/outcomes/rateLimited"
Shared outcomes eliminate the "copy error responses to every endpoint" problem. Define your error shapes once, reference everywhere.
Should you migrate now?
Honest assessment: for new API projects and your toolchain supports 4.0, use it. The structural improvements (overlays, decoupled pathItems, semantic outcomes) make specs easier to maintain as they grow. If you have a large existing 3.1 spec that works fine, wait 6-12 months for tooling to mature. The migration itself is straightforward but the ecosystem needs to catch up.
The biggest win is overlays. Going from maintaining four copies of a spec (dev, staging, prod, partner) to one base plus four 20-line overlay files represents significant improvement. That alone justifies the migration effort.