Skip to content

Alternatives

There are several other existing alternative crates for generating builders. bon was designed based on many lessons learned from them. A table that compares the builder crates with some additional explanations is below.

Featurebonbuildstructortyped-builderderive_builder
Builder for structs
Builder for functions
Builder for methods
Panic safebuild() returns Result
Option<T> makes members optional
T -> Option<T> is non-breakingdocsvia attr strip_optionvia attr strip_option
Generates T::builder() methodonly Builder::default()
Into conversion in settersopt-inimplicitopt-inopt-in
Validation in the finishing functiondocsdocsdocs
Validation in setters (fallible setters)✅ attr with = closureTryInto via attr try_setter
Custom methods on builder✅ via direct impl block✅ via attr mutators✅ via direct impl block
impl Trait, elided lifetimes support
Builder for fn hides original fn
Special setters for collections(see below)
Builder by &self/&mut self
Generates nice docs

Function Builder Paradigm Shift

If you ever hit a wall 🧱 with typed-builder or derive_builder, you'll have to hack something around their derive attributes syntax on a struct. With bon or buildstructor you can simply change the syntax from #[derive(Builder)] on a struct to a #[builder] on a function to gain more flexibility at any time 🤸. It is guaranteed to preserve compatibility, meaning it's not a breaking change.

Example

Suppose you already had a struct like the following with a builder derive:

rust
use bon::Builder;

#[derive(Builder)]
pub struct Line {
    x1: u32,
    y1: u32,

    x2: u32,
    y2: u32,
}

// Suppose this is your users' code
Line::builder().x1(1).y1(2).x2(3).y2(4).build();

Then you decided to refactor 🧹 your struct's internal representation by extracting a private utility Point type:

rust
use bon::Builder;

#[derive(Builder)]
pub struct Line {
    point1: Point,
    point2: Point,
}

// Private
struct Point {
    x: u32,
    y: u32,
}

// Suppose this is your users' code (it no longer compiles)
Line::builder().x1(1).y1(2).x2(3).y2(4).build(); 
//                 ^^- error[E0599]: no method named `x1` found for struct `LineBuilder`
//                                   available methods: `point1(Point)`, `point2(Point)`

There are two problems with #[derive(Builder)] syntax in this case:

  1. This refactoring becomes a breaking change to Line's builder API 😢.
  2. The private utility Point type leaks through the builder API via point1, and point2 setters 😭.

The fundamental problem is that the builder's API is coupled ⛓️ with your struct's internal representation. It's literally derived from the fields of your struct.

Suffering

If you were using typed-builder or derive_builder, you'd be stuck for a while trying to find the magical 🪄 combination of attributes that would let you do this change without breaking compatibility or leakage of the private Point type.

With no solution in sight 😮‍💨, you'd then fall back to writing the same builder manually. You'd probably expand the builder derive macro and edit the generated code directly, which, ugh... hurts 🤕.

However, that would be especially painful with typed-builder, which generates a complex typestate that is not human-readable and maintainable enough by hand. It also references some internal #[doc(hidden)] symbols from the typed-builder crate. Achoo... 🤧.

TIP

In contrast, bon's type state is human-readable, maintainable, and documented 👍

Behold the Function-Based Builder

This change is as simple as pie 🥧 with bon or buildstructor. The code speaks for itself:

rust
use bon::bon;

// No more derives on a struct. Its internal representation is decoupled from the builder.
pub struct Line {
    point1: Point,
    point2: Point,
}

struct Point {
    x: u32,
    y: u32,
}

#[bon]
impl Line {
    #[builder]
    fn new(x1: u32, y1: u32, x2: u32, y2: u32) -> Self {
        Self {
            point1: Point { x: x1, y: y1 } ,
            point2: Point { x: x2, y: y2 } ,
        }
    }
}

// Suppose this is your users' code (it compiles after this change, yay 🎉!)
Line::builder().x1(1).y1(2).x2(3).y2(4).build();

Ah... Isn't this just so simple and beautiful? 😌 The fun part is that the constructor method new that we originally abandoned comes back to heroically save us ⛑️ at no cost, other than a star ⭐ on bon's Github repo maybe 🐈?

And you know what, our old friend new doesn't feel offended for being abandoned. It doesn't even feel emotions, actually 🗿. But it's happy to help you 🫂.

Moreover, it offers you a completely new dimension of flexibility:

  • Need some validation? Just change the new() method to return a Result. The generated build() method will then become fallible.
  • Need to do an async operation in the constructor? Just make your constructor async and your build() will return a Future.
  • Need some adrenaline 💉? Just add unsafe, and... you get the idea 😉.

The chances of hitting a wall with function builders are close to zero, and even if you ever do, you still have access to the Typestate API in bon for even more flexibility 💪.

Generated Docs Comparison

Here is a table that compares the rustdoc output for builders generated by different crates based on different syntax.

Underlying syntaxbonbuildstructortyped-builderderive_builder
StructLinkLinkLinkLink
FunctionLink
MethodLinkLink

All builders were configured to produce roughly similar builder APIs. The notable exceptions are:

  • buildstructor doesn't support #[builder(default)] and #[builder(into)]-like annotations;
  • buildstructor doesn't support doc comments on function arguments;
  • derive_builder doesn't support typestate-based builders;

Docs generated by typed-builder and buildstructor suffer from the problem of noisy generics. This problem significantly worsens with the number of fields/arguments in structs/functions. bon solves this problem by using a trait-based design for its typestate.

bon also includes the default values assigned via #[builder(default)] in the docs (more examples here).

Special Setter Methods for Collections

Other builder crates provide a way to generate methods to build collections one element at a time. For example, buildstructor even generates such methods by default:

rust
#[derive(buildstructor::Builder)]
struct User {
    friends: Vec<String>
}

fn main() {
    User::builder()
        .friend("Foo")
        .friend("Bar")
        .friend("`String` value is also accepted".to_owned())
        .build();
}

TIP

Why is there an explicit main() function in this code snippet 🤔? It's a long story explained in a blog post (feel free to skip).

This feature isn't available today in bon, but it's planned for the future. However, it won't be enabled by default; rather, it will be opt-in like it is in derive_builder.

The problem with this feature is that a setter that pushes an element into a collection like that may confuse the reader if only one element is pushed. This may hide the fact that the member is actually a collection called friends in the plural. However, this feature is still useful to provide backwards compatibility when changing the type of a member from T or Option<T> to Collection<T>.

Alternatively, bon provides a separate solution. bon exposes the following macros that provide convenient syntax to create collections.

Vec<T>[T; N]*Map<K, V>*Set<K, V>
bon::vec![]bon::arr![]bon::map!{}bon::set![]

These macros share a common feature that every element of the collection is converted with Into to shorten the syntax if you, for example, need to initialize a Vec<String> with items of type &str. Use these macros only if you need this behaviour, or ignore them if you want to be explicit in code and avoid implicit Into conversions.

rust
use bon::Builder;

#[derive(Builder)]
struct User {
    friends: Vec<String>
}

User::builder()
    .friends(bon::vec![
      "Foo",
      "Bar",
      "`String` value is also accepted".to_owned(),
    ])
    .build();

Another difference is that fields of collection types are considered required by default in bon, which isn't the case in buildstructor.