Framework
dropbox/pb-jelly avatar
dropbox/pb-jelly

pb-jelly: Dropbox's protobuf generator for Rust, built for zero-copy decoding

A protobuf code generation framework for the Rust language developed at Dropbox.

613 stars28 forksRustApache-2.0

At a glance

What is it?
pb-jelly generates Rust structs from .proto files with Rust-specific extensions such as `[(rust.zero_copy)=true]` for `Lazy<bytes::Bytes>` fields. It came out of Dropbox's need to stop serializing protobuf payloads twice on the way through its RPC stack, and it keeps the generated API close to rust-protobuf for easier migration.
Who is it for?
pb-jelly fits a Rust service that already depends on protobuf and needs generated code that plays well with an RPC stack doing its own encoding, especially where large byte payloads should be decoded without copying. It does not fit a project that wants a stable public API, because the crates are versioned at 0.0.x and the last push was on 2026-06-25, and it does not fit a team that would rather not maintain a protoc plugin and a generation crate.
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 last received commits 120 days ago.
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 23, 2026, and from our analysis. They are not legal advice.

Editorial analysis

The origin is a double serialization in Dropbox's own RPC stack

pb-jelly is a protobuf code generation framework for the Rust language, developed at Dropbox. The README explains why it exists in unusually specific terms. The implementation was first written in 2016 to satisfy the need of shuffling large amounts of bytes in Dropbox's Storage System, Magic Pocket. Before that, Dropbox used rust-protobuf, and the generated APIs were deliberately kept exactly the same to make migration easy. The problem was that serializing Rust structs to proto messages and then serializing them again in the RPC layer meant multiple copies, and the same happened in reverse on the parsing stack. Taking control of the implementation and integrating it into the RPC stack end to end removed those extra copies. That origin explains the shape of the tool. It is not a general protobuf generator with Rust output, it is a generator designed around a service that does its own framing.

The extension table is the real feature list

pb-jelly describes its features as functional Rust-minded proto extensions, and the README enumerates them in a table with the extension name, the type it applies to, and an example link. The headline one is `(rust.zero_copy)=true`, which generates a field of type `Lazy<bytes::Bytes>` for proto bytes fields so the payload is not copied during deserialization. Then `(rust.box_it)=true` generates a `Box<Message>` field, `(rust.type)="type"` generates a custom field type, `(rust.preserve_unrecognized)=true` keeps unknown proto fields in an `_unrecognized` struct field, and `(rust.nullable_field)=false` generates non-nullable field types. Two of the extensions target enums rather than fields: `(rust.nullable)=false` generates oneofs as non-nullable and fails on deserialization, and `(rust.err_if_default_or_unknown)=true` does the equivalent for enums so an unknown variant is an error rather than a silent default. `(rust.closed_enum)=true` generates only a closed enum that fails deserialization on unknown values but is easier to work with in Rust. The pattern is consistent throughout. You opt into Rust semantics per field or per enum instead of accepting whatever the proto defaults imply.

Generation is split across crates so large schemas stay compilable

The generator produces separate crates per module, with an option for a crate per directory, and it autogenerates the `Cargo.toml` for each. That is a scaling decision rather than a style one. A schema with hundreds of modules generated into a single crate produces one enormous compilation unit, and splitting it lets the compiler work in parallel and lets downstream teams depend on the parts they need. The workspace layout in the repository matches this, with `pb-jelly` and `pb-jelly-gen` as the two members, and `examples` and `pb-test` excluded from the workspace. The runtime crate `pb-jelly` holds the traits and structs that the generated code depends on, including `Message` and `Lazy`, and the generator crate `pb-jelly-gen` is what plugs into `protoc`. There is also a `CHANGELOG.md`, and the release history shows the crates are still published at 0.0.x, with v0.0.18 on 2026-06-25 after v0.0.17 in November 2024 and v0.0.16 in January 2024.

Two dependencies, and a generation crate you have to write yourself

The README says there are only two crates you need, `pb-jelly` and `pb-jelly-gen`, and they go into your `Cargo.toml` like this:

toml
[dependencies]
pb-jelly = "0.0.18"

The generator is added as a dependency of a separate generation crate, which then invokes `protoc` for you when you run `cargo run` inside it:

toml
[dependencies]
pb-jelly-gen = "0.0.18"

The reason for that extra crate is stated in the README as well. The goal is eventually to have generation happen inside a `build.rs` with `pb-jelly-gen` as a build dependency, but that is blocked on a known Cargo limitation. Until that is resolved, a generation crate is the workaround. The repository ships `examples/examples_gen` as a worked example of that crate. If you already invoke `protoc` yourself for other languages, you can skip it and pass the plugin directly, using `--rust_out=codegen.py` as a rust plugin.

Running the plugin by hand is two commands

