<!-- Canonical: https://lucioduran.com/blog/openapi-4-moonwalk-api-specification
Language: en
Content-SHA256: 2f72d045357720d3158d88c7a6afad1fad1e59507aac948fd7dca367dae0a9ba
-->

API Design

# OpenAPI 4.0 (Moonwalk): The Spec Rewrite That Actually Matters

2025-10-265 min read

Lucio Durán
Engineering Manager & AI Solutions Architect

Also available in: [Español](<https://lucioduran.com/es/blog/openapi-4-moonwalk-api-specification>)

## The structural shift: from paths to requests

In [OpenAPI](<https://www.openapis.org/>) 3.1, the top-level structure revolves around ` paths `:

```yaml
# 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 `:

```yaml
# 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.

```yaml
# 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" }
```

```yaml
# 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
```

```yaml
# 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:

```bash
# 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:

```yaml
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:

```bash
# 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:

1. **Naming outcomes**: The converter maps ` "200" ` to ` outcome200 `. Rename to ` success `, ` created `, ` notFound `, etc.
2. **Extracting shared pathItems**: If you had duplicated path definitions, consolidate them.
3. **Webhook delivery metadata**: Add retry policies and signing info that didn't exist in 3.1.
4. **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**

```yaml
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.

## Frequently asked questions

### What changed from OpenAPI 3.1 to 4.0?

The structure is reorganized around 'requests' and 'outcomes' instead of paths and operations. Webhooks are first-class citizens. Overlays allow layering modifications. URL templates replace path patterns. The component reuse model is simplified.

### Is OpenAPI 4.0 backward compatible with 3.1?

No, it's a structural rewrite. However, automated migration tools (like Redocly and Stoplight converters) handle most of the transformation. The conceptual model is similar enough that migration is mechanical, not creative.

### Should I migrate existing APIs to 4.0?

Not immediately. Wait for tooling maturity, code generators, validators, and documentation renderers need to catch up. For new APIs, 4.0 is worth adopting if your toolchain supports it.

## Tips

1. **Tip:** Start with Overlays before migrating your full spec to 4.0. You can keep your 3.1 source of truth and layer environment-specific details (server URLs, auth schemes) with ` overlay.yaml ` files.
2. **Tip:** Use ` pathItems ` instead of the old ` paths ` object when building shared patterns. A single pathItem definition can be referenced from multiple URL templates, eliminating copy-paste in large APIs.
3. **Tip:** Validate your 4.0 spec with ` @redocly/cli lint ` before trusting code generators. Most generators lag behind the spec by 3-6 months, and invalid input produces subtly broken client code.

- openapi
- moonwalk
- api-design
- api-specification
- webhooks
- swagger
- rest-api
- api-gateway

[Back to Blog](<https://lucioduran.com/blog>)
