tomato·bazeldocs v0 · latest
Docs/Reference/Modules/rules_openapi

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

Latest0.4.0
Versions5
CategoryBazel rules
MaintainersMatt Marshall
Registryhttps://registry.tbzl.dev/modules/rules_openapi/
Sourcegithub.com/tomato-bazel/rules_openapi
MODULE.bazelstarlark
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.
  • oneOf with a bare {type: "null"} branch → drop the branch, mark nullable.
  • a oneOf + discriminator with inline branches (no $ref, no mapping) → each branch is lifted to a named components/schemas entry keyed by its discriminator value, and the mapping is 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 createdAt alongside created_at) → an x-go-name override 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. 200 returns one schema, default returns an Error struct). progenitor asserts response_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:

  1. Preprocess the spec — drop the default response or the alternative content types — before feeding it through the rule.

  2. 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 your MODULE.bazel ahead 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_client above).

Roadmap

  • v0.3: Server stubs (openapi_rust_server, openapi_go_server).
  • v0.4: Compose with rules_jsonschema — extract components/schemas in a preprocessing step, pipe through jsonschema_rust_library, so the client codegen only handles operations. Eliminates the type-generation duplication.

Testing

bazel test //...
TargetCoverage
//tools/openapi_to_rust_client:openapi_to_rust_client_conformsplugin contract conformance
//examples/smoke:keeper_client_testOpenAPI → 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

NameDescriptionTypeMandatoryDefault
nameA unique name for this target.Namerequired
pluginThe plugin binary to test.Labelrequired

from docs/defs.md

Rust user-facing rules for rules_openapi.

