Open-source project
elastio/bon avatar
elastio/bon

bon: Rust builders for functions, not just structs

Next-gen compile-time-checked builder generator, named function's arguments, and more!

2,137 stars47 forksRustApache-2.0

At a glance

What is it?
bon generates compile-time-checked builders for structs, functions and methods, using typestate so missing or repeated setters are compile errors, with no_std support and a compatibility guarantee between its derive and attribute forms. Its MSRV badge and its own Clippy comment disagree about which compiler it targets.
Who is it for?
Adopt bon when your API has functions or methods with optional or numerous parameters, where named arguments and partial application are the reason to add a builder, and start with #[builder] on a free function to see whether it fits before converting a struct. Do not adopt it for structs with two required fields, and do not write code that calls a setter twice, because that is a compile error by design.
Can I use it commercially?
Yes. Apache-2.0 is a permissive licence: you can use, modify and sell software built on it, as long as you keep its copyright and licence notices.
Is it still maintained?
Yes. The repository received new commits within the last day.
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 2, 2026, and from our analysis. They are not legal advice.

Editorial analysis

The function builder is the reason this crate exists

`bon` is a Rust crate for generating compile-time-checked builders for structs and functions, and it also provides idiomatic partial application with optional and named parameters for functions and methods.

Struct builders are a solved problem with several good solutions. Function builders are not. The README's framing is a positional function turned into a named-parameter function by adding one attribute.

rust
use bon::builder;

#[builder]
fn greet(name: &str, level: Option<u32>) -> String {
    let level = level.unwrap_or(0);

    format!("Hello {name}! Your level is {level}")
}

let greeting = greet()
    .name("Bon")
    .level(24)
    .call();

The mechanism is partial application. `greet()` does not call `greet`, it produces a builder that accumulates named arguments, and `.call()` finally invokes the function. That gives you two things Rust does not otherwise have: arguments in any order, and optional arguments made explicit by their type rather than by a sentinel value.

The README states any function syntax is supported, including `async`, fallible, generic functions and `impl Trait`. Those are the cases where the generated builder has to preserve lifetimes, propagate the `?` operator through the accumulating state, and defer monomorphisation, and they are the reason the crate is a procedural macro rather than a trait.

Where that comes from is stated plainly in the acknowledgments. The project was heavily inspired by `buildstructor`, `typed-builder` and `derive_builder`, and was designed with many lessons learned from them.

Three entry points, and one compatible API

There are three ways in, and they produce the same API, which is the design decision worth understanding.

For a struct, `#[derive(Builder)]` on the definition generates the builder.

rust
#[derive(Builder)]
struct User {
    name: String,
    is_admin: bool,
    level: Option<u32>,
}

let user = User::builder()
    .name("Bon".to_owned())
    .level(24)
    .is_admin(true)
    .build();

Note what `level: Option<u32>` does. It is optional, so it can be omitted, and `.build()` compiles without it. Fields without `Option` are required. That is the rule, and it is type-driven rather than attribute-driven.

For a method, `#[bon]` goes on the impl block in addition to `#[builder]` on the method. A method named `new` generates `builder()` and `build()`; every other method generates `{method_name}()` and `call()`. So `User::new(id, name)` becomes `User::builder().id(1).name("Bon".to_owned()).build()`, and a `greet` method becomes `greeter.greet().target("the world").call()`.

The compatibility point is what makes this usable in a real codebase. The README states that `#[derive(Builder)]` on a struct generates a builder API fully compatible with placing `#[builder]` on the `new()` method when the signature resembles the struct's fields, and there is a dedicated page on switching between the two. That means migrating a struct to a hand-written constructor does not force every call site to change.

Typestate, and the heading 'No Panics Possible'

Builders generated by bon's macros use the typestate pattern to ensure all required parameters are filled, and the same setters are not called repeatedly in order to prevent unintentional overwrites. If something is wrong, a compile error is produced rather than a runtime failure.

Two separate guarantees are bundled under that heading, and they are worth separating.

