Protobuf to Rust Converter
Paste a .proto schema. Get Rust structs and enums with serde derives, snake_case fields, and Option<T> on nullable submessages — ready to drop into your crate.
Input (.proto schema)
Output (Rust)
What this tool does
You have a Protocol Buffers schema and a Rust service or client that needs to talk to it. The standard path is prost (or tonic for gRPC) wired into a build.rs script — solid, but overkill when you just want to sketch the types and check shapes against your code. This converter turns the schema into struct definitions in your browser. Paste, copy, drop into src/types.rs, keep moving.
Output uses #[derive(Debug, Default, Clone, Serialize, Deserialize)] on every struct so it round-trips through JSON via serde, and #[repr(i32)] enums with the zero-value variant marked #[default] so Default::default() matches proto3 semantics. Singular nested messages become Option<T> — Rust requires explicit nullability, and that matches what prost generates. Vec<T> for repeated fields and HashMap<K, V> for maps both get #[serde(default)] so missing fields deserialize to empty containers instead of erroring out.
Type mapping follows the proto3 JSON mapping spec: string/bytes → String/Vec<u8>, the various sized integers map to i32/i64/u32/u64, and the floats land on f32/f64. Field names get converted to snake_case (already the Rust convention, so most fields pass through unchanged), and enum variants drop the screaming-snake enum prefix — ORDER_STATUS_UNSPECIFIED in enum OrderStatus becomes Unspecified, the way you would write it by hand. Conversion runs entirely client-side; nothing about your schema leaves the page.
How to use it
Three steps. Output is paste-ready for any Rust crate that has serde in its dependencies.
Paste your .proto schema
Drop the schema into the left editor. syntax = "proto3"; at the top is optional — the parser handles nested message blocks, enum declarations, oneof, map<K, V>, and field options. Imports are recognised but skipped, so paste imported types inline if you depend on them.
Field names follow Rust conventions automatically: order_id stays order_id, shippingAddress (if you have any camelCase in your schema) becomes shipping_address. Struct and enum names stay PascalCase.
Read the output
On the right: a flat list of pub struct and pub enum declarations. Enums come first, then messages in declaration order, with nested types ahead of their parents. The use serde::{Deserialize, Serialize}; line is always there; use std::collections::HashMap; only appears if your schema has map fields.
Drop into your crate
Paste the output into a module file. As long as you have serde = { version = "1", features = ["derive"] } and serde_json in your Cargo.toml, it compiles. From there it is regular Rust — wire it into a reqwest client, a Cloudflare Worker, an axum handler, anywhere you need typed access to the message data.
When this actually saves time
Sketching types for a new Rust service
You are building a Rust service that consumes a gRPC-gateway endpoint, but you do not want to set up a full build.rs with prost just yet. Paste the .proto, drop the structs into types.rs, JSON-decode the response, ship the prototype.
Reviewing a Protobuf API change
A backend teammate added fields to a message. You want to see how the Rust shape will change without rebuilding the whole crate. Paste the new .proto, diff the output against your existing types, leave a focused review comment.
Cross-checking prost-generated code
Your build uses prost with custom type_attribute rules and you want a clean reference of what the unmodified types look like. Paste the schema here for a side-by-side. Useful when planning attribute changes or migrating to a different codegen pipeline.
CLI tools and one-off scripts
You are writing a small Rust CLI that calls a JSON API backed by Protobuf. Wiring up prost just for a 100-line tool is more ceremony than the job needs. Grab the structs from here, derive serde, ship the binary.
Common questions
Is my schema sent anywhere?
No. The parser and Rust emitter both run as JavaScript in your browser. Open DevTools, watch the Network tab while you paste — zero requests. Useful when your schema includes internal type names, package paths, or anything you would not want to ship to a third-party service.
Why are nested messages wrapped in Option?
Rust does not have nullable references the way Java or Go do — every value is either present or you use Option<T> to make absence explicit. Singular submessages in proto3 are nullable on the wire (the field tag may be missing entirely), so the idiomatic Rust shape is Option<Address>, not Address. This matches what prost generates. Scalars and enums are not wrapped because their zero values (empty string, 0, false, the #[default] enum variant) are valid Rust values.
Why are 64-bit ints typed as i64/u64 instead of strings?
In Rust there is no precision concern — i64 and u64 are first-class types, unlike JavaScript where 64-bit values are strings. If you are deserializing JSON produced by a strict proto3 JSON encoder, the values will arrive as strings; serde will not coerce automatically. Add #[serde(with = "serde_with::DisplayFromStr")] or a custom deserializer on those fields if you hit that case.
Why use HashMap instead of BTreeMap?
HashMap matches what prost emits for map<K, V> fields and is the right default — proto map fields are unordered. If you need stable iteration order, change the type to BTreeMap by hand; the rest of the code does not need to change.
Should I use this output with prost or tonic?
These are plain serde-derived structs, not prost-encoded ones. They round-trip through proto3 JSON via serde_json, but they will not encode/decode the binary wire format. If you need binary protobuf encoding, run prost via a build.rs step. If you need gRPC, use tonic. This converter is for the JSON-over-HTTP case (gRPC-gateway, Connect, JSON transcoding).
Does it handle oneof?
Each oneof field is emitted as a regular struct field. The output does not enforce the "exactly one" constraint that oneof implies — for that you would need a Rust enum with a variant per case, which depends on your runtime semantics. If you need strict oneof handling, edit the output by hand into a pub enum.
Related tools
If you are working with Protobuf, JSON, and Rust, these pair well: