Skip to content

Commit 4b95068

Browse files
committed
Sync open source content 🐝 (from ad17633f5f85255bb31837f51e0a260ccec99ebc)
1 parent d268e49 commit 4b95068

File tree

3 files changed

+232
-1
lines changed

3 files changed

+232
-1
lines changed

β€Ž_meta.global.tsxβ€Ž

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -48,6 +48,26 @@ const meta = {
4848
},
4949
"prep-openapi": {
5050
title: "Prepare OpenAPI Spec",
51+
items: {
52+
"best-practices": {
53+
title: "Best Practices",
54+
},
55+
linting: {
56+
title: "Linting",
57+
},
58+
merge: {
59+
title: "Merge Documents",
60+
},
61+
overlays: {
62+
title: "Overlays",
63+
},
64+
transformations: {
65+
title: "Transformations",
66+
},
67+
maintenance: {
68+
title: "Maintenance",
69+
},
70+
},
5171
},
5272
customize: {
5373
title: "Customize",
Lines changed: 211 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,211 @@
1+
---
2+
title: "Merge multiple OpenAPI documents"
3+
description: "Combine multiple OpenAPI 3.x documents into a single unified spec for SDK generation using the Speakeasy workflow or CLI."
4+
---
5+
6+
import { Callout } from "@/mdx/components";
7+
8+
# Merge multiple OpenAPI documents
9+
10+
When an API is split across multiple OpenAPI documents, such as separate specs for different microservices or API modules, Speakeasy can merge them into a single unified document. This merged output can then drive SDK generation, documentation, and other downstream workflows.
11+
12+
## Using the CLI
13+
14+
Merge documents directly from the command line:
15+
16+
```bash
17+
speakeasy merge \
18+
-s service-a.yaml \
19+
-s service-b.yaml \
20+
-o merged.yaml
21+
```
22+
23+
The output format is determined by the file extension of the `-o` flag: `.yaml`/`.yml` produces YAML, `.json` produces JSON.
24+
25+
## In workflow files
26+
27+
For repeatable merges tied to SDK generation, define multiple inputs in a source within the `workflow.yaml` file:
28+
29+
```yaml
30+
workflowVersion: 1.0.0
31+
speakeasyVersion: latest
32+
sources:
33+
my-source:
34+
inputs:
35+
- location: ./service-a.yaml
36+
- location: ./service-b.yaml
37+
overlays:
38+
- location: ./overlay.yaml
39+
targets:
40+
my-sdk:
41+
target: typescript
42+
source: my-source
43+
```
44+
45+
When `speakeasy run` executes, the inputs are merged in order, overlays are applied to the merged result, and the final document is passed to each target for generation.
46+
47+
## Merge order matters
48+
49+
Documents are merged **sequentially**. The first document forms the base, then each subsequent document is merged into it in order. For most fields, **last wins**: if two documents define the same field, the value from the later document takes precedence.
50+
51+
Place your primary or most authoritative spec first, then list more specific documents after.
52+
53+
## Namespaces
54+
55+
When merging documents that have overlapping component names, assign a **model namespace** to each input to prevent naming collisions:
56+
57+
```yaml
58+
sources:
59+
my-source:
60+
inputs:
61+
- location: service-a.yaml
62+
modelNamespace: serviceA
63+
- location: service-b.yaml
64+
modelNamespace: serviceB
65+
```
66+
67+
With namespaces, all component names from each document are prefixed. For example, a `User` schema in a document with namespace `serviceA` becomes `serviceA_User` in the merged output. All `$ref` references are updated automatically.
68+
69+
Speakeasy adds two extensions to each namespaced component:
70+
71+
- `x-speakeasy-name-override` preserves the original name for serialization
72+
- `x-speakeasy-model-namespace` records which namespace the component belongs to
73+
74+
After merging, a deduplication pass collapses equivalent namespaced components back to a single entry where possible.
75+
76+
**Namespace rules:**
77+
78+
- Allowed characters: letters, numbers, underscores, hyphens, dots (`[a-zA-Z0-9_\-\.]+`)
79+
- Forward slashes are not allowed
80+
- Either every input must have a namespace, or none of them should
81+
- An empty string namespace skips prefixing for that document's components
82+
83+
<Callout title="Tip" type="info">
84+
For more granular control, apply the `x-speakeasy-model-namespace` extension directly to individual schemas instead of using `modelNamespace` in the workflow file.
85+
</Callout>
86+
87+
## How each section merges
88+
89+
### Info
90+
91+
| Field | Strategy |
92+
| --- | --- |
93+
| `title` | Last wins |
94+
| `version` | Last wins |
95+
| `description` | **Appended** from all documents, deduplicated |
96+
| `summary` | **Appended** from all documents, deduplicated |
97+
| `contact` | Last wins |
98+
| `license` | Last wins |
99+
| `termsOfService` | Last wins |
100+
101+
The OpenAPI version (for example, `3.0.1` vs `3.1.0`) resolves to the **highest version** across all inputs.
102+
103+
### Paths and operations
104+
105+
When two documents define the same path, merging depends on whether HTTP methods conflict:
106+
107+
- **Different methods on the same path** merge together. If doc A defines `GET /users` and doc B defines `POST /users`, the merged spec has both.
108+
- **Same method on the same path with identical content** uses last wins.
109+
- **Same method on the same path with different content** creates **fragment paths**:
110+
111+
```
112+
/users non-conflicting methods stay here
113+
/users#svcA conflicting operations from document A
114+
/users#svcB conflicting operations from document B
115+
```
116+
117+
Without namespaces, the suffix is a numeric counter (`#1`, `#2`).
118+
119+
<Callout title="Note" type="info">
120+
Two operations are considered identical if they are structurally the same after ignoring `description` and `summary` fields. Operations that differ only in descriptions are not treated as a conflict.
121+
</Callout>
122+
123+
### Tags
124+
125+
Tags merge **case-insensitively**. `Pets` and `pets` are treated as the same tag.
126+
127+
| Scenario | Result |
128+
| --- | --- |
129+
| Same name, same content | Last wins |
130+
| Same name (case-insensitive), different content | Both kept, suffixed with namespace or counter |
131+
| Differs only in description/summary | Not a conflict, last description wins |
132+
133+
After merging, all operation-level tag references are normalized to match the casing of the document-level tag definitions.
134+
135+
### Servers
136+
137+
If the incoming document's servers share URLs with the existing merged servers, they merge at the document level. If the incoming servers have **different URLs**, they are moved to **operation-level servers** on the operations from that document. This ensures each operation retains access to the correct server URL.
138+
139+
### Components
140+
141+
All component types merge: schemas, parameters, responses, request bodies, headers, examples, links, callbacks, and path items.
142+
143+
When a component with the same name already exists:
144+
145+
1. The incoming component **replaces** the existing one (last wins)
146+
2. If the components differ structurally (ignoring description/summary), a warning is reported
147+
148+
With namespaces, components are prefixed to avoid collisions entirely.
149+
150+
### Security schemes
151+
152+
Security schemes use **type-aware merging**:
153+
154+
| Scheme type | Mergeable when |
155+
| --- | --- |
156+
| OAuth2 | Same flow types with matching URLs (authorization, token, refresh) |
157+
| HTTP | Same scheme and bearer format |
158+
| API Key | Same name and location (`in`) |
159+
| OpenID Connect | Same `openIdConnectUrl` |
160+
| Mutual TLS | Always mergeable |
161+
162+
When OAuth2 schemes are mergeable, their **scopes are unioned** across all documents. Document-level `security` requirements use last-wins semantics.
163+
164+
### Extensions
165+
166+
Custom extensions (`x-*`) merge recursively. If two documents define different values for the same key, the last value wins and a warning is logged.
167+
168+
### Webhooks
169+
170+
Webhooks from all documents are combined. Conflicting webhook paths follow the same resolution as regular paths.
171+
172+
## OperationID handling
173+
174+
After merging, duplicate `operationId` values are automatically suffixed:
175+
176+
- With namespaces: `listUsers_serviceA`, `listUsers_serviceB`
177+
- Without namespaces: `listUsers_1`, `listUsers_2`
178+
179+
There is no need to manually ensure unique operation IDs across input documents.
180+
181+
## Reference handling
182+
183+
All `$ref` references are updated during the merge to remain valid. This includes schema references, parameter references, nested property references, and security requirement keys. When namespaces are enabled, references are rewritten to point to the prefixed component names.
184+
185+
## Resolving and bundling mode
186+
187+
The `--resolve` flag inlines all local `$ref` references in a single document instead of merging multiple documents:
188+
189+
```bash
190+
speakeasy merge -s spec.yaml -o bundled.yaml --resolve
191+
```
192+
193+
This produces a self-contained spec with all references inlined and unused components removed.
194+
195+
## Tips for better merges
196+
197+
1. **Use namespaces** when merging documents with overlapping component names to prevent silent overwrites.
198+
2. **Put your primary spec first.** Place documents in order of increasing priority since most fields use last-wins.
199+
3. **Keep operation IDs unique across documents** when possible for cleaner output.
200+
4. **Avoid conflicting paths when possible.** Fragment paths (`/users#svcA`) work but may not be supported by all downstream tools outside Speakeasy.
201+
5. **Align tag names and casing** across documents to avoid unnecessary suffixing.
202+
6. **Use overlays for post-merge adjustments.** In a workflow, overlays apply after merging. Use them to clean up or adjust the merged output.
203+
204+
## Caveats and limitations
205+
206+
- **OpenAPI 3.x only.** Swagger 2.0 documents are not supported for merging and will be rejected. Convert them first using `speakeasy openapi transform convert-swagger`.
207+
- **Last-wins can silently overwrite.** Without namespaces, if two documents define a component with the same name but different content, the later one replaces the earlier one with only a warning. Use namespaces to prevent this.
208+
- **Description/summary differences are ignored for conflict detection.** Two operations or components that differ only in descriptions are treated as equivalent. The last document's descriptions win without warning.
209+
- **Fragment paths may not be universally supported.** The `path#suffix` syntax is valid in OpenAPI but may not be handled correctly by tools outside Speakeasy.
210+
- **Server merging can move servers to operation level.** If documents have incompatible global server lists, servers may end up at the operation level in the merged output.
211+
- **Namespace count is all-or-nothing.** You cannot namespace some documents and not others.

β€Ždocs/speakeasy-reference/workflow-file.mdxβ€Ž

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -79,7 +79,7 @@ Each Source is given a name. In this example the name is `my-source`. This name
7979

8080
### Inputs
8181

82-
Each Source has a list of inputs. Each input is an OpenAPI document or Overlay. The OpenAPI documents and Overlays are merged together to create a single OpenAPI document.
82+
Each Source has a list of inputs. Each input is an OpenAPI document or Overlay. The OpenAPI documents and Overlays are merged together to create a single OpenAPI document. For a detailed guide on how merging works, including merge order, conflict resolution, and namespaces, see [Merge multiple OpenAPI documents](/docs/sdks/prep-openapi/merge).
8383

8484
Swagger 2.0 documents are also supported as inputs. When a Swagger 2.0 document is detected, Speakeasy automatically converts it to OpenAPI 3.x before processing. See [Transformations](/docs/sdks/prep-openapi/transformations#convert-swagger-to-openapi) for details.
8585

0 commit comments

Comments
Β (0)