rules_openapi
Bazel rules turning OpenAPI 3 specs into typed code (Rust client via progenitor for v0.1), layered on rules_jsonschema's plugin contract
| Latest | 0.4.0 |
|---|---|
| Versions | 5 |
| Category | Bazel rules |
| Maintainers | Matt Marshall |
| Registry | https://registry.tbzl.dev/modules/rules_openapi/ |
| Source | github.com/tomato-bazel/rules_openapi |
bazel_dep(name = "rules_openapi", version = "0.4.0")
View source & releases on GitHub ↗
Bazel rules that turn an OpenAPI 3 document into typed code, layered
on top of rules_jsonschema’s
plugin contract. The spec is the single source of truth: regenerated
on every build, plugins live in their target language, swappable via
Bazel toolchains.
Status: Rust + Go clients
Ships two paths today: OpenAPI → typed Rust HTTP client via
progenitor, and OpenAPI → typed Go HTTP
client via oapi-codegen. Both
sit behind the same language-neutral plugin contract, swappable via Bazel
toolchains. Server stubs and richer composition with rules_jsonschema for
components/schemas are the next steps.
Architecture
Same shape as rules_jsonschema:
//openapi: language-neutral core
- toolchain_type per (language, use_case) pair
- OpenapiCodegenToolchainInfo provider
- openapi_codegen_toolchain rule (register a plugin)
- openapi_plugin_contract_test rule (verify conformance)
- plugin_contract.md (authoritative spec)
//rust: Rust output
- openapi_rust_client rule
- default toolchain → //tools/openapi_to_rust_client (progenitor-backed)
//go: Go output
- openapi_go_client rule
- default toolchain → //tools/openapi_to_go_client (oapi-codegen-backed)
//tools/openapi_to_rust_client default Rust client plugin
//tools/openapi_to_go_client default Go client plugin
//tools/contract_test contract conformance driver
//openapi/private/extensions.bzl http_file pin for the smoke fixture
The plugin contract is identical in shape to rules_jsonschema’s —
stdin = spec bytes, argv = --key=value, stdout = generated code,
stderr + exit code for errors. The only difference is what gets
shipped on stdin (an OpenAPI document instead of a JSON Schema)
and the per-plugin flags. See
openapi/plugin_contract.md.
Install
.bazelrc:
common --registry=https://registry.tbzl.dev/
common --registry=https://raw.githubusercontent.com/fastverk/bazel-registry/main/
common --registry=https://bcr.bazel.build/
MODULE.bazel:
bazel_dep(name = "rules_openapi", version = "0.4.0")
That’s all a Go consumer needs — openapi_go_client’s toolchain
(oapi-codegen + rules_go) is registered automatically.
Rust consumers (openapi_rust_client) additionally bring the Rust
codegen backend, which is not pulled in transitively as of 0.4.0 (so
Go-only consumers don’t inherit rules_rust):
bazel_dep(name = "rules_rust", version = "0.70.0")
bazel_dep(name = "rules_jsonschema", version = "0.1.0")
rust = use_extension("@rules_rust//rust:extensions.bzl", "rust")
rust.toolchain(edition = "2021", versions = ["1.95.0"])
use_repo(rust, "rust_toolchains")
register_toolchains(
"@rust_toolchains//:all",
"@rules_openapi//rust:default_rust_client_codegen_toolchain",
)
openapi_rust_client
load("@rules_openapi//rust:defs.bzl", "openapi_rust_client")
openapi_rust_client(
name = "petstore_client",
spec = "//path/to:petstore.yaml", # or @hosted//file:foo.yaml
)
Produces a rust_library exporting a Client struct with one method
per OpenAPI operation, plus a types module containing serde structs
for components/schemas. Consumers add it to deps and call methods
directly:
use petstore_client::*;
let client = Client::new("https://api.example.com");
let pet = client.get_pet().pet_id(42).send().await?;
Threading runtime crates
The generated client references progenitor-client, reqwest,
serde, serde_json, regress, chrono, uuid, and bytes from
its surrounding module scope (progenitor emits trait impls
referencing them unconditionally). Defaults point at
rules_openapi’s own @openapi_crates; downstream consumers using
their own crates_universe instance must thread their crate labels
through to avoid the trait-identity mismatch that
rules_jsonschema documents:
openapi_rust_client(
name = "my_client",
spec = "spec.yaml",
progenitor_client = "@my_crates//:progenitor-client",
reqwest = "@my_crates//:reqwest",
serde = "@my_crates//:serde",
serde_json = "@my_crates//:serde_json",
regress = "@my_crates//:regress",
)
openapi_go_client
load("@rules_openapi//go:defs.bzl", "openapi_go_client")
openapi_go_client(
name = "petstore_client",
spec = "//path/to:petstore.yaml", # or @hosted//file:foo.yaml
package = "petstore",
)
Produces a go_library exporting a Client + ClientWithResponses with one
method per OpenAPI operation, plus Go types for components/schemas. Consumers
add it to deps (or embed) and call methods directly:
client, _ := petstore.NewClient("https://api.example.com")
resp, _ := client.GetPet(ctx, 42)
Threading the runtime
The generated client references github.com/oapi-codegen/runtime (parameter
binding) and .../runtime/types (Date/UUID/File formats). Defaults point at
rules_openapi’s own go_deps; consumers building against their own go_deps
universe thread their labels through:
openapi_go_client(
name = "my_client",
spec = "spec.yaml",
package = "myclient",
runtime = "@my_go_deps//github.com/oapi-codegen/runtime",
runtime_types = "@my_go_deps//github.com/oapi-codegen/runtime/types:types",
)
OpenAPI 3.1 normalization
The default Go plugin runs a spec-normalization pass before oapi-codegen for constructs it doesn’t handle natively — so real-world 3.1 specs generate cleanly rather than erroring:
type: [X, "null"](3.1 nullable arrays) →type: X+nullable: true.oneOfwith a bare{type: "null"}branch → drop the branch, mark nullable.- a
oneOf+discriminatorwith inline branches (no$ref, nomapping) → each branch is lifted to a namedcomponents/schemasentry keyed by its discriminator value, and themappingis filled in, so oapi-codegen emits a proper discriminated union with named branch types. - Go field-name collisions from dual camelCase/snake_case properties (e.g. a
deprecated
createdAtalongsidecreated_at) → anx-go-nameoverride on the non-canonical property, keeping both.
The plugin also prunes oapi-codegen’s broad import block itself (via go/ast)
rather than shelling out to goimports, so it runs in a hermetic sandbox with no
go on PATH.
progenitor’s limitations
The default plugin wraps progenitor 0.14, which doesn’t handle some common OpenAPI patterns:
- Distinct success vs. error response schemas (e.g.
200returns one schema,defaultreturns anErrorstruct). progenitor assertsresponse_types.len() <= 1; OAI’s canonical petstore trips it. - Multi-content-type request bodies (e.g.
application/json+application/xml). Swagger’s v3 petstore trips this.
Two clean escape hatches:
-
Preprocess the spec — drop the
defaultresponse or the alternative content types — before feeding it through the rule. -
Register your own toolchain pointing at a different codegen (
openapi-generator, hand-rolled, etc.). The plugin contract makes that swap mechanical:load("@rules_openapi//openapi:toolchains.bzl", "openapi_codegen_toolchain") openapi_codegen_toolchain( name = "my_custom_rust_client_codegen", binary = "//path/to:your_binary", ) toolchain( name = "my_custom_rust_client_codegen_toolchain", toolchain = ":my_custom_rust_client_codegen", toolchain_type = "@rules_openapi//openapi:rust_client_codegen_toolchain_type", )Then
register_toolchains("//path:my_custom_rust_client_codegen_toolchain")in yourMODULE.bazelahead of rules_openapi’s default.
Conformance testing
The openapi_plugin_contract_test rule runs the contract scenarios
(valid input, malformed input, unknown flag, determinism) against
any plugin executable:
load("@rules_openapi//openapi:contract_test.bzl",
"openapi_plugin_contract_test")
openapi_plugin_contract_test(
name = "my_plugin_conforms",
plugin = "//my:openapi_rust_client_codegen",
)
The default plugin is gated by this test
(//tools/openapi_to_rust_client:openapi_to_rust_client_conforms).
Compatibility
- Bazel: 7.4+, bzlmod required (tested on 9.1).
- Rust: 1.88+ (transitive deps need stabilised
let-chains). - Go: SDK 1.24+ (downloaded by rules_go).
- OpenAPI: 3.0 + 3.1. The Rust path follows progenitor’s coverage (gaps
documented above); the Go path normalizes the common 3.1 constructs
oapi-codegen doesn’t handle (see
openapi_go_clientabove).
Roadmap
- v0.3: Server stubs (
openapi_rust_server,openapi_go_server). - v0.4: Compose with rules_jsonschema — extract
components/schemasin a preprocessing step, pipe throughjsonschema_rust_library, so the client codegen only handles operations. Eliminates the type-generation duplication.
Testing
bazel test //...
| Target | Coverage |
|---|---|
//tools/openapi_to_rust_client:openapi_to_rust_client_conforms | plugin contract conformance |
//examples/smoke:keeper_client_test | OpenAPI → generated client → typed decode round-trip |
License
MIT.
Usage#
Real usage, taken from the module’s examples/.
examples/smoke/BUILD.bazel
load("@rules_openapi//rust:defs.bzl", "openapi_rust_client")
load("@rules_rust//rust:defs.bzl", "rust_test")
# A small real OpenAPI 3 spec (Oxide's "keeper" service from
# progenitor's own test suite), fetched via http_file. See
# //openapi/private:extensions.bzl for why we use this instead of
# a canonical petstore.
openapi_rust_client(
name = "keeper_client",
spec = "@openapi_keeper_example//file:keeper.json",
)
# Compile-and-import test: pull in the generated `Client` + types,
# verify the public surface exists. No real HTTP — that would need a
# test server — but constructing a Client and referencing the
# generated types fails at build time if the codegen pipeline broke.
rust_test(
name = "keeper_client_test",
srcs = ["client_test.rs"],
edition = "2021",
deps = [
":keeper_client",
"@openapi_crates//:serde_json",
],
)
examples/smoke_go/BUILD.bazel
# gazelle:ignore
load("@rules_go//go:def.bzl", "go_test")
load("@rules_openapi//go:defs.bzl", "openapi_go_client")
# Generate a Go client from a small real OpenAPI 3 spec (Oxide's "keeper"
# service from progenitor's test suite — the same fixture the Rust smoke example
# uses). See //openapi/private:extensions.bzl for why we use this over a petstore.
openapi_go_client(
name = "keeper_client",
package = "keeper",
spec = "@openapi_keeper_example//file:keeper.json",
)
# Compile-and-import test: construct the generated Client + reference its public
# surface. No real HTTP (that needs a server), but the whole codegen → go_library
# pipeline fails at build time if anything broke.
go_test(
name = "keeper_client_test",
srcs = ["client_test.go"],
embed = [":keeper_client"],
)Rules & providers#
Generated with Stardoc from the module's .bzl sources.
from docs/contract_test.md
OpenAPI plugin conformance test.
openapi_plugin_contract_test(name, plugin) runs the rules_openapi
plugin contract scenarios against any plugin executable. Mirrors
rules_jsonschema’s jsonschema_plugin_contract_test but with
OpenAPI-flavored fixtures (a minimal OpenAPI 3.1 document instead
of a JSON Schema).
openapi_plugin_contract_test
load("@rules_openapi//openapi:contract_test.bzl", "openapi_plugin_contract_test")
openapi_plugin_contract_test(name, plugin)
Run the rules_openapi plugin contract scenarios against a plugin binary.
ATTRIBUTES
| Name | Description | Type | Mandatory | Default |
|---|---|---|---|---|
| name | A unique name for this target. | Name | required | |
| plugin | The plugin binary to test. | Label | required |
from docs/defs.md
Rust user-facing rules for rules_openapi.
openapi_rust_client is the Rust client codegen rule:
- Resolves the
rust_client_codegen_toolchain_typetoolchain. - Runs the toolchain’s binary on the OpenAPI spec (stdin/argv/
stdout per
//openapi/plugin_contract.md), producing a.rs. - Wraps the
.rsin arust_librarywhose deps includeprogenitor-client,reqwest,serde,serde_json, and any additional crates the consumer threads through.
The default toolchain points at the in-repo openapi_to_rust_client
binary, which wraps progenitor under the hood. Unlike the Go path,
the Rust backend is dev-gated (so Go-only consumers don’t pull
rules_rust): a consumer registers it — plus rules_rust + the
crate deps — themselves (see the README “Install” section). Swap by
declaring your own openapi_codegen_toolchain and registering it
ahead of the default.
openapi_rust_client
load("@rules_openapi//rust:defs.bzl", "openapi_rust_client")
openapi_rust_client(name, spec, extra_args, progenitor_client, reqwest, serde, serde_json, regress,
chrono, uuid, bytes, visibility, **rust_library_kwargs)
Generate a rust_library of a typed OpenAPI HTTP client.
The library exports a Client struct with one method per
OpenAPI operation, plus a types module containing serde
structs for components/schemas.
PARAMETERS
from docs/go_defs.md
Go user-facing rules for rules_openapi.
openapi_go_client is the Go client codegen rule:
- Resolves the
go_client_codegen_toolchain_typetoolchain. - Runs the toolchain’s binary on the OpenAPI spec (stdin/argv/
stdout per
//openapi/plugin_contract.md), producing a.go. - Wraps the
.goin ago_librarywhose deps includegithub.com/oapi-codegen/runtime(the runtime the generated client references) plus any additional packages the consumer threads through.
The default toolchain (registered by MODULE.bazel) points at the
in-repo openapi_to_go_client binary, which wraps oapi-codegen
under the hood (with a spec-normalization pass for OpenAPI 3.1
constructs oapi-codegen doesn’t handle natively). Swap by declaring
your own openapi_codegen_toolchain and registering it ahead of the
default.
openapi_go_client
load("@rules_openapi//go:defs.bzl", "openapi_go_client")
openapi_go_client(name, spec, package, importpath, include_tags, include_operations, runtime,
runtime_types, deps, visibility, **go_library_kwargs)
Generate a go_library of a typed OpenAPI HTTP client.
The library exports a Client + ClientWithResponses with one
method per OpenAPI operation, plus Go types for components/schemas.
PARAMETERS
from docs/providers.md
Providers exposed by rules_openapi.
Same shape as rules_jsonschema’s JsonschemaCodegenToolchainInfo —
the plugin contract is identical (stdin/argv/stdout), the only
difference is the schema content shipped on stdin (OpenAPI document
rather than a JSON Schema).
OpenapiCodegenToolchainInfo
load("@rules_openapi//openapi:providers.bzl", "OpenapiCodegenToolchainInfo")
OpenapiCodegenToolchainInfo(binary)
An OpenAPI → code codegen tool.
FIELDS
| Name | Description |
|---|---|
| binary | File: the codegen executable. Invoked with --schema-name=NAME --rule-name=NAME plus per-plugin flags the calling rule passes through. |
from docs/toolchains.md
Toolchain rules for rules_openapi codegen.
openapi_codegen_toolchain wraps a single codegen executable as a
Bazel toolchain. Toolchain types are split per (language, use_case)
pair — Rust clients, Go clients, Rust servers, etc. — so a consumer
can swap one plugin without affecting the rest.
Default toolchains are registered in the per-language directories
(//rust:BUILD.bazel, …). To swap an implementation, declare your
own openapi_codegen_toolchain and register_toolchains(...) it
ahead of rules_openapi’s default in your MODULE.bazel.
openapi_codegen_toolchain
load("@rules_openapi//openapi:toolchains.bzl", "openapi_codegen_toolchain")
openapi_codegen_toolchain(name, binary)
Declare an OpenAPI → code codegen executable as a Bazel toolchain.
ATTRIBUTES
| Name | Description | Type | Mandatory | Default |
|---|---|---|---|---|
| name | A unique name for this target. | Name | required | |
| binary | The codegen executable. Must accept --schema-name=NAME --rule-name=NAME plus any per-plugin flags the calling rule passes through. | Label | required |
Conformance#
1 finding across 1 invariant. 11 contested atoms. See how gating works or the full report.
| version | toolchain |
|---|---|
0.4.0 | //go:default_go_client_codegen_toolchain |
Contested atoms
Third-party modules where this module resolves a different version than others do. Not a violation of anything this module did — it is the actionable form of a registry-level convergence finding, and the sentence a maintainer can act on.
| Atom | Resolved here | Elsewhere |
|---|---|---|
apple_support | 1.24.2 | 2.2.0 ×1 |
bazel_skylib | 1.8.2 | 1.9.0 ×2 |
gazelle | 0.44.0 | 0.30.0 ×50.36.0 ×140.51.0 ×3 |
nlohmann_json | 3.6.1 | 3.12.0.bcr.1 ×1 |
package_metadata | 0.0.2 | 0.0.5 ×3 |
protobuf | 33.4 | 34.0.bcr.1 ×2 |
rules_go | 0.60.0 | 0.39.1 ×5 |
rules_jvm_external | 6.7 | 6.8 ×4 |
rules_python | 1.7.0 | 2.0.1 ×1 |
rules_swift | 3.1.2 | 3.6.1 ×1 |
upb | 0.0.0-20220923-a547704 | 0.0.0-20230516-61a97ef ×1 |
Dependencies#
Depends on
Used by (2 in the registry)
Versions#
5 published versions, newest first. Each resolves to an immutable, integrity-checked archive.
| Version | Integrity (sha256) | Source archive |
|---|---|---|
0.4.0 latest | 0EIPd7dA+TAZIC0V… | tag archive ↗ |
0.3.0 | 9L7b3eP6MhbuDsE8… | tag archive ↗ |
0.2.1 | OlAjuLDwvdDqUqHZ… | tag archive ↗ |
0.2.0 | l2rBfMj/IzukTeD8… | tag archive ↗ |
0.1.0 | YX6iTB7Q+x/CwSLQ… | tag archive ↗ |
Changelog#
All notable changes to rules_openapi. The format is loosely Keep a Changelog — version headers mirror the published bazel-registry entries.
0.4.0 — Rust path is opt-in (Go consumers no longer pull rules_rust)
- Breaking (Rust consumers only):
rules_rust,rules_jsonschema,crate_universe, and the Rust toolchain are nowdev_dependencywiring, and the Rust codegen toolchain is registered in this repo’s.bazelrcrather than inMODULE.bazel.register_toolchains()inMODULE.bazelpropagates to every downstream module, so a Go-only consumer ofopenapi_go_clientwas forced to pullrules_rustand register Rust toolchains it never used. After 0.4.0, a Go consumer’s module graph is Rust-free. - Consumers of
openapi_rust_clientnow addrules_rust+rules_jsonschema, thecrate_universedeps, andregister_toolchains("@rust_toolchains//:all", "@rules_openapi//rust:default_rust_client_codegen_toolchain")in their ownMODULE.bazel— see the README “Install” section. No change to the rule APIs or generated output.
0.3.0 — Go client codegen
openapi_go_client(//go:defs.bzl): OpenAPI → typed Go HTTP client (Client+ClientWithResponses+components/schemastypes), backed by a new default toolchain//tools/openapi_to_go_clientthat wrapsoapi-codegen. Registered on the reservedgo_client_codegen_toolchain_type; swappable like the Rust one.- The Go plugin normalizes OpenAPI 3.1 constructs oapi-codegen doesn’t handle
natively so real specs generate cleanly: nullable-type arrays
(
type: [X, "null"]), null-onlyoneOfbranches, and discriminatedoneOfs with inline branches (lifted to named schemas + a filled-inmapping, giving proper discriminated unions). It also disambiguates Go field-name collisions from dual camelCase/snake_case properties viax-go-name. - The plugin prunes oapi-codegen’s broad import block itself with
go/astinstead of shelling out togoimports, so it runs in a hermetic sandbox with nogoonPATH. - Verified end-to-end against the WorkOS OpenAPI 3.1 spec (~57k lines of generated client, compiles), plus the keeper smoke example + the shared plugin contract test.
0.2.1 — threadable chrono / uuid / bytes
openapi_rust_clientgainschrono,uuid,bytesattrs (default to@openapi_crates, so existing callers are unchanged). Previously these three were hard-coded to@openapi_crateswhile the rest of the runtime deps were threadable — a consumer with its own crates_universe got a trait-identity mismatch (chrono’s serde impls resolved against@openapi_crates’ serde, not the consumer’s). Threading all runtime deps from one universe fixes it.
0.2.0 — docs + CI infrastructure
- Stardoc-generated reference docs for the 4 public-API .bzl files
in
docs/:defs.md,providers.md,toolchains.md,contract_test.md.bazel run //docs:updateregenerates;bazel test //docs:allgates the committed copies viadiff_test. - GitHub Actions CI:
bazel test //...on ubuntu + macos, plus a buildifier lint job. CHANGELOG.md(this file)..gitignore:.claude/andMODULE.bazel.lock.
No API changes.
0.1.0 — initial release
- OpenAPI 3 → typed code via the plugin contract from
rules_jsonschema(stdin = OpenAPI doc bytes, argv =--key=value, stdout = generated source). Identical shape to rules_jsonschema, with OpenAPI-specific argv knobs. openapi_rust_clientrule: generates arust_librarythat exports aClientstruct with one method per OpenAPI operation, plus atypesmodule containing serde structs forcomponents/schemas.- Default Rust client plugin (
tools/openapi_to_rust_client) wrapsprogenitor0.14. OpenapiCodegenToolchainInfoprovider + per-language toolchain types (rust_client_codegen_toolchain_typetoday, more to follow).openapi_plugin_contract_testconformance driver mirroring rules_jsonschema’s pattern (valid_minimal,malformed_input,unknown_flag,determinism).- End-to-end smoke fixture using Oxide’s
keeper.jsonfrom progenitor’s test suite (canonical petstore.yaml has multi- content-type request bodies that progenitor doesn’t model).