If you would rather not add a crate, the manual path is documented. You build the generator with `cargo build` inside `pb-jelly-gen`, then point `protoc` at the plugin binary with `protoc --plugin=protoc-gen-jellyrust=pb-jelly-gen/target/debug/protoc-gen-jellyrust --jellyrust_out=generated/ input.proto`.

The `protoc-gen-jellyrust` name is the plugin protocol name, and `--jellyrust_out` selects the generator while `generated/` is where the output goes. Either way you need `protoc` installed, and the README points at the upstream project and its releases page for precompiled binaries, with Homebrew as the macOS option through `brew install protobuf`. Two smaller details are worth knowing. Comments on proto fields are retained in the generated Rust, which matters a lot for generated code that a human has to debug. And Serde support is available through `(rust.serde_derive)=true` at the file level, but the README warns that it is not compliant with the JSON protobuf specification, so wire formats that rely on standard JSON mapping will not match.

What pb-jelly does not solve for you

The gaps are structural rather than missing features. There is no single crate that does everything, because the build script limitation still forces the extra generation crate into your repository. The Serde integration is not JSON-spec compliant, so a service that speaks protobuf over JSON cannot treat the two as interchangeable. The crate versions are pre-release at 0.0.x, which in Cargo semantics means the API is not covered by semver guarantees, so a patch-level release can change generated code shape. And the whole approach requires owning a `protoc` plugin, which means the generated code and the runtime crate have to move together. If your team is starting a new service with a small schema and a JSON API, that machinery is a lot of setup for the benefit, and a generator that produces plain structs with standard serde support is the smaller decision.

Apache 2.0 and a codebase built for an internal service first

The licence is Apache 2.0, declared in the workspace package metadata and in the LICENSE file, which keeps adoption inside a commercial product straightforward. The project is used in several parts of Dropbox, including the Sync Engine and Magic Pocket, and the README notes that other Rust protobuf implementations exist and that Dropbox wanted to share its own as well. That is the honest framing: this is internal infrastructure published outward, and the feature set reflects what a storage system needs, which is why zero-copy bytes handling, closed enums, and crate-per-module generation all appear before anything about ergonomics for small schemas. The repository also carries a `rustfmt.toml`, a `ci` directory, and a `pb-test` crate excluded from the workspace, and the workspace pins `bytes` 1.0, `byteorder` 1.4, `compact_str` 0.5 with bytes and serde features, `indexmap` 2.0.2, `tempfile`, and `walkdir`, which is a fairly lean dependency set for a protobuf runtime.

Editorial conclusion

pb-jelly fits a Rust service that already depends on protobuf and needs generated code that plays well with an RPC stack doing its own encoding, especially where large byte payloads should be decoded without copying. It does not fit a project that wants a stable public API, because the crates are versioned at 0.0.x and the last push was on 2026-06-25, and it does not fit a team that would rather not maintain a protoc plugin and a generation crate. Before adopting it, read the extensions table and pick only the extensions you will actually use, then check that the generated code style matches what your serde setup expects, since the Serde support is explicitly not compliant with the JSON protobuf specification.

Frequently asked questions

How do I add pb-jelly to a Rust project?

Add two dependencies: `pb-jelly = "0.0.18"` for the runtime traits and structs, and `pb-jelly-gen = "0.0.18"` in a separate generation crate that runs `protoc` for you. The README notes that a build.rs based flow is the eventual goal but is blocked on a Cargo issue.

What does the rust.zero_copy extension do in pb-jelly?

Applied to a field, `(rust.zero_copy)=true` generates a field of type `Lazy<bytes::Bytes>` for proto bytes fields, which supports deserialization without copying the payload. It is a field level extension and has to be enabled per field.

How do I generate Rust code from a .proto file with pb-jelly?

Install `protoc`, then either create an inner generation crate that depends on `pb-jelly-gen` and run `cargo run` in it, or build the generator with `cargo build` and invoke `protoc --plugin=protoc-gen-jellyrust=pb-jelly-gen/target/debug/protoc-gen-jellyrust --jellyrust_out=generated/ input.proto`.

Is pb-jelly's Serde support compatible with the JSON protobuf specification?

No. The README states that Serde support is not compliant with the JSON protobuf specification, so a service that relies on standard JSON mapping will not produce identical output.

How does pb-jelly handle recursive message definitions and enums?

Messages that reference themselves are automatically boxed. For enums, `(rust.closed_enum)=true` generates a closed enum that fails deserialization on unknown values but is easier to work with in Rust, and `(rust.err_if_default_or_unknown)=true` makes an unknown or default variant an error instead of a silent default.

Official sources

  1. dropbox/pb-jelly on GitHub
  2. Issues
  3. License: Apache-2.0
  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/dropbox-pb-jelly.svg)](https://hysenlabs.com/projects/dropbox-pb-jelly)

Was this useful?