Skip to content

Protobuf can be extended at compile time and at schema-definition time. This page covers plugins, extensions, and custom options.

Everything on this page works because the schema itself is data. When the compiler parses a .proto file, it produces descriptors, Protobuf messages that describe every file, message, field, and service in the schema. Each mechanism on this page reads or extends those descriptors.

Plugins consume descriptors at build time. The compiler parses your schema, serializes the descriptors into a CodeGeneratorRequest, and pipes it to a plugin: an ordinary program, in any language, that returns the files it wants to generate. The official code generators work this way, and you can write your own that emits documentation, SQL schemas, or API clients instead.

Extensions widen messages you don't own. A message can reserve a range of field numbers for external use, and other files then declare fields in that range without touching the original definition. The base message's schema doesn't change, but the messages it describes can carry the new fields.

Custom options are built on extensions and descriptors. The descriptor types that hold options, like FieldOptions and MethodOptions, declare extension ranges of their own, so extending them attaches your metadata to any schema element. That metadata travels inside the descriptors, where plugins and runtime reflection can read it back. protovalidate uses custom options to attach validation rules to fields.

protoc has generators for a few languages built in, such as C++, Java, and Python. Everything else, Go and TypeScript included, comes from a plugin: the compiler (protoc or buf generate) parses the .proto files and hands the resulting descriptors to a separate program.

A plugin is just an executable that reads a CodeGeneratorRequest and writes a CodeGeneratorResponse, both defined in google/protobuf/compiler/plugin.proto.

# buf.gen.yaml
version: v2
plugins:
  - local: ./my-plugin
    out: ./generated
    opt: log_level=debug,other_flag=true

stdin, stdout, stderr

The compiler starts the plugin program as a subprocess.

  • stdin: The compiler passes a binary serialized CodeGeneratorRequest message.
  • stdout: The plugin must return a binary serialized CodeGeneratorResponse message. The plugin must not modify the filesystem directly; it returns the files to be written in this response.
  • stderr: For logs and error messages only. Keep logging off by default and turn it on with a flag, so normal runs stay quiet.

Request and response

Options passed via --<plugin>_opt arrive in the parameter field of the CodeGeneratorRequest as a single comma-separated string. The plugin must parse and split it.

The compiler passes many files (including dependencies), but the plugin must only generate code for the files listed in the file_to_generate field of the request.

In the CodeGeneratorResponse, declare the features your plugin supports. Setting supported_features along with minimum_edition and maximum_edition is required to compile files that use Protobuf Editions. Without them, the compiler refuses to run the plugin on those files, since it can't tell whether the plugin understands their features.

Where you can use extensions depends on the syntax:

  • proto2: Allows extensions on any message (both user-defined messages and standard options).
  • proto3: Restricts extensions exclusively to option messages (specifically to define custom options; more on that later).
  • Editions: Restores the ability to extend any message (bringing back general-purpose extensions) while keeping option definitions standard and native.

To use extensions in proto2 or Editions, you must define an extension range in the base message using the extensions keyword. External files can then declare fields targeting that range.

Extension Numbers are Field Numbers

Extension numbers are ordinary field numbers, and they share the base message's tag space on the wire. No two extensions of the same message can use the same number, or their data collides and gets misread.

base.proto (definition)
edition = "2024";

message UserProfile {
  string username = 1;

  // Declare range of tags reserved for third-party extensions
  extensions 100 to 199;
}
billing.proto (extension)
edition = "2024";
import "base.proto";

// Extend the custom UserProfile message directly
extend UserProfile {
  string stripe_customer_id = 100;
}

Teams use them for validation rules (protovalidate), for tagging fields that hold PII, and for access control, like the roles an RPC requires.

The compiler records these annotations in the descriptors, so any program that reads your schema can see them: a protoc plugin generating custom code, a service that configures itself at startup, or a tool that loads schemas at runtime through reflection.

A custom option is an extension of one of the built-in option messages, such as FieldOptions or MethodOptions, declared with the extend keyword.

Available Scopes

FileMessageFieldOneofEnumEnumValueServiceMethod

Metadata can be attached to any of these points by extending the respective standard descriptor messages.

For more information, see the Editions Custom Options Guide.

Extension Registries

Every extension of a descriptor message such as FieldOptions shares one set of field numbers, so your extension numbers can't overlap with anyone else's.

Google maintains a Global Extension Registry for public projects.

options.proto (definition)
edition = "2024";
import "google/protobuf/descriptor.proto";

extend google.protobuf.FieldOptions {
  bool is_pii = 50001;
}

extend google.protobuf.MethodOptions {
  string required_role = 50002;
}
service.proto (usage)
edition = "2024";
import "options.proto";

service UserService {
  rpc GetSensitiveData(GetRequest) returns (GetResponse) {
    option (required_role) = "ADMIN";
  }
}

message Profile {
  string ssn = 1 [(is_pii) = true];
}

Next

Validation

protovalidate, which declares constraints as custom options on the fields they apply to.