CLI tool
oxidecomputer/progenitor avatar
oxidecomputer/progenitor

Progenitor, an OpenAPI client generator that knows about Dropshot

An OpenAPI client generator

1,028 stars134 forksRustMPL-2.0

At a glance

What is it?
Progenitor is a Rust crate that turns an OpenAPI 3.0 document into an opinionated client, with a Client type whose methods correspond to the operations, futures for the asynchronous calls and streams for the paginated ones. It can also generate a command line client and typed httpmock helpers, and it is built around the OpenAPI dialect that Oxide's own Dropshot framework emits.
Who is it for?
Progenitor fits a Rust team that owns an OpenAPI document and wants a typed client without writing the request plumbing, and the three integration styles cover most needs, from a macro in a single file to a frozen standalone crate for a service you publish.
Can I use it commercially?
Yes, with conditions. MPL-2.0 is a weak copyleft licence: you can use it inside commercial and closed-source software, but if you distribute changes to its own files, you must publish those changes under the same licence.
Is it still maintained?
Yes. The repository last received commits 4 days ago.
What is it written in?
Mainly Rust, according to GitHub's language statistics.

Answers come from the project's GitHub data, last synced on October 23, 2026, and from our analysis. They are not legal advice.

Editorial analysis

One Client type, one method per operation

Progenitor is a Rust crate for generating opinionated clients from API descriptions in the OpenAPI 3.0.x specification. Two words in that sentence do the work. The first is opinionated, which is a warning as much as a promise: the generated code will look like a particular style of Rust client, and it will not be shaped to your taste. The second is generating, because what you get is code, not a runtime wrapper around a description.

The shape of that code is described precisely. It generates a type called Client with methods that correspond to the operations specified in the OpenAPI document, it makes use of Rust futures for the asynchronous API calls, and Streams for paginated interfaces. So a paginated endpoint becomes a stream of items rather than a call that returns a page and a cursor, which is the design decision that matters most for a client of a large service.

Two additional outputs are possible. Progenitor can also generate a CLI to interact with an OpenAPI service instance, and httpmock helpers to create a strongly typed mock of that service. The mock output is the one that pays for itself in a test suite, because a test can then assert on a typed request rather than on a stringly-typed one.

The primary target is OpenAPI documents emitted by Dropshot, the framework Oxide wrote and maintains, but the readme is clear that it can be used for many OpenAPI documents, and equally clear that since OpenAPI covers a wide range of APIs, Progenitor may fail for some of them. The mitigation is a good one and worth repeating to any team considering the tool: if you hit a failure, file an issue that includes the OpenAPI document that produced it.

Three ways to use it, with different costs

The readme says there are three different ways of using the progenitor crate, and that the one you choose depends on your use case and preferences.

The first is the macro, and it is the simplest. In a source file, often main.rs, lib.rs or mod.rs, you invoke the macro with the path to the OpenAPI document. The second is a build.rs, which the readme describes as slightly more onerous than the macro but with the advantage of making the generated code visible. The third is a static crate, where Progenitor emits a stand-alone crate for the generated client, which ensures no unexpected changes such as those from updates to Progenitor, and which is however the most manual way to use it.

The reason to know all three is that the capabilities are not evenly distributed. The capability of generating a CLI and httpmock helpers is only available using build.rs, with the Generator functions cli and httpmock respectively. So if your requirement includes a command line client or typed mocks, the macro route is not an option and the choice is between the build script and the standalone crate.

The static crate exists for a reason that is easy to miss. When you generate a client from a spec at build time, your build depends on the generator, and a new version of the generator can change the code it emits, which means your build can change without your source changing. A pre-generated crate removes that dependency entirely, at the cost of checking the generated code into your tree or publishing it.

The examples in the repository match the three routes, with example-macro, example-build, example-out-dir and example-wasm as separate workspaces, which is a useful way to see the same client produced four different ways.

The macro options are the shape of the generated API

The macro has a set of options that control the generated code, and they are worth reading as a description of the decisions a generated client makes on your behalf.

The spec option names the OpenAPI document. The interface option chooses positional, which is the default, or builder style, which matters for an API with many optional parameters, because a builder call stays readable where a positional call with four options becomes a puzzle. The tags option chooses whether operation tags are Merged or Separate, with Separate as the default, which determines whether one method per operation or one method per tag.

Then there are the hooks. The hooks option can be set to Expected, which requires an implementation of ClientHooks for the Client, and is optional by default. The inner_type option names a client inner type that is available to the pre and post hooks, and the pre_hook and post_hook options name a closure or a path to a function that is invoked before issuing the HTTP request and prior to receiving the HTTP response respectively. And the derives option applies additional derive macros to the generated types, with the example being schemars::JsonSchema.

