Shotgun — make one API answer to another API's shape
OpenAPI → OpenAPI translation proxy<br>Turn any REST API into the shape of another.
Shotgun is a reverse proxy generator for OpenAPI specs.
Get started
$cargo install shotgun-proxycopy
OpenAPI 3.0 / 3.1 + Swagger 2.0<br>Deterministic matching<br>Rust + axum
The idea
APIs in the same domain already share most of their shape.
Git forges, payment processors, and CMS platforms usually share 60 to 80 percent of their structure.<br>Shotgun maps the obvious parts and leaves you a checklist for the rest.
01<br>Client thinks it's calling
GET /repos/{owner}/{repo}/issues/{issue_number}
gh, Renovate, and CI scripts run unmodified.
──▶
02<br>Shotgun rewrites
path_params · renames · defaults · drops
Driven by mappings.toml. Nothing guessed at runtime.
──▶
03<br>Upstream gets
GET /api/v1/repos/{owner}/{repo}/issues/{index}
The response comes back in the shape the client expected.
Quick start
Two commands and one edit get you a proxy.
Diff, review, serve. When a spec changes, re-diff without losing your edits.
01
Diff the specs
shotgun init matches endpoints and fields, then writes a mappings.toml. It leaves blank anything it can't resolve.
02
Fill in the gaps
Anything left as target = "" is your todo list. Mark what you touch edited = true to protect it from sync.
03
Serve it
Point --target-url at the upstream. Clients keep speaking the source API's shape.
terminal<br>copy
# 1. Diff two specs and generate a mapping file<br>shotgun init --source github-api.json --target forgejo-api.json --output mappings.toml
# 2. Review mappings.toml, fill in anything left as target = ""
# 3. Run the proxy<br>shotgun serve --mappings mappings.toml --target-url https://forgejo.example.com
# 4. Clients call Shotgun in the GitHub shape; it translates on the fly<br>curl http://localhost:8080/repos/owner/repo
# When specs change, re-diff without losing your edits.<br>shotgun sync --source github-api-v2.json --target forgejo-api-v2.json --mappings mappings.toml
# Check a mapping file for problems<br>shotgun validate --mappings mappings.toml
Matching<br>Shotgun only maps what it can prove.
It guesses nothing at request time. When it can't prove a match, it<br>leaves the entry unmapped and says so.
/Endpoints
Shotgun matches on path and HTTP method, ignoring {param} names. If that misses, it tries operationId. Anything left over stays unmapped.
=Fields
A field maps when the name matches exactly and the type fits. If the types clash, Shotgun flags it and forces nothing.
↺Renames
Shotgun never writes one. You write every rename, then mark it edited = true so sync leaves it alone.
+Defaults and drops
A field that only one side has becomes a default holding a zero value, or a drop that clients never see.
⌥Nested schemas
Shotgun diffs a User inside a Repository once. It reuses that map every time the type shows up, through a named [[schemas]] entry.
»Pagination
Shotgun remaps query params between the two styles. It also rewrites Link header URLs to point back at the proxy.
mappings.toml<br>One file you can read in a diff.
The whole translation lives in one TOML file. You can review it, version it, and mark<br>entries edited = true so a later sync skips them.
[settings]<br># prepended to every target path<br>target_base_path = "/api/v1"<br># stripped from the request before matching<br>source_base_path = "/api/v3"<br># "reject" (501) or "passthrough"<br>unmapped_endpoint_behavior = "reject"<br># "passthrough" / "drop" / "drop_unknown"<br>unmapped_field_behavior = "passthrough"
[settings.pagination]<br>source_style = "link_header"<br>target_style = "link_header"<br>rewrite_link_urls = true
[settings.pagination.param_map]<br>per_page = "limit"
[[endpoints]]<br>source = "GET /repos/{owner}/{repo}/issues/{issue_number}"<br>target = "GET /repos/{owner}/{repo}/issues/{index}"<br>edited = true # sync will not overwrite this
[endpoints.path_params]<br>issue_number = "index"
[endpoints.response.renames]<br>number = "index" # humans write these, never the diff
[endpoints.response.defaults]<br>draft = false # source-only field, synthesized<br>labels = []
[endpoints.response]<br>drops = ["due_date"] # target-only, hidden from clients
[[endpoints.response.nested]]<br>path = "user"<br>schema_map = "User" # reuse a [[schemas]] entry
[[schemas]]<br>name = "User"<br>edited = true
[schemas.renames]<br>login = "username"
[schemas.defaults]<br>site_admin = false
# No target equivalent exists; returns 501 at runtime<br>[[endpoints]]<br>source = "POST /repos/{owner}/{repo}/issues/{issue_number}/lock"<br>target = ""<br>note = "Forgejo has no issue locking API"
# Same name, incompatible types; flagged, left untouched<br>[endpoints.response.type_conflicts]<br>reactions = "object vs array"
Flagship use case
Anvil points GitHub tooling at Forgejo.
Anvil is a curated mappings.toml on top of this proxy. It lets GitHub tools like<br>gh, Renovate, and CI scripts talk to a self-hosted Forgejo instance with no<br>client-side changes.
It drives Shotgun's work against real specs, not toy ones.
View Anvil...