A step-by-step guide for migrating an existing prost-based project to buffa.
# Cargo.toml
[dependencies]
-prost = "0.13"
-prost-types = "0.13"
+buffa = "0.3"
+buffa-types = "0.3"
[build-dependencies]
-prost-build = "0.13"
+buffa-build = "0.3"If you use JSON serialization via pbjson:
[dependencies]
-pbjson = "0.7"
-pbjson-types = "0.7"
+buffa = { version = "0.3", features = ["json"] }
+buffa-types = { version = "0.3", features = ["json"] }
serde_json = "1"
-
-[build-dependencies]
-pbjson-build = "0.7" // build.rs
fn main() {
- prost_build::compile_protos(
- &["proto/my_service.proto"],
- &["proto/"],
- ).unwrap();
+ buffa_build::Config::new()
+ .files(&["proto/my_service.proto"])
+ .includes(&["proto/"])
+ .compile()
+ .unwrap();
}With configuration:
-prost_build::Config::new()
- .btree_map(&["."])
- .bytes(&["my_pkg.MyMessage.data"])
- .type_attribute(".", "#[derive(serde::Serialize)]")
- .out_dir("src/generated/")
- .compile_protos(&["src/items.proto"], &["src/"])?;
+buffa_build::Config::new()
+ .files(&["src/items.proto"])
+ .includes(&["src/"])
+ .out_dir("src/generated/")
+ .generate_json(true) // built-in, proto-conformant JSON
+ .compile()?;The include!() pattern in src/lib.rs is unchanged.
This is the biggest API change. Prost uses Option<Box<M>>, which requires explicit unwrapping. Buffa uses MessageField<M>, which derefs to a default instance when unset.
// Reading a sub-message field
-if let Some(ref addr) = msg.address {
- println!("{}", addr.street);
-}
+println!("{}", msg.address.street); // "" if unset
+if msg.address.is_set() {
+ // handle set case
+}
// Setting
-msg.address = Some(Box::new(Address {
- street: "123 Main".into(),
- ..Default::default()
-}));
+msg.address = buffa::MessageField::some(Address {
+ street: "123 Main".into(),
+ ..Default::default()
+});
// Or use get_or_insert_default for piecewise construction
+msg.address.get_or_insert_default().street = "123 Main".into();
// Clearing
-msg.address = None;
+msg.address = buffa::MessageField::none();
// Converting to Option for interop
-let opt: Option<&Address> = msg.address.as_deref();
+let opt: Option<&Address> = msg.address.as_option();
// Enforcing presence (e.g. in an RPC handler)
-let addr = msg.address.ok_or_else(|| Error::missing("address"))?;
+let addr = msg.address.ok_or_else(|| Error::missing("address"))?; // sameProst represents all enum fields as i32. Buffa uses EnumValue<E> for open enums (proto3 default) and bare E for closed enums (proto2).
// Setting an enum field
-msg.status = Status::Active as i32;
+msg.status = buffa::EnumValue::from(Status::ACTIVE);
+// or simply:
+msg.status = Status::ACTIVE.into();
// Reading
-match Status::try_from(msg.status) {
- Ok(Status::Active) => { /* ... */ }
- Ok(s) => { /* other known variant */ }
- Err(_) => { /* unknown value */ }
-}
+match msg.status {
+ buffa::EnumValue::Known(Status::ACTIVE) => { /* ... */ }
+ buffa::EnumValue::Known(s) => { /* other known variant */ }
+ buffa::EnumValue::Unknown(v) => { /* unknown value */ }
+}
+// or use direct comparison:
+if msg.status == Status::ACTIVE { /* ... */ }
// Getting the raw integer
-let raw: i32 = msg.status;
+let raw: i32 = msg.status.to_i32(); use buffa::Message; // was: use prost::Message;
// Encode to Vec — unchanged
let bytes = msg.encode_to_vec();
// Encode to buffer — now infallible (no Result)
-msg.encode(&mut buf)?;
+msg.encode(&mut buf);
// encoded_len — now compute_size (returns u32, caches result)
-let len = msg.encoded_len();
+let len = msg.compute_size() as usize; // Decode from bytes::Buf — now takes &mut
-let msg = Person::decode(buf)?;
+let msg = Person::decode(&mut buf)?;
// Decode from slice — use the convenience method
-let msg = Person::decode(&*bytes)?;
+let msg = Person::decode_from_slice(&bytes)?;
// Merge
-msg.merge(buf)?;
+msg.merge_from_slice(&bytes)?;Prost's DecodeError is an opaque struct with a description string. Buffa's DecodeError is a structured enum you can match on:
match Person::decode_from_slice(&bytes) {
Ok(msg) => { /* ... */ }
Err(buffa::DecodeError::InvalidUtf8) => { /* bad string */ }
Err(buffa::DecodeError::RecursionLimitExceeded) => { /* too deep */ }
Err(e) => { /* other decode error */ }
}-use prost_types::Timestamp;
-use prost_types::Duration;
-use prost_types::Any;
-use prost_types::Struct;
-use prost_types::Value;
+use buffa_types::google::protobuf::Timestamp;
+use buffa_types::google::protobuf::Duration;
+use buffa_types::google::protobuf::Any;
+use buffa_types::google::protobuf::Struct;
+use buffa_types::google::protobuf::Value;Important: Prost maps some wrapper types to Rust primitives (google.protobuf.Int32Value → i32, google.protobuf.Empty → ()). Buffa does not — all well-known types are proper structs:
// Wrapper types
-let val: Option<i32> = msg.optional_int; // prost maps to primitive
+let val: buffa::MessageField<buffa_types::google::protobuf::Int32Value> = msg.optional_int;
+let inner: i32 = msg.optional_int.value; // access the wrapped valueIf you were using pbjson / pbjson-build for proto-canonical JSON:
// build.rs
-prost_build::Config::new()
- .compile_protos(&["my.proto"], &["proto/"])?;
-pbjson_build::Builder::new()
- .register_descriptors(&descriptor_bytes)?
- .build(&[".my_package"])?;
+buffa_build::Config::new()
+ .files(&["my.proto"])
+ .includes(&["proto/"])
+ .generate_json(true) // built-in, no separate crate needed
+ .compile()?;Usage is the same — serde_json::to_string / serde_json::from_str.
These are buffa features with no prost equivalent:
use buffa::MessageView;
let view = PersonView::decode_view(&bytes)?;
println!("name: {}", view.name); // &str, no allocation
let owned: Person = view.to_owned_message();OwnedView<V> wraps a view with its backing Bytes buffer, producing a 'static + Send + Sync type that works with tower, tokio::spawn, and RPC frameworks:
use buffa::view::OwnedView;
let view = OwnedView::<PersonView>::decode(bytes)?;
println!("name: {}", view.name); // Deref, zero-copy, 'staticBuffa preserves unknown fields by default. Messages decoded from a newer schema version retain the unknown fields through re-encoding.
Buffa's two-pass compute_size() / write_to() model avoids the quadratic size computation that affects prost with deeply nested messages.
Features that prost supports but buffa does not (yet):
| prost feature | buffa status |
|---|---|
btree_map(&[...]) |
Not supported. Maps always use HashMap. |
bytes(&[...]) |
Supported. .use_bytes_type() for all, or .use_bytes_type_in(&[...]) for specific fields. |
extern_path(proto, rust) |
Supported. Same API: .extern_path(".pkg", "::crate"). |
type_attribute(path, attr) |
Not supported. Use generate_json(true) for serde. |
field_attribute(path, attr) |
Not supported. |
service_generator(...) |
Not supported. Services codegen is planned. |
#[derive(prost::Message)] |
Not provided. Implement Message by hand and use extern_path (see Custom types). |
prost::Name trait |
Not supported. The generated TYPE_URL associated constant covers the common case (Any packing/unpacking). |
Features that buffa has but prost does not:
| buffa feature | prost equivalent |
|---|---|
MessageField<T> (deref to default) |
Option<Box<T>> (manual unwrap) |
EnumValue<E> (typed open enums) |
i32 (raw integer) |
Zero-copy MessageView types |
None |
| Unknown field preservation | None |
| Built-in proto-canonical JSON | Requires pbjson / pbjson-build |
| Two-pass cached-size encoding | None (recomputes at each level) |
| Proto2 closed enum support | Partial (no closed-enum routing to unknown fields) |
| Protobuf editions support | None |