The pre and post hooks are the feature that turns this from a code generator into an integration point. A pre-hook is where an authentication header gets added, a request id gets stamped or a clock skew offset is applied, and doing that in generated code for every call site is exactly the kind of repetition the generator is supposed to remove.

One detail of the macro behaviour is easy to trip over. The macro will be re-evaluated when the spec OpenAPI document changes, when its modification time is updated. So editing the spec, or a build system that touches the file, regenerates the code, and that is a behaviour you want to know about before a surprising rebuild appears in CI.

The generated code needs the crates your specification implies

A detail of Progenitor that other generators handle differently is how the generated code's dependencies are communicated. The answer is that you add them yourself, and the readme spells out which ones, keyed to what appears in your document.

The core set is futures, the progenitor crate itself, reqwest with the json, query and stream features, and serde with derive. Then the optional set, and each is conditional on the content of your OpenAPI document rather than on a feature flag in Progenitor.

If the document contains string types with the format field set to date or date-time, you add chrono with the serde feature. If there is a format field set to uuid, you add uuid with the serde and v4 features. If there are any websocket channel endpoints, you add base64 and rand. And if the types include regular expression validation, you add regress.

If you derive schemars::JsonSchema on the generated types, you add schemars and enable the necessary features for each formatted type that appears in your spec, with the example being the chrono and uuid1 features.

This is a deliberate design, and it is a good one. The generator does not force a dependency on every possible type format, and it does not silently produce code that will not compile because a crate is missing. It tells you exactly what to add, and the addition is one line in your manifest. The cost is that the dependency list of your crate is assembled by the reader of the specification, which is a step that a fully automatic generator would have hidden.

It also means the generated code integrates with the rest of your dependency set rather than pinning its own versions, which matters when the spec changes format and you want to bump chrono without regenerating everything.

build.rs makes the generated code visible

The build script route is worth reading in the readme because it shows the whole pipeline, and because it explains the split between the generator and the runtime client.

The script opens the OpenAPI document, prints a rerun-if-changed line for it so Cargo knows to rebuild when the file changes, deserialises the specification with serde_json, and then creates a generator, generates the tokens, parses them with syn, and formats them with prettyplease:

rust
fn main() {
    let src = "../sample_openapi/keeper.json";
    println!("cargo:rerun-if-changed={}", src);
    let file = std::fs::File::open(src).unwrap();
    let spec = serde_json::from_reader(file).unwrap();
    let mut generator = progenitor::Generator::default();

    let tokens = generator.generate_tokens(&spec).unwrap();
    let ast = syn::parse2(tokens).unwrap();
    let content = prettyplease::unparse(&ast);

    let mut out_file = std::path::Path::new(&std::env::var("OUT_DIR").unwrap()).to_path_buf();
    out_file.push("codegen.rs");

    std::fs::write(out_file, content).unwrap();
}

The source file then includes the result with include! and the concat of the OUT_DIR environment variable.

The split between the crates is the other thing the example teaches. The progenitor crate is used by build.rs, but the generated code required progenitor-client. In other words the generator is a build-time dependency and the client runtime is a normal dependency, and the generated code does not drag the generator into your binary.

The build dependencies are prettyplease, progenitor, serde_json and syn, and the runtime dependencies are futures, progenitor-client, reqwest and serde. That is a clean separation, and it is why the build script route is the one that scales to a large project: the generator's dependency tree never reaches production.

The static crate freezes the client against generator drift

The third route emits a crate you can publish, and the readme gives the exact commands.

First the tool is installed, with cargo install cargo-progenitor. Then the generator runs, with the input document, the output directory, the crate name and the crate version:

bash
cargo install cargo-progenitor
cargo progenitor -i sample_openapi/keeper.json -o keeper -n keeper -v 0.1.0

The same thing can be run from inside the repository, with cargo run and the binary named, which is how a contributor tests a change to the generator against a sample document without installing anything.

The options are -i for the OpenAPI definition document in JSON or YAML, -o for the generated Rust crate directory, -n for the target crate name and -v for the version. The options --license and --registry-name may also be used to improve the metadata before publishing the static crate, which is the detail that tells you the output is meant to be published to a registry rather than merely committed.

