The following guides show the changes you'll need to switch your existing code base
from protobuf-javascript
or from protobuf-ts
to Protobuf-ES.
Feature | Protobuf-ES | protobuf-javascript | protobuf-ts |
---|---|---|---|
Initializers | ✅ | ❌ | ✅ |
Plain properties | ✅ | ❌ | ✅ |
JSON format | ✅ | ❌ | ✅ |
Binary format | ✅ | ✅ | ✅ |
TypeScript | ✅ | ❌ | ✅ |
Standard module system | ✅ | ❌ | ✅ |
Tree shaking | ✅ | ❌ | ✅ |
Reflection | ✅ | ❌ | ✅ |
Dynamic messages | ✅ | ❌ | ✅ |
Wrappers unboxing | ✅ | ❌ | ❌ |
Comments | ✅ | ❌ | ✅ |
Deprecation | ✅ | ❌ | ✅ |
proto2 syntax | ✅ | ✅ | ❌ |
proto2 extensions | ✅ | ✅ | ❌ |
With protobuf-javascript
, we mean the official implementation hosted at
github.com/protocolbuffers/protobuf-javascript,
consisting of the code generator protoc-gen-js
and the runtime library google-protobuf
.
Unfortunately, the code it generates feels a bit awkward to use, because it uses getter /
setter methods instead of plain properties.
And if you dig a bit deeper, you'll notice it does not implement the JSON format,
does not support TypeScript,
does not have any reflection capabilities,
does not use a standard module system,
and produces rather large bundles
for the web. Protobuf-ES fixes those issues. It is a modern replacement for protobuf-javascript
.
The following steps show the changes needed to migrate:
Assuming you have installed protoc-gen-es
,
change your compiler invocation as follows:
- protoc -I . helloworld.proto --js_out . -js_opt import_style=commonjs,binary
+ protoc -I . helloworld.proto --es_out .
Note that the output uses ECMAScript modules, the official standard for JavaScript.
Singular scalar fields like string foo
and message fields like Example bar
become
plain properties:
let message = new Example();
- message.setFoo("baz");
- message.setBar(message);
+ message.foo = "baz";
+ message.bar = message;
Optional fields like optional string value
simply become optional properties:
- message.getValue(); // string - might be the default value ""
- if (message.hasValue()) {
- message.getValue(); // string
- }
+ message.value; // string | undefined
Update your import paths for well-known types as follows:
- import { Timestamp } from "google-protobuf/google/protobuf/timestamp_pb";
+ import { Timestamp } from "@bufbuild/protobuf";
// google.protobuf.Timestamp
- let ts = new Timestamp();
- ts.fromDate(someDateObject);
+ let ts = Timestamp.fromDate(someDateObject);
// google.protobuf.Any
declare var example: Example;
- let any = new Any();
- any.pack(example.serializeBinary(), "Example");
+ let any = Any.pack(example);
- any.unpack((packed) => Timestamp.deserializeBinary(packed), "Example");
+ any.unpackTo(example);
Fields using wrapper messages from google/protobuf/wrappers.proto
simply become optional properties. For a field google.protobuf.BoolValue tristate
:
- let value = new BoolValue();
- value.setValue(true);
- message.setTristrate(value);
+ message.tristate = true;
- message.getTristate()?.value;
+ messsage.tristate; // boolean | undefined
Where protobuf-javascript uses goog.collections.map,
we use plain objects.
For a field map<string, int32> map_field
, map access changes as follows:
// setting a value:
- message.getMapField().set("a", 123);
+ message.mapField["a"] = 123;
// retrieving a value:
- message.getMapField().get("a"); // number | undefined
+ message.mapField["a"]; // number | undefined
// clearing all values:
- message.clearMapField();
+ message.mapField = {};
For a field repeated string values
, array access changes as follows:
// accessing the array:
- message.getValuesList();
+ message.values;
// replacing the array:
- message.setValuesList(["a", "b", "c"]);
+ message.values = ["a", "b", "c"];
// adding a value:
- message.addValues("a");
- message.addValues("b");
+ message.values.push("a", "b");
// clearing all values:
- message.clearValues();
+ message.values = [];
Where protobuf-javascript uses getters, has'ers, and a case enumeration, we use an algebraic data type for oneof groups. For the following definition:
message Example {
oneof result {
Example a = 1;
string b = 2;
}
}
Narrowing down the selected field correctly becomes much less cumbersome, because the type system is now aware of the oneof group:
- switch (message.getResultCase()) {
- case Example.ResultCase.A:
- let a = message.getA(); // undefined | Example
- if (a !== undefined) {
- a; // Example
- }
- break;
- // ...
- }
+ switch (message.result.case) {
+ case "a":
+ message.result.value; // Example
+ break;
+ // ...
+ }
// selecting a field:
- message.setB("foo");
+ message.result = { case: "b", value: "foo" };
// clearing the selected field:
- message.clearA();
- message.clearB();
+ message.result = { case: undefined };
Protobuf-ES adds an initializer argument to constructors. Using it is optional:
- let message = new Example();
- message.setFoo("baz");
- message.setBar(true);
+ let message = new Example({
+ foo: "baz",
+ bar: true,
+ });
Using the binary format is a simple change:
let message = new Example();
- let bytes = message.serializeBinary();
+ let bytes = message.toBinary();
- message = Example.deserializeBinary(bytes);
+ message = Example.fromBinary(bytes);
Note that protobuf-javascript does not implement the JSON format. Messages have
a toObject()
method that returns a plain object, but it is very different
from the canonical JSON mapping.
We drop prefixes from enum values.
An enum definition like enum Foo { FOO_BAR = 0; FOO_BAZ = 1; }
becomes:
- MyEnum.MY_ENUM_FOO
+ MyEnum.FOO
Protobuf-ES does not provide a toObject()
method, because the messages it generates already are rather simple objects.
- example.toObject()
+ example
Object.keys(example); // ["foo", "bar"]
Note that you can use toJson()
to convert to an object that matches the JSON
representation.
protobuf-ts
is an open source implementation
of protocol buffers focused on TypeScript. If you are familiar with it, you will probably
recognize many concepts from protobuf-ts
in Protobuf-ES. To some degree, that is because
many bits are from the same author, but also because they have proven themselves.
So why add another implementation? protobuf-ts
comes with several RPC implementations,
uses interfaces for messages (which is nice, but also has some downsides), and is married
to the TypeScript compiler API to generate code, so it is not straight-forward to write
plugins based on it. You can think of Protobuf-ES as a refined version of protobuf-ts
,
that is suitable as a foundation for other projects to build upon.
The following steps show the changes needed to migrate:
Assuming you have installed protoc-gen-es
,
change your compiler invocation as follows:
- protoc -I . helloworld.proto --ts_out . -ts_opt long_type_bigint,output_javascript
+ protoc -I . helloworld.proto --es_out .
With protobuf-ts
you are always using locally generated versions of well-known types.
With Protobuf-ES, you import them from @bufbuild/protobuf:
- import { Timestamp } from "./google/protobuf/timestamp_pb";
+ import { Timestamp } from "@bufbuild/protobuf";
There are slight API changes, mostly because Protobuf-ES has instance methods:
// google.protobuf.Any
declare var message: Example;
declare var any: Any;
- any = Any.pack(message, Example);
+ any = Any.pack(message);
- Any.contains(any, Example);
+ any.is(Example);
- message = Any.unpack(any, Example);
+ any.unpackTo(message);
// google.protobuf.Timestamp
declare var someDate: Date;
let ts = Timestamp.fromDate(someDate);
- someDate = Timestamp.toDate(ts);
+ someDate = ts.toDate();
Fields using wrapper messages from google/protobuf/wrappers.proto
simply become optional properties. For a field google.protobuf.BoolValue tristate
:
- message.triState = BoolValue.create(true);
+ message.tristate = true;
- message.tristate?.value;
+ messsage.tristate; // boolean | undefined
// serialize to the binary format
- Example.toBinary(message);
+ message.toBinary();
// serialize to JSON
- Example.toJson(message);
+ message.toJson();
// unchanged
Example.fromBinary();
Example.fromJson();
- let message = Example.create({ foo: "baz" });
+ let message = new Example({ foo: "baz" });
declare var message: Example;
- let clone = Example.clone(message);
+ let clone = message.clone();
- Example.is(message);
+ isMessage(message, Example);
- for (let field of Example.fields)
+ for (let field of Example.fields.byNumber())
- const Example = new MessageType("Example", [
+ const Example = proto3.makeMessageType("Example", [
{ no: 1, name: "foo", kind: "scalar", T: ScalarType.STRING },
]);
Note that the type of message and enum fields does not need to be deferred:
- { no: 1, name: "foo", kind: "message", T: () => OtherMessage },
+ { no: 1, name: "foo", kind: "message", T: OtherMessage },
In case a message refers to itself, the entire field list can be deferred:
- [{ no: 1, name: "foo", kind: "message", T: Example } ]
+ () => [{ no: 1, name: "foo", kind: "message", T: Example } ]