Aggregate and merge OpenAPI 3.x specifications from multiple sources into a single spec.
Available as both a Rust library and a CLI tool.
- Multiple source types – local YAML files, local JSON files, and HTTP endpoints (with custom headers)
- Config-file driven – define sources and merge options in a single YAML config
- Conflict resolution – choose between
error,overwrite, orrenamestrategies for duplicate paths and component names $refrewriting – when using therenamestrategy,$refpointers are automatically updated- Path prefixing – optionally prefix every path with the source name to guarantee uniqueness
- Info override – set a custom
title,version, anddescriptionin the merged output - Per-source custom blocks – deep-merge arbitrary OpenAPI blocks/extensions from config (provider-agnostic)
curl -sSfL https://raw.githubusercontent.com/includeamin/openapi-aggregator/main/install.sh | shInstall a specific version or to a custom directory:
VERSION=v0.1.0 curl -sSfL https://raw.githubusercontent.com/includeamin/openapi-aggregator/main/install.sh | sh
INSTALL_DIR=/usr/local/bin curl -sSfL https://raw.githubusercontent.com/includeamin/openapi-aggregator/main/install.sh | shUninstall:
curl -sSfL https://raw.githubusercontent.com/includeamin/openapi-aggregator/main/install.sh | sh -s -- --uninstallcargo install --path .Download from GitHub Releases. Binaries are available for Linux (x86_64, aarch64), macOS (x86_64, aarch64), and Windows (x86_64).
# Merge using a config file (defaults to openapi-aggregator.yaml)
openapi-aggregator
# Specify a config file and output location
openapi-aggregator -c my-config.yaml -o merged.yaml
# Output as JSON
openapi-aggregator -c my-config.yaml -f json
# Print help
openapi-aggregator --helpCreate an openapi-aggregator.yaml (see config.example.yaml):
sources:
- name: petstorepath: ./specs/petstore.yamladditional_blocks:
x-custom-root:
enabled: truepaths:
/pets:
get:
x-custom-operation:
rate_limit: 100
- name: userspath: ./specs/users.json
- name: billingurl: https://billing.example.com/openapi.jsonheaders:
Authorization: "Bearer token"output:
format: yaml # yaml | jsonmerge:
conflict_strategy: error # error | overwrite | renameprefix_paths: falseinfo:
title: "My Aggregated API"version: "1.0.0"Sources are detected automatically by their fields:
- If
urlis present → HTTP source - If
pathis present → file source (YAML or JSON auto-detected from content)
Each source can define additional_blocks as any YAML/JSON object. It is deep-merged into that source document before merge, so you can inject vendor extensions (for example API gateway related x-... blocks) or other custom OpenAPI fragments without adding provider-specific fields.
use openapi_aggregator::{aggregate,Config,Source,MergeConfig};#[tokio::main]asyncfnmain() -> Result<(),Box<dyn std::error::Error>>{let config = Config{sources:vec![Source::File{
name:Some("petstore".into()),
path:"./specs/petstore.yaml".into(),
additional_blocks:None,
tag_prefix:None,},Source::Http{
name:Some("billing".into()),
url:"https://billing.example.com/openapi.json".into(),
headers:[("Authorization".into(),"Bearer token".into())].into_iter().collect(),
additional_blocks:None,
tag_prefix:None,},],output:Default::default(),merge:MergeConfig::default(),};let merged = aggregate(&config).await?;println!("{}", serde_json::to_string_pretty(&merged)?);Ok(())}Or load directly from a config file:
use openapi_aggregator::aggregate_from_file;use std::path::Path;#[tokio::main]asyncfnmain() -> Result<(),Box<dyn std::error::Error>>{let merged = aggregate_from_file(Path::new("openapi-aggregator.yaml")).await?;println!("{}", serde_yaml::to_string(&merged)?);Ok(())}| Strategy | Duplicate paths | Duplicate components |
|---|---|---|
error | Fail immediately | Fail immediately |
overwrite | Last source wins | Last source wins |
rename | Prefix path with /{source_name} | Rename to {source_name}_{component} |
When prefix_paths: true, all paths are prefixed regardless of conflicts.
When using rename, any $ref pointing to a renamed component is rewritten automatically.
# Run tests
cargo test# Lint
cargo clippy --all-targets -- -D warnings
# Format
cargo fmt