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 safeโœ…โœ…โœ…build() returns Result
Option<T> makes members optionalโœ…โœ…
T -> Option<T> is non-breakingโœ… docsโœ…via attr strip_optionvia attr strip_option
Generates T::builder() methodโœ…โœ…โœ…only Builder::default()
Into conversion in settersopt-inimplicitopt-inopt-in
Validation in the finishing functionโœ… docsโœ… docsโœ… docs
Validation in setters (fallible setters)โœ… attr with = closureโœ… TryInto via attr try_setter
Custom methods on builderโœ… via direct impl blockโœ… via attr mutatorsโœ… via direct impl block
Custom fields on builderโœ… attr fieldโœ… attr via_mutatorsโœ… attr field
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).

It is possible to implement this with bon using custom fields and custom setters. See an example of how this can be done here.

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 members of collection types are considered required by default in bon, which isn't the case in buildstructor.