openapi_rust_client is the Rust client codegen rule:

  1. Resolves the rust_client_codegen_toolchain_type toolchain.
  2. Runs the toolchain’s binary on the OpenAPI spec (stdin/argv/ stdout per //openapi/plugin_contract.md), producing a .rs.
  3. Wraps the .rs in a rust_library whose deps include progenitor-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

NameDescriptionDefault Value
namerust_library target name. Consumers add this to deps.none
speclabel of an OpenAPI .yaml / .yml / .json document.none
extra_argsextra --key=value flags passed to the plugin.None
progenitor_clientlabel of the progenitor_client runtime crate the generated code references. Defaults to @openapi_crates//:progenitor-client. Consumers using their own crates_universe must thread this through (and likewise the other runtime-dep attrs below) to avoid the same trait- identity mismatch rules_jsonschema documents.None
reqwestlabel of reqwest (HTTP client the generated code uses).None
serdelabel of serde.None
serde_jsonlabel of serde_json.None
regresslabel of regress (used by typify-generated types nested inside progenitor’s output).None
chronolabel of chrono (date-time formats). Must come from the same crates_universe as serde.None
uuidlabel of uuid (uuid format). Same-universe-as-serde rule.None
byteslabel of bytes (binary format). Same-universe-as-serde rule.None
visibilityforwarded to rust_library.None
rust_library_kwargsforwarded to rust_library (e.g. extra deps).none

from docs/go_defs.md

Go user-facing rules for rules_openapi.

openapi_go_client is the Go client codegen rule:

  1. Resolves the go_client_codegen_toolchain_type toolchain.
  2. Runs the toolchain’s binary on the OpenAPI spec (stdin/argv/ stdout per //openapi/plugin_contract.md), producing a .go.
  3. Wraps the .go in a go_library whose deps include github.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

NameDescriptionDefault Value
namego_library target name. Consumers add this to deps.none
speclabel of an OpenAPI .yaml / .yml / .json document.none
packageGo package name for the generated file. Defaults to a sanitized form of name.None
importpathgo_library importpath. Defaults to package. Override when consumers import the client by a specific module path.None
include_tagsif set, generate only operations carrying one of these OpenAPI tags (plus the schemas they reach) — carve a small client out of a large API.None
include_operationsif set, generate only operations with one of these operationIds.None
runtimelabel of github.com/oapi-codegen/runtime (the runtime the generated client references for parameter binding). Defaults to @com_github_oapi_codegen_runtime//:runtime. Consumers using their own go_deps universe should thread this through so the generated code compiles against the same runtime they build with.None
runtime_typeslabel of github.com/oapi-codegen/runtime/types (the Date/UUID/File format types the generated code references when the spec uses those formats). Defaults to @com_github_oapi_codegen_runtime//types. Thread from the same universe as runtime.None
depsextra deps forwarded to go_library (for specs whose generated code references additional packages).None
visibilityforwarded to go_library.None
go_library_kwargsforwarded to go_library.none

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

NameDescription
binaryFile: 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

NameDescriptionTypeMandatoryDefault
nameA unique name for this target.Namerequired
binaryThe codegen executable. Must accept --schema-name=NAME --rule-name=NAME plus any per-plugin flags the calling rule passes through.Labelrequired

Conformance#

1 finding across 1 invariant. 11 contested atoms. See how gating works or the full report.

D2 a non-dev register_toolchains propagates to every transitive consumer why this matters ↗
versiontoolchain
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.

AtomResolved hereElsewhere
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#

rules_openapi in the registry graph — what it depends on (left) and what depends on it (right).

Depends on

platforms1.0.0bazel_skylib1.8.2rules_go0.60.0gazelle0.44.0rules_jsonschema0.1.0devrules_rust0.70.0devrules_shell0.6.1devstardoc0.7.2dev

Used by (2 in the registry)

Versions#

5 published versions, newest first. Each resolves to an immutable, integrity-checked archive.

VersionIntegrity (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 now dev_dependency wiring, and the Rust codegen toolchain is registered in this repo’s .bazelrc rather than in MODULE.bazel. register_toolchains() in MODULE.bazel propagates to every downstream module, so a Go-only consumer of openapi_go_client was forced to pull rules_rust and register Rust toolchains it never used. After 0.4.0, a Go consumer’s module graph is Rust-free.
  • Consumers of openapi_rust_client now add rules_rust + rules_jsonschema, the crate_universe deps, and register_toolchains("@rust_toolchains//:all", "@rules_openapi//rust:default_rust_client_codegen_toolchain") in their own MODULE.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/schemas types), backed by a new default toolchain //tools/openapi_to_go_client that wraps oapi-codegen. Registered on the reserved go_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-only oneOf branches, and discriminated oneOfs with inline branches (lifted to named schemas + a filled-in mapping, giving proper discriminated unions). It also disambiguates Go field-name collisions from dual camelCase/snake_case properties via x-go-name.
  • The plugin prunes oapi-codegen’s broad import block itself with go/ast instead of shelling out to goimports, so it runs in a hermetic sandbox with no go on PATH.
  • 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_client gains chrono, uuid, bytes attrs (default to @openapi_crates, so existing callers are unchanged). Previously these three were hard-coded to @openapi_crates while 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:update regenerates; bazel test //docs:all gates the committed copies via diff_test.
  • GitHub Actions CI: bazel test //... on ubuntu + macos, plus a buildifier lint job.
  • CHANGELOG.md (this file).
  • .gitignore: .claude/ and MODULE.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_client rule: generates a rust_library that exports a Client struct with one method per OpenAPI operation, plus a types module containing serde structs for components/schemas.
  • Default Rust client plugin (tools/openapi_to_rust_client) wraps progenitor 0.14.
  • OpenapiCodegenToolchainInfo provider + per-language toolchain types (rust_client_codegen_toolchain_type today, more to follow).
  • openapi_plugin_contract_test conformance driver mirroring rules_jsonschema’s pattern (valid_minimal, malformed_input, unknown_flag, determinism).
  • End-to-end smoke fixture using Oxide’s keeper.json from progenitor’s test suite (canonical petstore.yaml has multi- content-type request bodies that progenitor doesn’t model).

← All modules