The interesting behaviour is in how the client runtime is referenced. The output will use the published progenitor-client crate by default if Progenitor is built in release mode, and when built in debug mode the progenitor-client will be inlined into the generated crate by default. A command line flag, --include-client true or false, overrides the default, where true copies the client code in and false adds a dependency on progenitor-client in the generated Cargo.toml.

That conditional is a sensible default, because a release build of the generator should produce a crate that depends on the published client, so that a security fix to the client is picked up by a dependency update, while a debug build should produce something self-contained so that a developer can step through the client code.

Where Progenitor is the wrong tool

The first limit is the dialect. The primary target is OpenAPI documents emitted by Dropshot, and the readme says that since OpenAPI covers a wide range of APIs, Progenitor may fail for some documents. A generated client is only as good as the part of the specification the generator understands, and the parts it does not understand are the parts you will discover as a compile error.

The second is the word opinionated. The generated client is a design decision made once, by someone else, and it will not match the conventions of your codebase, your error handling or your async runtime. The builder interface option and the hooks options are the escape hatches, and they are worth using from the start rather than after a refactor.

The third is the build-time dependency in the macro and build script routes. The specification file becomes an input to your compilation, so the build needs it, and a change to the file triggers regeneration. That is exactly what you want for a spec-first workflow, and exactly what you do not want if your build has to run somewhere the document is not available.

The fourth is the versioning. The workspace crates are at 0.15.1, which is a pre-1.0 number, and the published progenitor-client is referenced in the example output as 0.9.1, so the client crate and the generator are versioned independently. A project that pins the client must therefore track two version lines.

The fifth is the macro's ergonomics for a large document. The macro is the simplest route, but it re-evaluates the document at compile time and produces code that you cannot read in your own source tree, which is why the build script and the static crate exist. For a large API, the static crate is the one to choose.

The last is the licence. MPL-2.0 is file-level copyleft, which is a reasonable choice for a generator and a question to answer if you plan to modify the generated output and redistribute it, because the licence covers the generator, not the code it emits.

Editorial conclusion

Progenitor fits a Rust team that owns an OpenAPI document and wants a typed client without writing the request plumbing, and the three integration styles cover most needs, from a macro in a single file to a frozen standalone crate for a service you publish. It does not fit a team that needs a client for an arbitrary third-party API, since the generator is opinionated and the readme says plainly that it may fail for some OpenAPI documents, and it does not fit a project that needs the generated code to be reviewed as source rather than produced at build time. Before adopting it, check that your document is close to the Dropshot shape, and if you use the build script, remember that the CLI and httpmock generators are reachable only from there. The licence is MPL-2.0, the workspace is on edition 2024 with a minimum Rust of 1.88, and the last push was on 2026-10-14.

Frequently asked questions

What does Progenitor generate from an OpenAPI document?

An opinionated Rust client with a type called Client whose methods correspond to the operations in the document, using futures for the asynchronous calls and Streams for paginated interfaces. It can also generate a CLI to talk to the service and httpmock helpers for a strongly typed mock.

Which OpenAPI documents does Progenitor handle?

The primary target is documents emitted by Dropshot, but it can be used for many OpenAPI documents. The README warns that it may fail for some, and asks that any problem be filed as an issue including the OpenAPI document that caused it.

What are the three ways to use Progenitor?

The generate_api! macro, a build.rs that calls the Generator, or a static crate emitted by the cargo-progenitor command. The macro is the simplest, the build script is more onerous but makes the generated code visible and is the only route to the CLI and httpmock output, and the static crate is the most manual but freezes the client.

Which extra crates does the generated Progenitor code need?

chrono for date and date-time formats, uuid for the uuid format, base64 and rand for websocket channel endpoints, regress for regular expression validation, and schemars if you derive JsonSchema on the generated types, with features enabled for each formatted type in your spec.

How do I generate a standalone client crate with Progenitor?

Install the tool with cargo install cargo-progenitor, then run cargo progenitor with -i for the OpenAPI document in JSON or YAML, -o for the output directory, -n for the crate name and -v for the version, adding --license and --registry-name to improve the metadata before publishing.

Official sources

  1. Issues
  2. License: MPL-2.0
  3. oxidecomputer/progenitor on GitHub
  4. README
Add this badge to your README

If you maintain this project, the badge below links readers to this analysis and shows its maintenance status from the daily GitHub snapshot. Paste the markdown into your README; add ?metric=license or ?metric=stars to the image URL for a different field.

Add this badge to your README

markdown
[![Hysen Labs](https://hysenlabs.com/badge/oxidecomputer-progenitor.svg)](https://hysenlabs.com/projects/oxidecomputer-progenitor)

Was this useful?