|
| 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. |
0 commit comments