Migrating from prost to buffa

July 17, 2026 · View on GitHub

A step-by-step guide for migrating an existing prost-based project to buffa.

Where is #[derive(Message)]?

Buffa generates message types from .proto files instead of treating Rust attributes as the schema. Keeping .proto as the source of truth preserves protobuf's cross-language contract and its surrounding tooling, including schema registries and breaking-change checks. To attach buffa implementations to an existing Rust type while retaining that schema, use extern_path and the custom-type options.

A limited derive for self-contained tests and examples is under discussion in #226.

1. Swap dependencies

 # Cargo.toml
 [dependencies]
-prost = "0.13"
-prost-types = "0.13"
+buffa = "0.9"
+buffa-types = "0.9"

 [build-dependencies]
-prost-build = "0.13"
+buffa-build = "0.9"

If you use JSON serialization via pbjson:

 [dependencies]
-pbjson = "0.7"
-pbjson-types = "0.7"
+buffa = { version = "0.9", features = ["json"] }
+buffa-types = { version = "0.9", features = ["json"] }
 serde_json = "1"
-
-[build-dependencies]
-pbjson-build = "0.7"

2. Rewrite build.rs

 // 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()?;

In src/lib.rs, replace prost's per-file include! with buffa's per-package macro:

 pub mod proto {
-    include!(concat!(env!("OUT_DIR"), "/my.package.rs"));
+    buffa::include_proto!("my.package");
 }

Or use .include_file("_include.rs") in the build config and include! that single file — recommended when your protos span multiple packages.

3. Optional message fields

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 = Address {
+    street: "123 Main".into(),
+    ..Default::default()
+}.into();

 // 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();

 // Converting an Option<&Address> back to a field (clones the Address)
+msg.address = opt.cloned().into();

 // 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"))?;  // same

4. Enum fields

Prost 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 = 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();

Buffa keeps the proto SHOUTY_SNAKE_CASE value names as the definitive Rust variants, but it also emits idiomatic UpperCamelCase associated-const aliases (prefix-stripped, the same names prost generates), so Status::Active-style spellings carried over from prost code resolve as-is — in expressions and in match patterns alike.

For proto2 closed enums, Buffa follows closed-enum semantics by default and routes unknown wire values through unknown fields. If you need prost-style direct access to unknown values while migrating, configure open_enums_in for the affected enum type, field, or package prefix.

5. Encoding API

 use buffa::Message;  // was: use prost::Message;

 // Encode to Vec — unchanged
 let bytes = msg.encode_to_vec();

 // Encode to buffer — no Result; panics only past the 2 GiB
 // protobuf limit (try_encode is the error-returning twin)
-msg.encode(&mut buf)?;
+msg.encode(&mut buf);

 // encoded_len — same name, returns u32 (try_encoded_len for no-panic)
-let len = msg.encoded_len();
+let len = msg.encoded_len() as usize;

6. Decoding API

 // 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)?;

Error types

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 */ }
}

7. Well-known type imports

-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.Int32Valuei32, 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 value

8. JSON serialization

If 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.

9. New capabilities

These are buffa features with no prost equivalent:

Zero-copy views

use buffa::MessageView;
use my_crate::pkg::__buffa::view::PersonView;

let view = PersonView::decode_view(&bytes)?;
println!("name: {}", view.name);  // &str, no allocation

let owned: Person = view.to_owned_message()?;

OwnedView for async/RPC use

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. Codegen emits a per-message PersonOwnedView wrapper with field accessor methods:

let view = PersonOwnedView::decode(bytes)?;
println!("name: {}", view.name());  // accessor, zero-copy, 'static

Unknown field preservation

Buffa preserves unknown fields by default. Messages decoded from a newer schema version retain the unknown fields through re-encoding.

Linear-time serialization

Buffa's two-pass compute_size() / write_to() model avoids the quadratic size computation that affects prost with deeply nested messages.

10. Feature comparison

Features that prost supports but buffa does not (yet):

prost featurebuffa 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, both package-level (.extern_path(".pkg", "::crate")) and per-type (.extern_path(".google.protobuf.Timestamp", "::pbjson_types::Timestamp")) mappings. A per-type mapping to a non-buffa crate requires .generate_views(false), or map to a buffa-generated crate instead — see External type paths.
type_attribute(path, attr)Supported. Same API, plus message_attribute / enum_attribute / oneof_attribute for narrower targeting. (For serde, prefer generate_json(true), which emits the proto3-canonical JSON impls.)
field_attribute(path, attr)Supported. Same API.
service_generator(...)Not supported. Services codegen is planned.
#[derive(prost::Message)]No derive; generate from .proto or use extern_path for an existing Rust type. See Where is #[derive(Message)]?.
prost::Name traitbuffa::MessageName — same shape (PACKAGE, NAME, plus FULL_NAME and TYPE_URL), but all four are &'static str consts computed at codegen time rather than runtime format! calls. Replace M::full_name() with M::FULL_NAME and M::type_url() with M::TYPE_URL. Implemented for both owned messages and view types.

Features that buffa has but prost does not:

buffa featureprost equivalent
MessageField<T> (deref to default)Option<Box<T>> (manual unwrap)
EnumValue<E> (typed open enums)i32 (raw integer)
Zero-copy MessageView typesNone
Unknown field preservationNone
Built-in proto-canonical JSONRequires pbjson / pbjson-build
Two-pass cached-size encodingNone (recomputes at each level)
Proto2 closed enum supportPartial (no closed-enum routing to unknown fields)
Protobuf editions supportNone
Runtime reflection — DescriptorPool / DynamicMessage in buffa-descriptor, plus ReflectMessage on generated types via reflect_mode (see Runtime reflection)Requires the separate prost-reflect crate; no reflection on prost-generated types themselves