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: petstore
path: ./specs/petstore.yaml
additional_blocks:
x-custom-root:
enabled: true
paths:
/pets:
get:
x-custom-operation:
rate_limit: 100
- name: users
path: ./specs/users.json
- name: billing
url: https://billing.example.com/openapi.json
headers:
Authorization: "Bearer token"
output:
format: yaml # yaml | json
merge:
conflict_strategy: error # error | overwrite | rename
prefix_paths: false
info:
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]
async fn main() -> 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]
async fn main() -> 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