The first is completeness. Each setter changes the builder's type, so a builder missing a required field is a different type from one that has everything, and only the latter has a `build()` or `call()` method. That is the standard typestate argument and it is why you get a missing-field error at the call site rather than a not-enough-arguments error inside generated code.

The second is single assignment. Calling `.name("Bon")` and then `.name("Alice")` does not overwrite, it does not compile. That is a stronger guarantee than most builder libraries make, and it is the one that changes how you write code: a setter is not idempotent and there is no reset. If you were relying on a later call replacing an earlier value, you now need a conditional.

The claim in the heading is about the generated code, and it is credible because the approach has no runtime state machine that can be indexed wrongly or unwrapped on a missing key. Everything is resolved by the type checker before the program runs.

Adding bon, and opting out of std

Installation is a single dependency line, versioned to the current minor.

toml
[dependencies]
bon = "3.10"

That pin is what the README ships, with a comment in the file reminding the maintainer to update `scripts/sync-version.sh` if it changes, which tells you the documentation version and the crate version are kept in step deliberately.

For embedded or kernel work, the feature flags are the part that matters. The README states you can opt out of `std` and `alloc` cargo features with `default-features = false` for `no_std` environments, and the repository topic list carries `no-std` and `no-std-alloc` for exactly that reason.

That is a real constraint rather than a marketing line. A crate that allocates in the generated builder cannot be used where there is no allocator, and one that references `std::` cannot link in a no_std binary. If your target is a microcontroller or a `no_std` kernel module, this is a criterion that eliminates most builder crates and you should check it before anything else.

Documentation is a guide book with a narrative introduction at bon-rs.com, an API reference index for the attributes, and a separate page comparing alternatives. Because `#[derive(Builder)]` on structs and `#[builder]` on functions have almost identical attribute APIs, the reference covers both.

The MSRV badge and a comment that contradicts it

The badge in the README states MSRV 1.88.0. Inside the workspace, the Clippy configuration contains this comment above a disabled lint.

toml
# We are targeting a pre-let-else MSRV so we can't use it
manual_let_else = "allow"

Those two statements cannot both describe the same compiler. `let ... else` was stabilised long before 1.88, so a pre-let-else MSRV is a version well below the badge. Either the comment is stale from an earlier release, the badge reflects what CI tests rather than what the crate compiles against, or the two numbers have been allowed to drift.

It matters more than a stale comment usually would. An MSRV is a promise to downstream users about the oldest compiler they must have installed, and a promise that is wrong in the permissive direction means your code fails to build on a toolchain that was supposed to work.

The surrounding configuration is otherwise careful, which makes the discrepancy more noticeable rather than less. Dozens of Clippy lints that are allowed by default are turned on at `warn` level rather than `deny`, with an explanatory comment: `deny` would make Clippy exit early on a violation, whereas `warn` lets it finish, and CI then treats warnings as errors. The comment links the specific Clippy issue about exiting early, which is the level of care you would expect around a stated MSRV.

Doctests as workspace members, and benchmarks as crates

The workspace layout tells you what this project spends its time on. The members are `benchmarks/compilation`, `benchmarks/compilation/codegen`, `benchmarks/runtime`, `bon`, `bon-macros`, `bon-sandbox` and `website/doctests`, with `resolver = "3"`.

`website/doctests` is the interesting one. Documentation code blocks are compiled and run as a test, which is the mechanism that keeps a builder crate's examples honest: if an example stops compiling after a language or macro change, the build fails.

`bon-sandbox` and the two benchmark crates are the other half. A macro crate can change compile times substantially, so measuring compilation is not a luxury here, and separating a compilation benchmark from a codegen benchmark lets you tell which one moved.

The benchmarking tool is gungraun, named in the workspace configuration with a link to its prerequisites page.

toml
# Required as per gungraun:
# https://gungraun.github.io/gungraun/latest/html/installation/prerequisites.html
[profile.bench]
debug = true

The release profile is separate and pinned at `opt-level = 3` with `debug = 0`, so benchmark and release builds do not share settings.

