Protocol Buffers to ReScript codegen with JSON codecs and gRPC-web client stubs.
rescript-grpc provides a complete .proto → ReScript codegen pipeline:
-
protoc-gen-rescript - Rust-based protoc plugin generating type-safe ReScript
-
@rescript-grpc/runtime - JSON encode/decode runtime (proto3 JSON mapping)
-
gRPC-web client stubs - Optional async HTTP clients for services
-
Type-safe ReScript types from
.protodefinitions -
JSON encode/decode codecs following proto3 JSON mapping specification
-
Polymorphic variants for enum types
-
Optional gRPC-web client stubs (opt-in via
--rescript_opt=grpc) -
Topological sorting ensures message dependencies compile correctly
-
Proto3 field semantics (scalars required, messages optional,
optionalkeyword supported) -
Zero npm dependencies (uses Deno or works standalone)
┌─────────────────────────────────────────────────────────────────┐
│ Build Time (protoc) │
├─────────────────────────────────────────────────────────────────┤
│ .proto ──→ protoc-gen-rescript │
│ │ │
│ └──→ ServiceProto.res (ReScript types) │
│ │ │
│ ├─ Message modules with make() │
│ ├─ toJson()/fromJson() codecs │
│ ├─ Enum toInt()/fromInt() │
│ └─ ServiceClient (if --grpc) │
└─────────────────────────────────────────────────────────────────┘
┌─────────────────────────────────────────────────────────────────┐
│ Runtime │
├─────────────────────────────────────────────────────────────────┤
│ open UserProto │
│ │
│ let user = User.make(~name="Alice", ~id=42, ~status=#Active) │
│ let json = User.toJson(user) │
│ let decoded = User.fromJson(json) │
│ │
│ // With gRPC-web client │
│ let result = await UserServiceClient.getUser( │
│ ~config={baseUrl: "http://localhost:8080", headers: None}, │
│ ~request=GetUserRequest.make(~id=1) │
│ ) │
└─────────────────────────────────────────────────────────────────┘# From source
cargo install --path protoc-gen-rescript
# Or build locally
cd protoc-gen-rescript && cargo build --release
# Add target/release to PATH# Basic generation (types + JSON codecs)
protoc --rescript_out=./src ./protos/user.proto
# With gRPC-web client stubs
protoc --rescript_out=./src --rescript_opt=grpc ./protos/user.proto// user.proto
syntax = "proto3";
package example;
enum Status {
STATUS_UNKNOWN = 0;
STATUS_ACTIVE = 1;
STATUS_INACTIVE = 2;
}
message User {
string name = 1;
int32 id = 2;
optional string email = 3; // Optional scalar
Status status = 4;
repeated string tags = 5;
Address address = 6; // Message fields always optional
}
message Address {
string street = 1;
string city = 2;
string country = 3;
}
service UserService {
rpc GetUser(GetUserRequest) returns (User);
rpc ListUsers(ListUsersRequest) returns (ListUsersResponse);
}// UserProto.res (generated)
module Status = {
type t = [#StatusUnknown | #StatusActive | #StatusInactive]
let toInt = (v: t): int => ...
let fromInt = (i: int): option<t> => ...
}
module Address = {
type t = { street: string, city: string, country: string }
let make = (~street, ~city, ~country): t => ...
let toJson = (msg: t): Js.Json.t => ...
let fromJson = (json: Js.Json.t): option<t> => ...
}
module User = {
type t = {
name: string,
id: int,
email: option<string>,
status: Status.t,
tags: array<string>,
address: option<Address.t>,
}
let make = (~name, ~id, ~email=?, ~status, ~tags=[], ~address=?): t => ...
let toJson = (msg: t): Js.Json.t => ...
let fromJson = (json: Js.Json.t): option<t> => ...
}
module UserServiceClient = {
type config = { baseUrl: string, headers: option<Js.Dict.t<string>> }
type error = NetworkError(string) | GrpcError(int, string) | DecodeError(string)
let getUser = async (~config, ~request: GetUserRequest.t): result<User.t, error> => ...
let listUsers = async (~config, ~request: ListUsersRequest.t): result<ListUsersResponse.t, error> => ...
}// Example.res
open UserProto
let alice = User.make(
~name="Alice",
~id=1,
~email="alice@example.com",
~status=#StatusActive,
~tags=["admin", "developer"],
~address=Address.make(~street="123 Main St", ~city="SF", ~country="USA"),
)
// JSON round-trip
let json = User.toJson(alice)
let decoded = User.fromJson(json)
// gRPC-web call
let fetchUser = async (id: int) => {
let config = { baseUrl: "http://localhost:8080", headers: None }
let request = GetUserRequest.make(~id)
await UserServiceClient.getUser(~config, ~request)
}rescript-grpc/
├── protoc-gen-rescript/ # Rust protoc plugin
│ ├── Cargo.toml
│ └── src/
│ ├── main.rs # Plugin entry point
│ ├── generator.rs # Code generation logic
│ └── templates.rs # ReScript code templates
├── runtime/ # @rescript-grpc/runtime
│ ├── rescript.json
│ └── src/
│ ├── Json.res # JSON encode/decode helpers
│ └── Fetch.res # Fetch API bindings for gRPC-web
├── codec/ # WASM codec (optional, for binary proto)
│ ├── Cargo.toml
│ └── src/lib.rs
└── examples/
└── basic/
├── protos/user.proto
└── src/
├── UserProto.res # Generated
└── Example.res # Usage exampleThe generated codecs follow the proto3 JSON mapping specification:
| Proto Type | JSON Representation |
|---|---|
|
number |
|
string (for precision) |
|
number |
|
boolean |
|
string |
|
base64 string |
|
integer value |
|
object |
|
array |
-
✓ protoc plugin (Rust)
-
✓ Type-safe message generation
-
✓ JSON encode/decode codecs
-
✓ Enum polymorphic variants
-
✓ gRPC-web client stubs
-
✓ Topological message sorting
-
❏ Streaming RPC support
-
❏ Server-side handlers
-
❏ Binary protobuf codec (WASM)
-
❏ Well-known types (google.protobuf.*)
-
❏ OneOf support
-
rescript-full-stack - ReScript ecosystem map
-
rescript-wasm-runtime - WASM runtime for ReScript
-
rescript-openapi - OpenAPI to ReScript codegen
-
prost - Rust Protocol Buffers implementation
MPL-2.0. See LICENSE.txt.