Skip to content

Serialization

Protobuf-ES uses the same generated schema for wire-compatible binary and ProtoJSON, so both formats stay tied to the same types. Binary is the default for network traffic and storage: faster, preserves unknown fields, and handles schema evolution well. JSON is useful for debugging and for integrations that already speak JSON.

Use toBinary() and fromBinary() for the Protobuf wire format:

import { fromBinary, toBinary } from "@bufbuild/protobuf";
import { type User, UserSchema } from "./gen/example_pb";
declare let user: User;
const bytes: Uint8Array = toBinary(UserSchema, user);
user = fromBinary(UserSchema, bytes);

Use toJson() and fromJson() for ProtoJSON values:

import { fromJson, toJson, type JsonValue } from "@bufbuild/protobuf";
import { type User, UserSchema } from "./gen/example_pb";
declare let user: User;
const json: JsonValue = toJson(UserSchema, user);
user = fromJson(UserSchema, json);

If you want strings instead of JsonValue, use toJsonString() and fromJsonString(). To parse into an existing message, use mergeFromBinary() or mergeFromJson().

toBinary() accepts one option:

  • writeUnknownFields?: boolean: Include unknown fields in the serialized output. By default, unknown fields are preserved and written back out.

fromBinary() accepts one option:

  • readUnknownFields?: boolean: Retain unknown fields while parsing. By default, unknown fields are kept.

fromJson() and fromJsonString() accept:

  • ignoreUnknownFields?: boolean: Ignore unknown properties and unknown enum string values instead of rejecting them.
  • registry?: Registry: Use a registry when parsing google.protobuf.Any and extensions from JSON.

toJson() and toJsonString() accept:

  • alwaysEmitImplicit?: boolean: Emit fields with implicit presence even when they hold default values.
  • enumAsInteger?: boolean: Write enum numbers instead of enum names.
  • useProtoFieldName?: boolean: Use original proto field names instead of lowerCamelCase JSON names.
  • registry?: Registry: Use a registry for google.protobuf.Any and extensions.
  • prettySpaces?: number: Only for toJsonString(). Passed through to JSON.stringify().

See Registries for creating registries, Any with registries for a complete Any example, and Plugin options if you want generated JSON types.

When Protobuf-ES parses binary data, unrecognized fields are stored on the message as $unknown?: UnknownField[] | undefined. When the message is serialized again, those fields are preserved by default.

Extensions use the same storage under the hood.

The public BinaryReader and BinaryWriter classes implement the low-level Protobuf wire format.

import { BinaryWriter, WireType } from "@bufbuild/protobuf/wire";
import { fromBinary } from "@bufbuild/protobuf";
import { UserSchema } from "./gen/example_pb";
const bytes = new BinaryWriter()
.tag(1, WireType.LengthDelimited)
.string("Homer")
.tag(3, WireType.Varint)
.bool(true)
.finish();
const user = fromBinary(UserSchema, bytes);

Use the message-level helpers unless you are working directly with the wire format.

Protobuf-ES uses the WHATWG Text Encoding API to convert UTF-8 to and from binary.

If your environment does not provide the API, call configureTextEncoding() from @bufbuild/protobuf/wire early during initialization and supply your own implementation.

Use the helpers from @bufbuild/protobuf/wire when you need a portable Base64 representation of binary data:

import { base64Encode, base64Decode } from "@bufbuild/protobuf/wire";
base64Encode(new Uint8Array([2, 4, 8, 16])); // "AgQIEA=="
base64Decode("AgQIEA=="); // Uint8Array(4)

Protobuf-ES supports the size-delimited format used to write multiple messages to a stream.

A size-delimited message is a varint length prefix followed by that many bytes of a normal binary Protobuf message.

Serialize with sizeDelimitedEncode():

import { sizeDelimitedEncode } from "@bufbuild/protobuf/wire";
import { type User, UserSchema } from "./gen/example_pb";
import { createWriteStream } from "node:fs";
declare const user: User;
const stream = createWriteStream("delim.bin", { encoding: "binary" });
stream.write(sizeDelimitedEncode(UserSchema, user));
stream.end();

Parse a stream with sizeDelimitedDecodeStream():

import { sizeDelimitedDecodeStream } from "@bufbuild/protobuf/wire";
import { createReadStream } from "node:fs";
import { UserSchema } from "./gen/example_pb";
const stream = createReadStream("delim.bin");
for await (const user of sizeDelimitedDecodeStream(UserSchema, stream)) {
console.log(user);
}

This format is compatible with the delimited message support in the C++, Java, Go, and other Protobuf runtimes.