Around the Rust side sit a few JavaScript-adjacent files: a `package.json` whose only devDependency is Prettier, `taplo.toml` for formatting the TOML files, `release-plz.toml` for release automation, `.githooks/` for commit hooks, and `rust-toolchain.toml` pinning the toolchain the project builds with.

Where bon is the wrong tool

Four cases, and they are structural.

If your struct has no required-field problem, a builder is ceremony. A struct with two `String` fields and no optional arguments is already constructible in one expression, and a builder adds a type-state machine to your public API for no gain. The README's own motivating use is a function with optional or many parameters, which is where named arguments pay.

If you need to overwrite a setter, this crate stops you. The same setter called twice is a compile error, by design, so code that computes a default and then conditionally replaces it needs restructuring rather than a second call. That is a genuine improvement in correctness and a genuine obstacle in migration.

If you are on an older compiler, check the MSRV before anything else, given the discrepancy described above. This is a 3.x line with an MSRV badge of 1.88.0, and a compiler below that is a non-starter regardless of what the code needs.

If you are generating builders from a schema at runtime, this is the wrong category. bon is a proc macro that runs at compile time on types written in Rust. It is not a code generator that takes a JSON definition and emits a builder, so a project whose models come from a service definition needs something else in that layer and can still use bon for the types it does own.

bon against typed-builder and derive_builder

The README names its inspirations and links a comparison page, and it does not pretend to be first. What it adds over the crates it lists is specific enough to state without guessing at their internals.

`typed-builder` and `derive_builder` are struct builders. bon does struct builders as well, and adds two things that a struct-only crate does not cover: builders for free functions, which is where named arguments and partial application live, and builders for associated methods. If your problem is constructing a struct, either of them solves it.

`buildstructor` is listed first in the acknowledgments, and bon describes itself as designed with many lessons learned from it. The README does not itemise those lessons, so the honest statement is that bon is a successor in intent rather than a drop-in replacement.

Where bon is clearly ahead is the compatibility surface. Because the derive form and the attribute form generate compatible APIs, a struct can move from a derived builder to a hand-written `new` without touching call sites. And because bon compiles to nothing at runtime, the no_std story, which is why `default-features = false` works without `std` or `alloc`, is a feature the others may not share.

If you only need struct builders, read the alternatives page before adding a second dependency. If you need named function arguments, the comparison is closer than it looks.

Editorial conclusion

Adopt bon when your API has functions or methods with optional or numerous parameters, where named arguments and partial application are the reason to add a builder, and start with #[builder] on a free function to see whether it fits before converting a struct. Do not adopt it for structs with two required fields, and do not write code that calls a setter twice, because that is a compile error by design. Verify first the real minimum Rust version on your toolchain, because the 1.88.0 badge conflicts with the pre-let-else comment in the workspace Clippy configuration.

Frequently asked questions

What is bon and what does it generate?

bon is a Rust crate that generates compile-time-checked builders for structs, functions and methods, and it provides partial application with optional and named parameters. It also works with no_std when you disable the default features.

How do I add bon to a Rust project?

Add bon = "3.10" under [dependencies] in Cargo.toml. For no_std environments, set default-features = false to opt out of the std and alloc features.

What is the minimum supported Rust version for bon?

The README badge states MSRV 1.88.0, though the workspace Clippy configuration contains a comment about targeting a pre-let-else MSRV, which does not match that badge.

Can bon guarantee that a builder is fully filled in?

Yes. It uses the typestate pattern so required parameters must be set before build() or call() exists, and calling the same setter twice is a compile error rather than an overwrite.

Which crates is bon based on or inspired by?

The acknowledgments name buildstructor, typed-builder and derive_builder as heavy inspiration, with lessons learned from each, and the guide links a page comparing alternatives.

How do I switch between derive(Builder) and builder on new?

The README states that #[derive(Builder)] on a struct generates an API fully compatible with #[builder] on a new() method when the signature resembles the struct's fields, and there is a dedicated Compatibility page on making that switch.

Official sources

  1. elastio/bon on GitHub
  2. License: Apache-2.0
  3. Project website
  4. README
  5. Releases
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/elastio-bon.svg)](https://hysenlabs.com/projects/elastio-bon)