openapiv3-merge/README.md
openapiv3-merge combines multiple OpenAPI 3.1 JSON documents into a single
document. It is the companion tool for protoc-gen-openapiv3,
which emits one document per proto file. Pipe its output through
openapiv3-merge to produce a single combined spec.
protoc-gen-openapiv3 follows the protobuf convention of one output file per
input file. That keeps the plugin well-behaved under all protoc/buf
invocations and avoids forcing buf users onto strategy: all. The trade-off
is that consumers who want one document have to combine the per-file outputs
themselves. openapiv3-merge is that step.
go install github.com/grpc-ecosystem/grpc-gateway/v2/openapiv3-merge@latest
openapiv3-merge FILE [FILE ...] > merged.openapi.json
The merged document is written to stdout. Errors go to stderr and the
process exits with a non-zero status. Input order matters: the first
input's info, servers, and externalDocs are kept in the merged
document, so put the file with the broadest scope first.
#!/usr/bin/env bash
set -euo pipefail
buf generate
# Put the file with the shared openapiv3_document annotation first so
# its info/servers/externalDocs are kept; sort the rest for determinism.
root=gen/api/v1/api.openapi.json
mapfile -t rest < <(find gen -name '*.openapi.json' ! -path "$root" | sort)
openapiv3-merge "$root" "${rest[@]}" > gen/api.openapi.json
#!/usr/bin/env bash
set -euo pipefail
protoc -I. \
--openapiv3_out=./gen \
$(find . -name '*.proto')
root=gen/api/v1/api.openapi.json
mapfile -t rest < <(find gen -name '*.openapi.json' ! -path "$root" | sort)
openapiv3-merge "$root" "${rest[@]}" > gen/api.openapi.json
For trivial trees where any input may go first:
openapiv3-merge $(find . -name '*.openapi.json' | sort) > api.openapi.json
The merger is strict: anything that could be a silent overwrite is rejected.
| Field | Rule |
|---|---|
openapi | Must match across all inputs. |
info, servers, externalDocs | Taken from the first input. Later inputs' values are dropped. |
paths, webhooks | Unioned in input order. Same path with non-identical content → error. |
components/* | Unioned with sorted keys. Same name with non-identical content → error. |
tags | Deduplicated by name. Same name with non-identical metadata → error. |
security | First input to declare a non-empty array wins. Later inputs that declare a different non-empty array → error. |
Unknown top-level keys (extensions, x-*) | Taken from the first input. |
"Non-identical content" is compared canonically: two values that differ only
in key order are equal. This means component schemas with the same fully
qualified name (which is what protoc-gen-openapiv3 emits — e.g.
example.v1.User) merge cleanly across packages.
paths and webhooks are emitted in input order.components/* sub-maps are sorted by key, matching protoc-gen-openapiv3.